diff --git a/.github/workflows/release-standalone-docker-img.yml b/.github/workflows/release-standalone-docker-img.yml index 84a0aa72e..5d91bd59c 100644 --- a/.github/workflows/release-standalone-docker-img.yml +++ b/.github/workflows/release-standalone-docker-img.yml @@ -1,11 +1,17 @@ name: Release standalone docker image -on: [workflow_dispatch] +on: + push: + tags: + - "infisical/v*.*.*" jobs: infisical-standalone: name: Build infisical standalone image runs-on: ubuntu-latest steps: + - name: Extract version from tag + id: extract_version + run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}" - name: ☁️ Checkout source uses: actions/checkout@v3 with: @@ -64,5 +70,6 @@ jobs: tags: | infisical/infisical:latest infisical/infisical:${{ steps.commit.outputs.short }} + infisical/infisical:${{ steps.extract_version.outputs.version }} platforms: linux/amd64,linux/arm64 file: Dockerfile.standalone-infisical diff --git a/README.md b/README.md index c5a0d3abc..b0b964d37 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

- Slack | + Slack | Infisical Cloud | Self-Hosting | Docs | @@ -36,7 +36,7 @@ Cloudsmith downloads - + Slack community channel @@ -135,15 +135,15 @@ Whether it's big or small, we love contributions. Check out our guide to see how Not sure where to get started? You can: - [Book a free, non-pressure pairing session / code walkthrough with one of our teammates](https://cal.com/tony-infisical/30-min-meeting-contributing)! -- Join our Slack, and ask us any questions there. +- Join our Slack, and ask us any questions there. ## Resources - [Docs](https://infisical.com/docs/documentation/getting-started/introduction) for comprehensive documentation and guides -- [Slack](https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg) for discussion with the community and Infisical team. +- [Slack](https://infisical.com/slack) for discussion with the community and Infisical team. - [GitHub](https://github.com/Infisical/infisical) for code, issues, and pull requests - [Twitter](https://twitter.com/infisical) for fast news -- [YouTube](https://www.youtube.com/@infisical_od) for videos on secret management +- [YouTube](https://www.youtube.com/@infisical_os) for videos on secret management - [Blog](https://infisical.com/blog) for secret management insights, articles, tutorials, and updates - [Roadmap](https://www.notion.so/infisical/be2d2585a6694e40889b03aef96ea36b?v=5b19a8127d1a4060b54769567a8785fa) for planned features diff --git a/backend/src/controllers/v1/index.ts b/backend/src/controllers/v1/index.ts index 488c9c6f2..a2664c7b0 100644 --- a/backend/src/controllers/v1/index.ts +++ b/backend/src/controllers/v1/index.ts @@ -14,22 +14,24 @@ import * as userActionController from "./userActionController"; import * as userController from "./userController"; import * as workspaceController from "./workspaceController"; import * as secretScanningController from "./secretScanningController"; +import * as webhookController from "./webhookController"; export { - authController, - botController, - integrationAuthController, - integrationController, - keyController, - membershipController, - membershipOrgController, - organizationController, - passwordController, - secretController, - serviceTokenController, - signupController, - userActionController, - userController, - workspaceController, - secretScanningController + authController, + botController, + integrationAuthController, + integrationController, + keyController, + membershipController, + membershipOrgController, + organizationController, + passwordController, + secretController, + serviceTokenController, + signupController, + userActionController, + userController, + workspaceController, + secretScanningController, + webhookController }; diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index 63828ef14..7ab79976b 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -2,7 +2,7 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { Integration } from "../../models"; import { EventService } from "../../services"; -import { eventPushSecrets } from "../../events"; +import { eventPushSecrets, eventStartIntegration } from "../../events"; import Folder from "../../models/folder"; import { getFolderByPath } from "../../services/FolderService"; import { BadRequestError } from "../../utils/errors"; @@ -27,19 +27,19 @@ export const createIntegration = async (req: Request, res: Response) => { owner, path, region, - secretPath, + secretPath } = req.body; const folders = await Folder.findOne({ workspace: req.integrationAuth.workspace._id, - environment: sourceEnvironment, + environment: sourceEnvironment }); if (folders) { const folder = getFolderByPath(folders.nodes, secretPath); if (!folder) { throw BadRequestError({ - message: "Path for service token does not exist", + message: "Path for service token does not exist" }); } } @@ -62,21 +62,21 @@ export const createIntegration = async (req: Request, res: Response) => { region, secretPath, integration: req.integrationAuth.integration, - integrationAuth: new Types.ObjectId(integrationAuthId), + integrationAuth: new Types.ObjectId(integrationAuthId) }).save(); if (integration) { // trigger event - push secrets EventService.handleEvent({ - event: eventPushSecrets({ + event: eventStartIntegration({ workspaceId: integration.workspace, - environment: sourceEnvironment, - }), + environment: sourceEnvironment + }) }); } return res.status(200).send({ - integration, + integration }); }; @@ -97,26 +97,26 @@ export const updateIntegration = async (req: Request, res: Response) => { appId, targetEnvironment, owner, // github-specific integration param - secretPath, + secretPath } = req.body; const folders = await Folder.findOne({ workspace: req.integration.workspace, - environment, + environment }); if (folders) { const folder = getFolderByPath(folders.nodes, secretPath); if (!folder) { throw BadRequestError({ - message: "Path for service token does not exist", + message: "Path for service token does not exist" }); } } const integration = await Integration.findOneAndUpdate( { - _id: req.integration._id, + _id: req.integration._id }, { environment, @@ -125,25 +125,25 @@ export const updateIntegration = async (req: Request, res: Response) => { appId, targetEnvironment, owner, - secretPath, + secretPath }, { - new: true, + new: true } ); if (integration) { // trigger event - push secrets EventService.handleEvent({ - event: eventPushSecrets({ + event: eventStartIntegration({ workspaceId: integration.workspace, - environment, - }), + environment + }) }); } return res.status(200).send({ - integration, + integration }); }; @@ -158,12 +158,12 @@ export const deleteIntegration = async (req: Request, res: Response) => { const { integrationId } = req.params; const integration = await Integration.findOneAndDelete({ - _id: integrationId, + _id: integrationId }); if (!integration) throw new Error("Failed to find integration"); return res.status(200).send({ - integration, + integration }); }; diff --git a/backend/src/controllers/v1/secretController.ts b/backend/src/controllers/v1/secretController.ts index bcb00d209..cda7b5576 100644 --- a/backend/src/controllers/v1/secretController.ts +++ b/backend/src/controllers/v1/secretController.ts @@ -80,7 +80,8 @@ export const pushSecrets = async (req: Request, res: Response) => { EventService.handleEvent({ event: eventPushSecrets({ workspaceId: new Types.ObjectId(workspaceId), - environment + environment, + secretPath: "/" }) }); diff --git a/backend/src/controllers/v1/webhookController.ts b/backend/src/controllers/v1/webhookController.ts new file mode 100644 index 000000000..c4957fffe --- /dev/null +++ b/backend/src/controllers/v1/webhookController.ts @@ -0,0 +1,140 @@ +import { Request, Response } from "express"; +import { Types } from "mongoose"; +import { client, getRootEncryptionKey } from "../../config"; +import { validateMembership } from "../../helpers"; +import Webhook from "../../models/webhooks"; +import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService"; +import { BadRequestError } from "../../utils/errors"; +import { ADMIN, ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, MEMBER } from "../../variables"; + +export const createWebhook = async (req: Request, res: Response) => { + const { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath } = req.body; + const webhook = new Webhook({ + workspace: workspaceId, + environment, + secretPath, + url: webhookUrl, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_BASE64 + }); + + if (webhookSecretKey) { + const rootEncryptionKey = await getRootEncryptionKey(); + const { ciphertext, iv, tag } = client.encryptSymmetric(webhookSecretKey, rootEncryptionKey); + webhook.iv = iv; + webhook.tag = tag; + webhook.encryptedSecretKey = ciphertext; + } + + await webhook.save(); + + return res.status(200).send({ + webhook, + message: "successfully created webhook" + }); +}; + +export const updateWebhook = async (req: Request, res: Response) => { + const { webhookId } = req.params; + const { isDisabled } = req.body; + const webhook = await Webhook.findById(webhookId); + if (!webhook) { + throw BadRequestError({ message: "Webhook not found!!" }); + } + + // check that user is a member of the workspace + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: webhook.workspace, + acceptedRoles: [ADMIN, MEMBER] + }); + + if (typeof isDisabled !== undefined) { + webhook.isDisabled = isDisabled; + } + await webhook.save(); + + return res.status(200).send({ + webhook, + message: "successfully updated webhook" + }); +}; + +export const deleteWebhook = async (req: Request, res: Response) => { + const { webhookId } = req.params; + const webhook = await Webhook.findById(webhookId); + if (!webhook) { + throw BadRequestError({ message: "Webhook not found!!" }); + } + + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: webhook.workspace, + acceptedRoles: [ADMIN, MEMBER] + }); + await webhook.remove(); + + return res.status(200).send({ + message: "successfully removed webhook" + }); +}; + +export const testWebhook = async (req: Request, res: Response) => { + const { webhookId } = req.params; + const webhook = await Webhook.findById(webhookId); + if (!webhook) { + throw BadRequestError({ message: "Webhook not found!!" }); + } + + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: webhook.workspace, + acceptedRoles: [ADMIN, MEMBER] + }); + + try { + await triggerWebhookRequest( + webhook, + getWebhookPayload( + "test", + webhook.workspace.toString(), + webhook.environment, + webhook.secretPath + ) + ); + await Webhook.findByIdAndUpdate(webhookId, { + lastStatus: "success", + lastRunErrorMessage: null + }); + } catch (err) { + await Webhook.findByIdAndUpdate(webhookId, { + lastStatus: "failed", + lastRunErrorMessage: (err as Error).message + }); + return res.status(400).send({ + message: "Failed to receive response", + error: (err as Error).message + }); + } + + return res.status(200).send({ + message: "Successfully received response" + }); +}; + +export const listWebhooks = async (req: Request, res: Response) => { + const { environment, workspaceId, secretPath } = req.query; + + const optionalFilters: Record = {}; + if (environment) optionalFilters.environment = environment as string; + if (secretPath) optionalFilters.secretPath = secretPath as string; + + const webhooks = await Webhook.find({ + workspace: new Types.ObjectId(workspaceId as string), + ...optionalFilters + }); + + return res.status(200).send({ + webhooks + }); +}; diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index b93846e70..cc1aae055 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -30,9 +30,11 @@ import Folder from "../../models/folder"; import { getFolderByPath, getFolderIdFromServiceToken, - searchByFolderId + searchByFolderId, + searchByFolderIdWithDir } from "../../services/FolderService"; import { isValidScope } from "../../helpers/secrets"; +import path from "path"; /** * Peform a batch of any specified CUD secret operations @@ -47,14 +49,13 @@ export const batchSecrets = async (req: Request, res: Response) => { const { workspaceId, environment, - requests, - secretPath + requests }: { workspaceId: string; environment: string; requests: BatchSecretRequest[]; - secretPath: string; } = req.body; + let secretPath = req.body.secretPath as string; let folderId = req.body.folderId as string; const createSecrets: BatchSecret[] = []; @@ -68,10 +69,6 @@ export const batchSecrets = async (req: Request, res: Response) => { }); const folders = await Folder.findOne({ workspace: workspaceId, environment }); - if (folders && folderId !== "root") { - const folder = searchByFolderId(folders.nodes, folderId as string); - if (!folder) throw BadRequestError({ message: "Folder not found" }); - } if (req.authData.authPayload instanceof ServiceTokenData) { const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); @@ -87,6 +84,15 @@ export const batchSecrets = async (req: Request, res: Response) => { folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); } + if (folders && folderId !== "root") { + const folder = searchByFolderIdWithDir(folders.nodes, folderId as string); + if (!folder?.folder) throw BadRequestError({ message: "Folder not found" }); + secretPath = path.join( + "/", + ...folder.dir.map(({ name }) => name).filter((name) => name !== "root") + ); + } + for await (const request of requests) { // do a validation @@ -319,7 +325,10 @@ export const batchSecrets = async (req: Request, res: Response) => { // // trigger event - push secrets await EventService.handleEvent({ event: eventPushSecrets({ - workspaceId: new Types.ObjectId(workspaceId) + workspaceId: new Types.ObjectId(workspaceId), + environment, + // root condition else this will be filled according to the path or folderid + secretPath: secretPath || "/" }) }); @@ -535,7 +544,9 @@ export const createSecrets = async (req: Request, res: Response) => { // trigger event - push secrets await EventService.handleEvent({ event: eventPushSecrets({ - workspaceId: new Types.ObjectId(workspaceId) + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath: secretPath || "/" }) }); }, 5000); @@ -1033,13 +1044,16 @@ export const updateSecrets = async (req: Request, res: Response) => { Object.keys(workspaceSecretObj).forEach(async (key) => { // trigger event - push secrets - setTimeout(async () => { - await EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId: new Types.ObjectId(key) - }) - }); - }, 10000); + // This route is not used anymore thus keep it commented out as it does not expose environment + // it will end up creating a lot of requests from the server + // setTimeout(async () => { + // await EventService.handleEvent({ + // event: eventPushSecrets({ + // workspaceId: new Types.ObjectId(key), + // environment, + // }) + // }); + // }, 10000); const updateAction = await EELogService.createAction({ name: ACTION_UPDATE_SECRETS, @@ -1174,11 +1188,13 @@ export const deleteSecrets = async (req: Request, res: Response) => { Object.keys(workspaceSecretObj).forEach(async (key) => { // trigger event - push secrets - await EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId: new Types.ObjectId(key) - }) - }); + // DEPRECIATED(akhilmhdh): as this would cause server to send so many request + // and this route is not used anymore thus like snapshot keeping it commented out + // await EventService.handleEvent({ + // event: eventPushSecrets({ + // workspaceId: new Types.ObjectId(key) + // }) + // }); const deleteAction = await EELogService.createAction({ name: ACTION_DELETE_SECRETS, userId: req.user?._id, diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index c0d46f851..b90b1a47b 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -1,34 +1,29 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; +import { Key, Membership, ServiceTokenData, Workspace } from "../../models"; import { - Key, - Membership, - ServiceTokenData, - Workspace, -} from "../../models"; -import { - pullSecrets as pull, - v2PushSecrets as push, - reformatPullSecrets, + pullSecrets as pull, + v2PushSecrets as push, + reformatPullSecrets } from "../../helpers/secret"; import { pushKeys } from "../../helpers/key"; import { EventService, TelemetryService } from "../../services"; import { eventPushSecrets } from "../../events"; interface V2PushSecret { - type: string; // personal or shared - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretKeyHash: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - secretValueHash: string; - secretCommentCiphertext?: string; - secretCommentIV?: string; - secretCommentTag?: string; - secretCommentHash?: string; + type: string; // personal or shared + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretKeyHash: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretValueHash: string; + secretCommentCiphertext?: string; + secretCommentIV?: string; + secretCommentTag?: string; + secretCommentHash?: string; } /** @@ -39,7 +34,7 @@ interface V2PushSecret { * @returns */ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { - // upload (encrypted) secrets to workspace with id [workspaceId] + // upload (encrypted) secrets to workspace with id [workspaceId] const postHogClient = await TelemetryService.getPostHogClient(); let { secrets }: { secrets: V2PushSecret[] } = req.body; const { keys, environment, channel } = req.body; @@ -62,13 +57,13 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { environment, secrets, channel: channel ? channel : "cli", - ipAddress: req.realIP, + ipAddress: req.realIP }); await pushKeys({ userId: req.user._id, workspaceId, - keys, + keys }); if (postHogClient) { @@ -79,8 +74,8 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { numberOfSecrets: secrets.length, environment, workspaceId, - channel: channel ? channel : "cli", - }, + channel: channel ? channel : "cli" + } }); } @@ -89,12 +84,13 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { event: eventPushSecrets({ workspaceId: new Types.ObjectId(workspaceId), environment, - }), + secretPath: "/" + }) }); - return res.status(200).send({ - message: "Successfully uploaded workspace secrets", - }); + return res.status(200).send({ + message: "Successfully uploaded workspace secrets" + }); }; /** @@ -105,7 +101,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { * @returns */ export const pullSecrets = async (req: Request, res: Response) => { - let secrets; + let secrets; const postHogClient = await TelemetryService.getPostHogClient(); const environment: string = req.query.environment as string; const channel: string = req.query.channel as string; @@ -128,7 +124,7 @@ export const pullSecrets = async (req: Request, res: Response) => { workspaceId, environment, channel: channel ? channel : "cli", - ipAddress: req.realIP, + ipAddress: req.realIP }); if (channel !== "cli") { @@ -144,18 +140,18 @@ export const pullSecrets = async (req: Request, res: Response) => { numberOfSecrets: secrets.length, environment, workspaceId, - channel: channel ? channel : "cli", - }, + channel: channel ? channel : "cli" + } }); } - return res.status(200).send({ - secrets, - }); + return res.status(200).send({ + secrets + }); }; export const getWorkspaceKey = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Return encrypted project key' #swagger.description = 'Return encrypted project key' @@ -183,43 +179,38 @@ export const getWorkspaceKey = async (req: Request, res: Response) => { } } */ - let key; + let key; const { workspaceId } = req.params; key = await Key.findOne({ workspace: workspaceId, - receiver: req.user._id, + receiver: req.user._id }).populate("sender", "+publicKey"); if (!key) throw new Error("Failed to find workspace key"); - return res.status(200).json(key); -} -export const getWorkspaceServiceTokenData = async ( - req: Request, - res: Response -) => { + return res.status(200).json(key); +}; +export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => { const { workspaceId } = req.params; - const serviceTokenData = await ServiceTokenData - .find({ - workspace: workspaceId, - }) - .select("+encryptedKey +iv +tag"); + const serviceTokenData = await ServiceTokenData.find({ + workspace: workspaceId + }).select("+encryptedKey +iv +tag"); - return res.status(200).send({ - serviceTokenData, - }); -} + return res.status(200).send({ + serviceTokenData + }); +}; /** * Return memberships for workspace with id [workspaceId] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getWorkspaceMemberships = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Return project memberships' #swagger.description = 'Return project memberships' @@ -255,22 +246,22 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => { const { workspaceId } = req.params; const memberships = await Membership.find({ - workspace: workspaceId, + workspace: workspaceId }).populate("user", "+publicKey"); - return res.status(200).send({ - memberships, - }); -} + return res.status(200).send({ + memberships + }); +}; /** * Update role of membership with id [membershipId] to role [role] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const updateWorkspaceMembership = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Update project membership' #swagger.description = 'Update project membership' @@ -323,33 +314,32 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) => } } */ - const { - membershipId, - } = req.params; + const { membershipId } = req.params; const { role } = req.body; - + const membership = await Membership.findByIdAndUpdate( membershipId, { - role, - }, { - new: true, + role + }, + { + new: true } ); - return res.status(200).send({ - membership, - }); -} + return res.status(200).send({ + membership + }); +}; /** * Delete workspace membership with id [membershipId] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const deleteWorkspaceMembership = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Delete project membership' #swagger.description = 'Delete project membership' @@ -385,23 +375,21 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) => } } */ - const { - membershipId, - } = req.params; - + const { membershipId } = req.params; + const membership = await Membership.findByIdAndDelete(membershipId); - + if (!membership) throw new Error("Failed to delete workspace membership"); - + await Key.deleteMany({ receiver: membership.user, - workspace: membership.workspace, + workspace: membership.workspace }); - - return res.status(200).send({ - membership, - }); -} + + return res.status(200).send({ + membership + }); +}; /** * Change autoCapitilzation Rule of workspace @@ -415,18 +403,18 @@ export const toggleAutoCapitalization = async (req: Request, res: Response) => { const workspace = await Workspace.findOneAndUpdate( { - _id: workspaceId, + _id: workspaceId }, { - autoCapitalization, + autoCapitalization }, { - new: true, + new: true } ); - return res.status(200).send({ - message: "Successfully changed autoCapitalization setting", - workspace, - }); + return res.status(200).send({ + message: "Successfully changed autoCapitalization setting", + workspace + }); }; diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index 3c7336241..e1edd95ad 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -21,22 +21,22 @@ export const getSecretsRaw = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), environment, secretPath, - authData: req.authData, + authData: req.authData }); const key = await BotService.getWorkspaceKeyWithBot({ - workspaceId: new Types.ObjectId(workspaceId), + workspaceId: new Types.ObjectId(workspaceId) }); return res.status(200).send({ secrets: secrets.map((secret) => { const rep = repackageSecretToRaw({ secret, - key, + key }); return rep; - }), + }) }); }; @@ -58,54 +58,47 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => { environment, type, secretPath, - authData: req.authData, + authData: req.authData }); const key = await BotService.getWorkspaceKeyWithBot({ - workspaceId: new Types.ObjectId(workspaceId), + workspaceId: new Types.ObjectId(workspaceId) }); return res.status(200).send({ secret: repackageSecretToRaw({ secret, - key, - }), + key + }) }); }; /** * Create secret with name [secretName] in plaintext * @param req - * @param res + * @param res */ export const createSecretRaw = async (req: Request, res: Response) => { const { secretName } = req.params; - const { - workspaceId, - environment, - type, - secretValue, - secretComment, - secretPath = "/", - } = req.body; + const { workspaceId, environment, type, secretValue, secretComment, secretPath = "/" } = req.body; const key = await BotService.getWorkspaceKeyWithBot({ - workspaceId: new Types.ObjectId(workspaceId), + workspaceId: new Types.ObjectId(workspaceId) }); const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8({ plaintext: secretName, - key, + key }); const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({ plaintext: secretValue, - key, + key }); const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({ plaintext: secretComment, - key, + key }); const secret = await SecretService.createSecret({ @@ -123,14 +116,15 @@ export const createSecretRaw = async (req: Request, res: Response) => { secretPath, secretCommentCiphertext: secretCommentEncrypted.ciphertext, secretCommentIV: secretCommentEncrypted.iv, - secretCommentTag: secretCommentEncrypted.tag, + secretCommentTag: secretCommentEncrypted.tag }); await EventService.handleEvent({ event: eventPushSecrets({ workspaceId: new Types.ObjectId(workspaceId), environment, - }), + secretPath + }) }); const secretWithoutBlindIndex = secret.toObject(); @@ -139,10 +133,10 @@ export const createSecretRaw = async (req: Request, res: Response) => { return res.status(200).send({ secret: repackageSecretToRaw({ secret: secretWithoutBlindIndex, - key, - }), + key + }) }); -} +}; /** * Update secret with name [secretName] @@ -151,21 +145,15 @@ export const createSecretRaw = async (req: Request, res: Response) => { */ export const updateSecretByNameRaw = async (req: Request, res: Response) => { const { secretName } = req.params; - const { - workspaceId, - environment, - type, - secretValue, - secretPath = "/", - } = req.body; + const { workspaceId, environment, type, secretValue, secretPath = "/" } = req.body; const key = await BotService.getWorkspaceKeyWithBot({ - workspaceId: new Types.ObjectId(workspaceId), + workspaceId: new Types.ObjectId(workspaceId) }); const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({ plaintext: secretValue, - key, + key }); const secret = await SecretService.updateSecret({ @@ -177,21 +165,22 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => { secretValueCiphertext: secretValueEncrypted.ciphertext, secretValueIV: secretValueEncrypted.iv, secretValueTag: secretValueEncrypted.tag, - secretPath, + secretPath }); await EventService.handleEvent({ event: eventPushSecrets({ workspaceId: new Types.ObjectId(workspaceId), environment, - }), + secretPath + }) }); return res.status(200).send({ secret: repackageSecretToRaw({ secret, - key, - }), + key + }) }); }; @@ -202,12 +191,7 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => { */ export const deleteSecretByNameRaw = async (req: Request, res: Response) => { const { secretName } = req.params; - const { - workspaceId, - environment, - type, - secretPath = "/", - } = req.body; + const { workspaceId, environment, type, secretPath = "/" } = req.body; const { secret } = await SecretService.deleteSecret({ secretName, @@ -215,25 +199,26 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => { environment, type, authData: req.authData, - secretPath, + secretPath }); await EventService.handleEvent({ event: eventPushSecrets({ workspaceId: new Types.ObjectId(workspaceId), environment, - }), + secretPath + }) }); const key = await BotService.getWorkspaceKeyWithBot({ - workspaceId: new Types.ObjectId(workspaceId), + workspaceId: new Types.ObjectId(workspaceId) }); return res.status(200).send({ secret: repackageSecretToRaw({ secret, - key, - }), + key + }) }); }; @@ -252,11 +237,11 @@ export const getSecrets = async (req: Request, res: Response) => { workspaceId: new Types.ObjectId(workspaceId), environment, secretPath, - authData: req.authData, + authData: req.authData }); return res.status(200).send({ - secrets, + secrets }); }; @@ -278,11 +263,11 @@ export const getSecretByName = async (req: Request, res: Response) => { environment, type, secretPath, - authData: req.authData, + authData: req.authData }); return res.status(200).send({ - secret, + secret }); }; @@ -306,7 +291,7 @@ export const createSecret = async (req: Request, res: Response) => { secretCommentCiphertext, secretCommentIV, secretCommentTag, - secretPath = "/", + secretPath = "/" } = req.body; const secret = await SecretService.createSecret({ @@ -324,25 +309,25 @@ export const createSecret = async (req: Request, res: Response) => { secretPath, secretCommentCiphertext, secretCommentIV, - secretCommentTag, + secretCommentTag }); await EventService.handleEvent({ event: eventPushSecrets({ workspaceId: new Types.ObjectId(workspaceId), environment, - }), + secretPath + }) }); const secretWithoutBlindIndex = secret.toObject(); delete secretWithoutBlindIndex.secretBlindIndex; return res.status(200).send({ - secret: secretWithoutBlindIndex, + secret: secretWithoutBlindIndex }); }; - /** * Update secret with name [secretName] * @param req @@ -357,7 +342,7 @@ export const updateSecretByName = async (req: Request, res: Response) => { secretValueCiphertext, secretValueIV, secretValueTag, - secretPath = "/", + secretPath = "/" } = req.body; const secret = await SecretService.updateSecret({ @@ -369,18 +354,19 @@ export const updateSecretByName = async (req: Request, res: Response) => { secretValueCiphertext, secretValueIV, secretValueTag, - secretPath, + secretPath }); await EventService.handleEvent({ event: eventPushSecrets({ workspaceId: new Types.ObjectId(workspaceId), environment, - }), + secretPath + }) }); return res.status(200).send({ - secret, + secret }); }; @@ -391,12 +377,7 @@ export const updateSecretByName = async (req: Request, res: Response) => { */ export const deleteSecretByName = async (req: Request, res: Response) => { const { secretName } = req.params; - const { - workspaceId, - environment, - type, - secretPath = "/", - } = req.body; + const { workspaceId, environment, type, secretPath = "/" } = req.body; const { secret } = await SecretService.deleteSecret({ secretName, @@ -404,17 +385,18 @@ export const deleteSecretByName = async (req: Request, res: Response) => { environment, type, authData: req.authData, - secretPath, + secretPath }); await EventService.handleEvent({ event: eventPushSecrets({ workspaceId: new Types.ObjectId(workspaceId), environment, - }), + secretPath + }) }); return res.status(200).send({ - secret, + secret }); }; diff --git a/backend/src/events/index.ts b/backend/src/events/index.ts index d8198d2fb..ac9ad176d 100644 --- a/backend/src/events/index.ts +++ b/backend/src/events/index.ts @@ -1,5 +1,4 @@ -import { eventPushSecrets } from "./secret" +import { eventPushSecrets } from "./secret"; +import { eventStartIntegration } from "./integration"; -export { - eventPushSecrets, -} \ No newline at end of file +export { eventPushSecrets, eventStartIntegration }; diff --git a/backend/src/events/integration.ts b/backend/src/events/integration.ts new file mode 100644 index 000000000..746858e46 --- /dev/null +++ b/backend/src/events/integration.ts @@ -0,0 +1,23 @@ +import { Types } from "mongoose"; +import { EVENT_START_INTEGRATION } from "../variables"; + +/* + * Return event for starting integrations + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace to push secrets to + * @returns + */ +export const eventStartIntegration = ({ + workspaceId, + environment +}: { + workspaceId: Types.ObjectId; + environment: string; +}) => { + return { + name: EVENT_START_INTEGRATION, + workspaceId, + environment, + payload: {} + }; +}; diff --git a/backend/src/events/secret.ts b/backend/src/events/secret.ts index 23ff9f59c..894e3300d 100644 --- a/backend/src/events/secret.ts +++ b/backend/src/events/secret.ts @@ -1,64 +1,54 @@ import { Types } from "mongoose"; -import { - EVENT_PULL_SECRETS, - EVENT_PUSH_SECRETS, -} from "../variables"; +import { EVENT_PULL_SECRETS, EVENT_PUSH_SECRETS } from "../variables"; interface PushSecret { - ciphertextKey: string; - ivKey: string; - tagKey: string; - hashKey: string; - ciphertextValue: string; - ivValue: string; - tagValue: string; - hashValue: string; - type: "shared" | "personal"; + ciphertextKey: string; + ivKey: string; + tagKey: string; + hashKey: string; + ciphertextValue: string; + ivValue: string; + tagValue: string; + hashValue: string; + type: "shared" | "personal"; } /** * Return event for pushing secrets * @param {Object} obj * @param {String} obj.workspaceId - id of workspace to push secrets to - * @returns + * @returns */ const eventPushSecrets = ({ + workspaceId, + environment, + secretPath +}: { + workspaceId: Types.ObjectId; + environment: string; + secretPath: string; +}) => { + return { + name: EVENT_PUSH_SECRETS, workspaceId, environment, -}: { - workspaceId: Types.ObjectId; - environment?: string; -}) => { - return ({ - name: EVENT_PUSH_SECRETS, - workspaceId, - environment, - payload: { - - }, - }); -} + secretPath, + payload: {} + }; +}; /** * Return event for pulling secrets * @param {Object} obj * @param {String} obj.workspaceId - id of workspace to pull secrets from - * @returns + * @returns */ -const eventPullSecrets = ({ +const eventPullSecrets = ({ workspaceId }: { workspaceId: string }) => { + return { + name: EVENT_PULL_SECRETS, workspaceId, -}: { - workspaceId: string; -}) => { - return ({ - name: EVENT_PULL_SECRETS, - workspaceId, - payload: { + payload: {} + }; +}; - }, - }); -} - -export { - eventPushSecrets, -} +export { eventPushSecrets }; diff --git a/backend/src/helpers/event.ts b/backend/src/helpers/event.ts index 124da257c..0521d6b85 100644 --- a/backend/src/helpers/event.ts +++ b/backend/src/helpers/event.ts @@ -1,12 +1,14 @@ import { Types } from "mongoose"; import { Bot } from "../models"; -import { EVENT_PUSH_SECRETS } from "../variables"; +import { EVENT_PUSH_SECRETS, EVENT_START_INTEGRATION } from "../variables"; import { IntegrationService } from "../services"; +import { triggerWebhook } from "../services/WebhookService"; interface Event { name: string; workspaceId: Types.ObjectId; environment?: string; + secretPath?: string; payload: any; } @@ -19,22 +21,31 @@ interface Event { * @param {Object} obj.event.payload - payload of event (depends on event) */ export const handleEventHelper = async ({ event }: { event: Event }) => { - const { workspaceId, environment } = event; + const { workspaceId, environment, secretPath } = event; // TODO: moduralize bot check into separate function const bot = await Bot.findOne({ workspace: workspaceId, - isActive: true, + isActive: true }); - if (!bot) return; - switch (event.name) { case EVENT_PUSH_SECRETS: - IntegrationService.syncIntegrations({ - workspaceId, - environment, - }); + if (bot) { + await IntegrationService.syncIntegrations({ + workspaceId, + environment + }); + } + triggerWebhook(workspaceId.toString(), environment || "", secretPath || ""); + break; + case EVENT_START_INTEGRATION: + if (bot) { + IntegrationService.syncIntegrations({ + workspaceId, + environment + }); + } break; } -}; \ No newline at end of file +}; diff --git a/backend/src/index.ts b/backend/src/index.ts index 93bdb937c..fe40a0a43 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -21,7 +21,7 @@ import { organizations as eeOrganizationsRouter, secret as eeSecretRouter, secretSnapshot as eeSecretSnapshotRouter, - workspace as eeWorkspaceRouter, + workspace as eeWorkspaceRouter } from "./ee/routes/v1"; import { auth as v1AuthRouter, @@ -42,6 +42,7 @@ import { userAction as v1UserActionRouter, user as v1UserRouter, workspace as v1WorkspaceRouter, + webhooks as v1WebhooksRouter } from "./routes/v1"; import { auth as v2AuthRouter, @@ -54,13 +55,13 @@ import { serviceTokenData as v2ServiceTokenDataRouter, serviceAccounts as v2ServiceAccountsRouter, environment as v2EnvironmentRouter, - tags as v2TagsRouter, + tags as v2TagsRouter } from "./routes/v2"; import { auth as v3AuthRouter, secrets as v3SecretsRouter, signup as v3SignupRouter, - workspaces as v3WorkspacesRouter, + workspaces as v3WorkspacesRouter } from "./routes/v3"; import { healthCheck } from "./routes/status"; import { getLogger } from "./utils/logger"; @@ -83,7 +84,7 @@ const main = async () => { app.use( cors({ credentials: true, - origin: await getSiteURL(), + origin: await getSiteURL() }) ); @@ -149,6 +150,7 @@ const main = async () => { app.use("/api/v1/integration-auth", v1IntegrationAuthRouter); app.use("/api/v1/folders", v1SecretsFolder); app.use("/api/v1/secret-scanning", v1SecretScanningRouter); + app.use("/api/v1/webhooks", v1WebhooksRouter); // v2 routes (improvements) app.use("/api/v2/signup", v2SignupRouter); @@ -180,7 +182,7 @@ const main = async () => { if (res.headersSent) return next(); next( RouteNotFoundError({ - message: `The requested source '(${req.method})${req.url}' was not found`, + message: `The requested source '(${req.method})${req.url}' was not found` }) ); }); @@ -188,9 +190,7 @@ const main = async () => { app.use(requestErrorHandler); const server = app.listen(await getPort(), async () => { - (await getLogger("backend-main")).info( - `Server started listening at port ${await getPort()}` - ); + (await getLogger("backend-main")).info(`Server started listening at port ${await getPort()}`); }); // await createTestUserForDevelopment(); diff --git a/backend/src/models/webhooks.ts b/backend/src/models/webhooks.ts new file mode 100644 index 000000000..b4a168878 --- /dev/null +++ b/backend/src/models/webhooks.ts @@ -0,0 +1,85 @@ +import { Document, Schema, Types, model } from "mongoose"; +import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8 } from "../variables"; + +export interface IWebhook extends Document { + _id: Types.ObjectId; + workspace: Types.ObjectId; + environment: string; + secretPath: string; + url: string; + lastStatus: "success" | "failed"; + lastRunErrorMessage?: string; + isDisabled: boolean; + encryptedSecretKey: string; + iv: string; + tag: string; + algorithm: "aes-256-gcm"; + keyEncoding: "base64" | "utf8"; +} + +const WebhookSchema = new Schema( + { + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true + }, + environment: { + type: String, + required: true + }, + secretPath: { + type: String, + required: true, + default: "/" + }, + url: { + type: String, + required: true + }, + lastStatus: { + type: String, + enum: ["success", "failed"] + }, + lastRunErrorMessage: { + type: String + }, + isDisabled: { + type: Boolean, + default: false + }, + // used for webhook signature + encryptedSecretKey: { + type: String, + select: false + }, + iv: { + type: String, + select: false + }, + tag: { + type: String, + select: false + }, + algorithm: { + // the encryption algorithm used + type: String, + enum: [ALGORITHM_AES_256_GCM], + required: true, + select: false + }, + keyEncoding: { + type: String, + enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], + required: true, + select: false + } + }, + { + timestamps: true + } +); + +const Webhook = model("Webhook", WebhookSchema); + +export default Webhook; diff --git a/backend/src/routes/v1/index.ts b/backend/src/routes/v1/index.ts index a1015c091..b9f26d32d 100644 --- a/backend/src/routes/v1/index.ts +++ b/backend/src/routes/v1/index.ts @@ -16,24 +16,26 @@ import integration from "./integration"; import integrationAuth from "./integrationAuth"; import secretsFolder from "./secretsFolder"; import secretScanning from "./secretScanning"; +import webhooks from "./webhook"; export { - signup, - auth, - bot, - user, - userAction, - organization, - workspace, - membershipOrg, - membership, - key, - inviteOrg, - secret, - serviceToken, - password, - integration, - integrationAuth, - secretsFolder, - secretScanning + signup, + auth, + bot, + user, + userAction, + organization, + workspace, + membershipOrg, + membership, + key, + inviteOrg, + secret, + serviceToken, + password, + integration, + integrationAuth, + secretsFolder, + secretScanning, + webhooks }; diff --git a/backend/src/routes/v1/webhook.ts b/backend/src/routes/v1/webhook.ts new file mode 100644 index 000000000..2091fa447 --- /dev/null +++ b/backend/src/routes/v1/webhook.ts @@ -0,0 +1,75 @@ +import express from "express"; +const router = express.Router(); +import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware"; +import { body, param, query } from "express-validator"; +import { ADMIN, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, MEMBER } from "../../variables"; +import { webhookController } from "../../controllers/v1"; + +router.post( + "/", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "body", + locationEnvironment: "body" + }), + body("workspaceId").exists().isString().trim(), + body("environment").exists().isString().trim(), + body("webhookUrl").exists().isString().isURL().trim(), + body("webhookSecretKey").isString().trim(), + body("secretPath").default("/").isString().trim(), + validateRequest, + webhookController.createWebhook +); + +router.patch( + "/:webhookId", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT] + }), + param("webhookId").exists().isString().trim(), + body("isDisabled").default(false).isBoolean(), + validateRequest, + webhookController.updateWebhook +); + +router.post( + "/:webhookId/test", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT] + }), + param("webhookId").exists().isString().trim(), + validateRequest, + webhookController.testWebhook +); + +router.delete( + "/:webhookId", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT] + }), + param("webhookId").exists().isString().trim(), + validateRequest, + webhookController.deleteWebhook +); + +router.get( + "/", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "query", + locationEnvironment: "query" + }), + query("workspaceId").exists().isString().trim(), + query("environment").optional().isString().trim(), + query("secretPath").optional().isString().trim(), + validateRequest, + webhookController.listWebhooks +); + +export default router; diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index cd550d99e..731e58eb0 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -5,7 +5,7 @@ import { requireAuth, requireSecretsAuth, requireWorkspaceAuth, - validateRequest, + validateRequest } from "../../middleware"; import { validateClientForSecrets } from "../../validation"; import { body, query } from "express-validator"; @@ -20,22 +20,18 @@ import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS, SECRET_PERSONAL, - SECRET_SHARED, + SECRET_SHARED } from "../../variables"; import { BatchSecretRequest } from "../../types/secret"; router.post( "/batch", requireAuth({ - acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - ], + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: "body", + locationWorkspaceId: "body" }), body("workspaceId").exists().isString().trim(), body("folderId").default("root").isString().trim(), @@ -52,10 +48,8 @@ router.post( if (secretIds.length > 0) { req.secrets = await validateClientForSecrets({ authData: req.authData, - secretIds: secretIds.map( - (secretId: string) => new Types.ObjectId(secretId) - ), - requiredPermissions: [], + secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)), + requiredPermissions: [] }); } } @@ -76,14 +70,11 @@ router.post( .custom((value) => { if (Array.isArray(value)) { // case: create multiple secrets - if (value.length === 0) - throw new Error("secrets cannot be an empty array"); + if (value.length === 0) throw new Error("secrets cannot be an empty array"); for (const secret of value) { if ( !secret.type || - !( - secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED - ) || + !(secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED) || !secret.secretKeyCiphertext || !secret.secretKeyIV || !secret.secretKeyTag || @@ -108,9 +99,7 @@ router.post( !value.secretValueIV || !value.secretValueTag ) { - throw new Error( - "secrets object is missing required secret properties" - ); + throw new Error("secrets object is missing required secret properties"); } } else { throw new Error("secrets must be an object or an array of objects"); @@ -120,17 +109,13 @@ router.post( }), validateRequest, requireAuth({ - acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - ], + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], locationWorkspaceId: "body", locationEnvironment: "body", - requiredPermissions: [PERMISSION_WRITE_SECRETS], + requiredPermissions: [PERMISSION_WRITE_SECRETS] }), secretsController.createSecrets ); @@ -148,14 +133,14 @@ router.get( AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT, - ], + AUTH_MODE_SERVICE_ACCOUNT + ] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], locationWorkspaceId: "query", locationEnvironment: "query", - requiredPermissions: [PERMISSION_READ_SECRETS], + requiredPermissions: [PERMISSION_READ_SECRETS] }), secretsController.getSecrets ); @@ -167,8 +152,7 @@ router.patch( .custom((value) => { if (Array.isArray(value)) { // case: update multiple secrets - if (value.length === 0) - throw new Error("secrets cannot be an empty array"); + if (value.length === 0) throw new Error("secrets cannot be an empty array"); for (const secret of value) { if (!secret.id) { throw new Error("Each secret must contain a ID property"); @@ -187,15 +171,11 @@ router.patch( }), validateRequest, requireAuth({ - acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - ], + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN] }), requireSecretsAuth({ acceptedRoles: [ADMIN, MEMBER], - requiredPermissions: [PERMISSION_WRITE_SECRETS], + requiredPermissions: [PERMISSION_WRITE_SECRETS] }), secretsController.updateSecrets ); @@ -210,8 +190,7 @@ router.delete( if (Array.isArray(value)) { // case: delete multiple secrets - if (value.length === 0) - throw new Error("secrets cannot be an empty array"); + if (value.length === 0) throw new Error("secrets cannot be an empty array"); return value.every((id: string) => typeof id === "string"); } @@ -221,15 +200,11 @@ router.delete( .isEmpty(), validateRequest, requireAuth({ - acceptedAuthModes: [ - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - ], + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN] }), requireSecretsAuth({ acceptedRoles: [ADMIN, MEMBER], - requiredPermissions: [PERMISSION_WRITE_SECRETS], + requiredPermissions: [PERMISSION_WRITE_SECRETS] }), secretsController.deleteSecrets ); diff --git a/backend/src/services/WebhookService.ts b/backend/src/services/WebhookService.ts new file mode 100644 index 000000000..c66f44e47 --- /dev/null +++ b/backend/src/services/WebhookService.ts @@ -0,0 +1,93 @@ +import axios from "axios"; +import crypto from "crypto"; +import { Types } from "mongoose"; +import picomatch from "picomatch"; +import { client, getRootEncryptionKey } from "../config"; +import Webhook, { IWebhook } from "../models/webhooks"; + +export const triggerWebhookRequest = async ( + { url, encryptedSecretKey, iv, tag }: IWebhook, + payload: Record +) => { + const headers: Record = {}; + payload["timestamp"] = Date.now(); + + if (encryptedSecretKey) { + const rootEncryptionKey = await getRootEncryptionKey(); + const secretKey = client.decryptSymmetric(encryptedSecretKey, rootEncryptionKey, iv, tag); + const webhookSign = crypto + .createHmac("sha256", secretKey) + .update(JSON.stringify(payload)) + .digest("hex"); + headers["x-infisical-signature"] = `t=${payload["timestamp"]};${webhookSign}`; + } + + const req = await axios.post(url, payload, { headers }); + return req; +}; + +export const getWebhookPayload = ( + eventName: string, + workspaceId: string, + environment: string, + secretPath?: string +) => ({ + event: eventName, + project: { + workspaceId, + environment, + secretPath + } +}); + +export const triggerWebhook = async ( + workspaceId: string, + environment: string, + secretPath: string +) => { + const webhooks = await Webhook.find({ workspace: workspaceId, environment, isDisabled: false }); + // TODO(akhilmhdh): implement retry policy later, for that a cron job based approach is needed + // for exponential backoff + const toBeTriggeredHooks = webhooks.filter(({ secretPath: hookSecretPath }) => + picomatch.isMatch(secretPath, hookSecretPath, { strictSlashes: false }) + ); + const webhooksTriggered = await Promise.allSettled( + toBeTriggeredHooks.map((hook) => + triggerWebhookRequest( + hook, + getWebhookPayload("secrets.modified", workspaceId, environment, secretPath) + ) + ) + ); + const successWebhooks: Types.ObjectId[] = []; + const failedWebhooks: Array<{ id: Types.ObjectId; error: string }> = []; + webhooksTriggered.forEach((data, index) => { + if (data.status === "rejected") { + failedWebhooks.push({ id: toBeTriggeredHooks[index]._id, error: data.reason.message }); + return; + } + successWebhooks.push(toBeTriggeredHooks[index]._id); + }); + // dont remove the workspaceid and environment filter. its used to reduce the dataset before $in check + await Webhook.bulkWrite([ + { + updateMany: { + filter: { workspace: workspaceId, environment, _id: { $in: successWebhooks } }, + update: { lastStatus: "success", lastRunErrorMessage: null } + } + }, + ...failedWebhooks.map(({ id, error }) => ({ + updateOne: { + filter: { + workspace: workspaceId, + environment, + _id: id + }, + update: { + lastStatus: "failed", + lastRunErrorMessage: error + } + } + })) + ]); +}; diff --git a/backend/src/variables/event.ts b/backend/src/variables/event.ts index c5de005d1..126ede040 100644 --- a/backend/src/variables/event.ts +++ b/backend/src/variables/event.ts @@ -1,2 +1,3 @@ export const EVENT_PUSH_SECRETS = "pushSecrets"; -export const EVENT_PULL_SECRETS = "pullSecrets"; \ No newline at end of file +export const EVENT_PULL_SECRETS = "pullSecrets"; +export const EVENT_START_INTEGRATION = "startIntegration"; diff --git a/cli/packages/cmd/vault.go b/cli/packages/cmd/vault.go index e6662c5d6..edf3befc7 100644 --- a/cli/packages/cmd/vault.go +++ b/cli/packages/cmd/vault.go @@ -48,7 +48,7 @@ var vaultSetCmd = &cobra.Command{ return } - fmt.Printf("\nSuccessfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]", currentVaultBackend, wantedVaultTypeName) + fmt.Printf("\nSuccessfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]\n", currentVaultBackend, wantedVaultTypeName) Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION)) } else { @@ -81,7 +81,7 @@ func printAvailableVaultBackends() { Telemetry.CaptureEvent("cli-command:vault", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("version", util.CLI_VERSION)) - fmt.Printf("\n\nYou are currently using [%s] vault to store your login credentials", string(currentVaultBackend)) + fmt.Printf("\n\nYou are currently using [%s] vault to store your login credentials\n", string(currentVaultBackend)) } // Checks if the vault that the user wants to switch to is a valid available vault diff --git a/docker-compose.yml b/docker-compose.yml index bc1a24dd8..7f516fdcc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,19 +41,19 @@ services: networks: - infisical - secret-scanning-git-app: - container_name: infisical-secret-scanning-git-app - restart: unless-stopped - depends_on: - - backend - - frontend - - mongo - ports: - - "3000:3001" - image: infisical/staging_deployment_secret-scanning-git-app - env_file: .env - networks: - - infisical + # secret-scanning-git-app: + # container_name: infisical-secret-scanning-git-app + # restart: unless-stopped + # depends_on: + # - backend + # - frontend + # - mongo + # ports: + # - "3000:3001" + # image: infisical/staging_deployment_secret-scanning-git-app + # env_file: .env + # networks: + # - infisical mongo: container_name: infisical-mongo diff --git a/docs/documentation/platform/token.mdx b/docs/documentation/platform/token.mdx index 8af35081e..1371d4621 100644 --- a/docs/documentation/platform/token.mdx +++ b/docs/documentation/platform/token.mdx @@ -12,7 +12,7 @@ This level of control not only ensures maximum flexibility but also significantl ## Creating a service token -To generate the the token, head over to your project settings as shown below. On creating a service token you can scope it to a path to limit the access. +To generate the token, head over to your project settings as shown below. On creating a service token you can scope it to a path to limit the access. ![token add](../../images/project-token-add.png) diff --git a/docs/documentation/platform/webhooks.mdx b/docs/documentation/platform/webhooks.mdx new file mode 100644 index 000000000..e0de7be05 --- /dev/null +++ b/docs/documentation/platform/webhooks.mdx @@ -0,0 +1,36 @@ +--- +title: "Webhooks" +description: "How Infisical webhooks works?" +--- + +Webhooks can be used to trigger changes to your integrations when secrets are modified, providing smooth integration with other third-party applications. + +![webhooks](../../images/webhooks.png) + +To create a webhook for a particular project, go to `Project Settings > Webhooks`. + +When creating a webhook, you can specify an environment and folder path (using glob patterns) to trigger only specific integrations. + +## Secret Key Verification + +A secret key is a way for users to verify that a webhook request was sent by Infisical and is intended for the correct integration. + +When you provide a secret key, Infisical will sign the payload of the webhook request using the key and attach a header called `x-infisical-signature` to the request with a payload. + +The header will be in the format `t=;`. You can then generate the signature yourself by generating a SHA256 hash of the payload with the secret key that you know. + +If the signature in the header matches the signature that you generated, then you can be sure that the request was sent by Infisical and is intended for your integration. The timestamp in the header ensures that the request is not replayed. + +### Webhook Payload Format + +```json +{ + "event": "secret.modified", + "project": { + "workspaceId":"the workspace id", + "environment": "project environment", + "secretPath": "project folder path" + }, + "timestamp": "" +} +``` diff --git a/docs/images/webhooks.png b/docs/images/webhooks.png new file mode 100644 index 000000000..a726e77a0 Binary files /dev/null and b/docs/images/webhooks.png differ diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx index 75715e144..01e011f79 100644 --- a/docs/integrations/platforms/kubernetes.mdx +++ b/docs/integrations/platforms/kubernetes.mdx @@ -15,23 +15,40 @@ The operator continuously updates secrets and can also reload dependent deployme The operator can be install via [Helm](helm.sh) or [kubectl](https://github.com/kubernetes/kubectl) - - Install Infisical Helm repository + + **Install the latest Infisical Helm repository** ```bash helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' helm repo update ``` - Install the Helm chart + **Install the Helm chart** + + For production deployments, it is highly recommended to set the chart version and the application version during installs and upgrades. + This will prevent the operator from being accidentally updated to the latest version and introduce unintended breaking changes. + + View application versions [here](https://hub.docker.com/r/infisical/kubernetes-operator/tags) and chart versions [here](https://cloudsmith.io/~infisical/repos/helm-charts/packages/detail/helm/secrets-operator/#versions) + ```bash - helm install --generate-name infisical-helm-charts/secrets-operator + helm install --generate-name infisical-helm-charts/secrets-operator --version= --set controllerManager.manager.image.tag= + + # Example installing app version v0.2.0 and chart version 0.1.4 + helm install --generate-name infisical-helm-charts/secrets-operator --version=0.1.4 --set controllerManager.manager.image.tag=v0.2.0 ``` - The operator will be installed in `infisical-operator-system` namespace - ``` + For production deployments, it is highly recommended to set the version of the Kubernetes operator manually instead of pointing to the latest version. + Doing so will help you avoid accidental updates to the newest release which may introduce unintended breaking changes. View all application versions [here](https://hub.docker.com/r/infisical/kubernetes-operator/tags). + + + The command below will install the most recent version of the Kubernetes operator. + However, to set the version manually, download the manifest and set the image tag version of `infisical/kubernetes-operator` according to your desired version. + + Once you apply the manifest, the operator will be installed in `infisical-operator-system` namespace. + + ``` kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical/main/k8-operator/kubectl-install/install-secrets-operator.yaml ``` diff --git a/docs/mint.json b/docs/mint.json index d2e970e69..a749524ef 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -73,7 +73,7 @@ { "name": "Slack", "icon": "slack", - "url": "https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg" + "url": "https://infisical.com/slack" }, { "name": "GitHub", @@ -114,6 +114,7 @@ "documentation/platform/project", "documentation/platform/folder", "documentation/platform/secret-reference", + "documentation/platform/webhooks", "documentation/platform/pit-recovery", "documentation/platform/secret-versioning", "documentation/platform/audit-logs", @@ -177,7 +178,12 @@ { "group": "Integrations", "pages": [ - "integrations/overview", + "integrations/overview" + ] + }, + { + "group": "Infrastructure Integrations", + "pages": [ { "group": "Docker", "pages": [ @@ -186,7 +192,12 @@ ] }, "integrations/platforms/kubernetes", - "integrations/frameworks/terraform", + "integrations/frameworks/terraform" + ] + }, + { + "group": "3rd-party Integrations", + "pages": [ { "group": "AWS", "pages": [ @@ -209,7 +220,12 @@ "integrations/cicd/githubactions", "integrations/cicd/gitlab", "integrations/cicd/circleci", - "integrations/cicd/travisci", + "integrations/cicd/travisci" + ] + }, + { + "group": "Framework Integrations", + "pages": [ "integrations/frameworks/spring-boot-maven", "integrations/frameworks/react", "integrations/frameworks/vue", @@ -234,18 +250,6 @@ "group": "Overview", "pages": ["sdks/overview"] }, - { - "group": "SDKs", - "pages": [ - "sdks/languages/node", - "sdks/languages/python", - "sdks/languages/java", - "sdks/languages/ruby", - "sdks/languages/go", - "sdks/languages/rust", - "sdks/languages/php" - ] - }, { "group": "Overview", "pages": [ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 717741bfb..cb3360485 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -44,6 +44,7 @@ "classnames": "^2.3.1", "cookies": "^0.8.0", "cva": "npm:class-variance-authority@^0.4.0", + "dayjs": "^1.11.9", "framer-motion": "^6.2.3", "fs": "^0.0.2", "gray-matter": "^4.0.3", @@ -10571,6 +10572,11 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/dayjs": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", + "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -30438,6 +30444,11 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "dayjs": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", + "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index a678504f7..f20477e6f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -52,6 +52,7 @@ "classnames": "^2.3.1", "cookies": "^0.8.0", "cva": "npm:class-variance-authority@^0.4.0", + "dayjs": "^1.11.9", "framer-motion": "^6.2.3", "fs": "^0.0.2", "gray-matter": "^4.0.3", diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index c387d938b..e4c671ca4 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -251,6 +251,10 @@ } }, "settings": { + "webhooks": { + "title": "Webhooks", + "description": "Manage webhooks to setup deployment hooks for your various integrations." + }, "members": { "title": "Project Members", "description": "This page shows the members of the selected project, and allows you to modify their permissions." diff --git a/frontend/src/components/basic/dialog/AddProjectMemberDialog.tsx b/frontend/src/components/basic/dialog/AddProjectMemberDialog.tsx index 2a25c8be6..87004e0e8 100644 --- a/frontend/src/components/basic/dialog/AddProjectMemberDialog.tsx +++ b/frontend/src/components/basic/dialog/AddProjectMemberDialog.tsx @@ -83,7 +83,7 @@ const AddProjectMemberDialog = ({ diff --git a/frontend/src/components/dashboard/AddTagsMenu.tsx b/frontend/src/components/dashboard/AddTagsMenu.tsx index 4d10f1d8a..e97c979a9 100644 --- a/frontend/src/components/dashboard/AddTagsMenu.tsx +++ b/frontend/src/components/dashboard/AddTagsMenu.tsx @@ -50,7 +50,7 @@ const AddTagsMenu = ({ allTags, currentTags, modifyTags, id }: { allTags: Tag[]; diff --git a/frontend/src/components/signup/TeamInviteStep.tsx b/frontend/src/components/signup/TeamInviteStep.tsx index b53532c4f..398ccd78d 100644 --- a/frontend/src/components/signup/TeamInviteStep.tsx +++ b/frontend/src/components/signup/TeamInviteStep.tsx @@ -5,7 +5,6 @@ import { useRouter } from "next/router"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { usePopUp } from "@app/hooks/usePopUp"; import addUserToOrg from "@app/pages/api/organization/addUserToOrg"; -import getOrganizations from "@app/pages/api/organization/getOrgs"; import { Button, EmailServiceSetupModal } from "../v2"; @@ -21,9 +20,7 @@ export default function TeamInviteStep(): JSX.Element { // Redirect user to the getting started page const redirectToHome = async () => { - const userOrgs = await getOrganizations(); - const userOrg = userOrgs[0]._id; - router.push(`/org/${userOrg?._id}/overview`); + router.push(`/org/${localStorage.getItem("orgData.id")}/overview`); }; const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => { diff --git a/frontend/src/components/v2/Button/Button.tsx b/frontend/src/components/v2/Button/Button.tsx index 785459c4f..b69545766 100644 --- a/frontend/src/components/v2/Button/Button.tsx +++ b/frontend/src/components/v2/Button/Button.tsx @@ -61,7 +61,8 @@ const buttonVariants = cva( { colorSchema: "primary", variant: "star", - className: "bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary hover:text-black hover:border-primary-400 duration-100" + className: + "bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary hover:text-black hover:border-primary-400 duration-100" }, { colorSchema: "primary", @@ -76,12 +77,14 @@ const buttonVariants = cva( { colorSchema: "primary", variant: "outline_bg", - className: "bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/[0.1] hover:border-primary/40 text-bunker-200" + className: + "bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/[0.1] hover:border-primary/40 text-bunker-200" }, { colorSchema: "secondary", variant: "star", - className: "bg-mineshaft-700 border border-mineshaft-600 hover:bg-mineshaft hover:text-white" + className: + "bg-mineshaft-700 border border-mineshaft-600 hover:bg-mineshaft hover:text-white" }, { colorSchema: "danger", @@ -163,13 +166,13 @@ export const Button = forwardRef( type="button" className={twMerge( buttonVariants({ - className, colorSchema, size, variant, isRounded, isDisabled, - isFullWidth + isFullWidth, + className }) )} disabled={isDisabled} @@ -193,7 +196,15 @@ export const Button = forwardRef( > {leftIcon} - {children} + + {children} +
& { children: ReactNode; content?: ReactNode; isOpen?: boolean; @@ -10,7 +10,7 @@ export type TooltipProps = { asChild?: boolean; onOpenChange?: (isOpen: boolean) => void; defaultOpen?: boolean; -} & Omit; +}; export const Tooltip = ({ children, diff --git a/frontend/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx b/frontend/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx index bf97b6750..fb1a21d40 100644 --- a/frontend/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx +++ b/frontend/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx @@ -17,7 +17,7 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele const { currentOrg } = useOrganization(); const { mutateAsync, isLoading } = useGetOrgTrialUrl(); const link = (subscription && subscription.slug !== null) - ? `/settings/billing/${localStorage.getItem("projectData.id") as string}` + ? `/org/${currentOrg?._id}/billing` : "https://infisical.com/scheduledemo"; const handleUpgradeBtnClick = async () => { diff --git a/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx b/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx index 8eb64bc6c..aca2e5328 100644 --- a/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx +++ b/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx @@ -3,8 +3,7 @@ import { createContext, ReactNode, useContext, useMemo } from "react"; import { useGetOrgSubscription } from "@app/hooks/api"; import { SubscriptionPlan } from "@app/hooks/api/types"; -import { useWorkspace } from "../WorkspaceContext"; -// import { Subscription } from '@app/hooks/api/workspace/types'; +import { useOrganization } from "../OrganizationContext"; type TSubscriptionContext = { subscription?: SubscriptionPlan; @@ -18,9 +17,10 @@ type Props = { }; export const SubscriptionProvider = ({ children }: Props): JSX.Element => { - const { currentWorkspace } = useWorkspace(); + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgSubscription({ - orgID: currentWorkspace?.organization || "" + orgID: currentOrg?._id || "" }); // memorize the workspace details for the context diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 075702bc7..299629416 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -13,4 +13,5 @@ export * from "./serviceTokens"; export * from "./subscriptions"; export * from "./tags"; export * from "./users"; +export * from "./webhooks"; export * from "./workspace"; diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index 1c4316410..098078090 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -8,6 +8,7 @@ export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types" export type { SubscriptionPlan } from "./subscriptions/types"; export type { WsTag } from "./tags/types"; export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from "./users/types"; +export type { TWebhook } from "./webhooks/types"; export type { CreateEnvironmentDTO, CreateWorkspaceDTO, diff --git a/frontend/src/hooks/api/webhooks/index.tsx b/frontend/src/hooks/api/webhooks/index.tsx new file mode 100644 index 000000000..44f1b5cfc --- /dev/null +++ b/frontend/src/hooks/api/webhooks/index.tsx @@ -0,0 +1,2 @@ +export { useCreateWebhook, useDeleteWebhook, useTestWebhook, useUpdateWebhook } from "./mutation"; +export { useGetWebhooks } from "./query"; diff --git a/frontend/src/hooks/api/webhooks/mutation.tsx b/frontend/src/hooks/api/webhooks/mutation.tsx new file mode 100644 index 000000000..786fbb459 --- /dev/null +++ b/frontend/src/hooks/api/webhooks/mutation.tsx @@ -0,0 +1,67 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { queryKeys } from "./query"; +import { TCreateWebhookDto, TDeleteWebhookDto, TTestWebhookDTO, TUpdateWebhookDto } from "./types"; + +export const useCreateWebhook = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TCreateWebhookDto>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.post("/api/v1/webhooks", dto); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId)); + } + }); +}; + +export const useTestWebhook = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TTestWebhookDTO>({ + mutationFn: async ({ webhookId }) => { + const { data } = await apiRequest.post(`/api/v1/webhooks/${webhookId}/test`); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId)); + }, + onError: (_, { workspaceId }) => { + queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId)); + } + }); +}; + +export const useUpdateWebhook = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TUpdateWebhookDto>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.patch(`/api/v1/webhooks/${dto.webhookId}`, { + isDisabled: dto.isDisabled + }); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId)); + } + }); +}; + +export const useDeleteWebhook = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TDeleteWebhookDto>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.delete(`/api/v1/webhooks/${dto.webhookId}`); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId)); + } + }); +}; diff --git a/frontend/src/hooks/api/webhooks/query.tsx b/frontend/src/hooks/api/webhooks/query.tsx new file mode 100644 index 000000000..fc1840409 --- /dev/null +++ b/frontend/src/hooks/api/webhooks/query.tsx @@ -0,0 +1,26 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TWebhook } from "./types"; + +export const queryKeys = { + getWebhooks: (workspaceId: string) => ["webhooks", { workspaceId }] +}; + +const fetchWebhooks = async (workspaceId: string) => { + const { data } = await apiRequest.get<{ webhooks: TWebhook[] }>("/api/v1/webhooks", { + params: { + workspaceId + } + }); + + return data.webhooks; +}; + +export const useGetWebhooks = (workspaceId: string) => + useQuery({ + queryKey: queryKeys.getWebhooks(workspaceId), + queryFn: () => fetchWebhooks(workspaceId), + enabled: Boolean(workspaceId) + }); diff --git a/frontend/src/hooks/api/webhooks/types.ts b/frontend/src/hooks/api/webhooks/types.ts new file mode 100644 index 000000000..8fd64bcc2 --- /dev/null +++ b/frontend/src/hooks/api/webhooks/types.ts @@ -0,0 +1,36 @@ +export type TWebhook = { + _id: string; + workspace: string; + environment: string; + secretPath: string; + url: string; + lastStatus: "success" | "failed"; + lastRunErrorMessage?: string; + isDisabled: boolean; + createdAt: string; + updatedAt: string; +}; + +export type TCreateWebhookDto = { + workspaceId: string; + environment: string; + webhookUrl: string; + webhookSecretKey?: string; + secretPath: string; +}; + +export type TUpdateWebhookDto = { + webhookId: string; + workspaceId: string; + isDisabled?: boolean; +}; + +export type TDeleteWebhookDto = { + webhookId: string; + workspaceId: string; +}; + +export type TTestWebhookDTO = { + webhookId: string; + workspaceId: string; +}; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 1449a736a..6a41e820a 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -80,7 +80,7 @@ type TAddProjectFormData = yup.InferType; export const AppLayout = ({ children }: LayoutProps) => { const router = useRouter(); const { createNotification } = useNotificationContext(); - const { mutateTrialAsync } = useGetOrgTrialUrl(); + const { mutateAsync } = useGetOrgTrialUrl(); // eslint-disable-next-line prefer-const const { workspaces, currentWorkspace } = useWorkspace(); @@ -301,7 +301,7 @@ export const AppLayout = ({ children }: LayoutProps) => { Documentation { + {/* + + } + icon="system-outline-82-extension" + > + Audit Logs + + + */} { if (!subscription || !currentOrg) return; // direct user to start pro trial - const url = await mutateTrialAsync({ + const url = await mutateAsync({ orgId: currentOrg._id, success_url: window.location.href }); diff --git a/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx b/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx index 7c313b0e9..7adb88959 100644 --- a/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx +++ b/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx @@ -180,7 +180,7 @@ export const Navbar = () => { onKeyDown={() => null} role="button" tabIndex={0} - onClick={() => router.push(`/settings/personal/${router.query.id}`)} + onClick={() => router.push("/personal-settings")} className="mx-1 my-1 flex cursor-pointer flex-row items-center rounded-md px-1 hover:bg-white/5" >
diff --git a/frontend/src/pages/cli-redirect.tsx b/frontend/src/pages/cli-redirect.tsx index e28a9e9dc..ab308ef6d 100644 --- a/frontend/src/pages/cli-redirect.tsx +++ b/frontend/src/pages/cli-redirect.tsx @@ -1,4 +1,5 @@ import Head from "next/head"; +import Image from "next/image"; export default function CliRedirect() { return ( @@ -8,9 +9,12 @@ export default function CliRedirect() {
-

Head back to your terminal!

-

- You've successfully logged into infisical-cli +

+ Infisical Logo +
+

Head back to your terminal

+

+ You've successfully logged in to the Infisical CLI

diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index 22f58cb42..45b9a1743 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -379,7 +379,7 @@ export default function Organization() { icon: faSlack, time: "1 min", userAction: "slack_cta_clicked", - link: "https://join.slack.com/t/infisical-users/shared_invite/zt-1ye0tm8ab-899qZ6ZbpfESuo6TEikyOQ" + link: "https://infisical.com/slack" })}
{orgWorkspaces.length !== 0 &&
diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx index 1a3b0f73e..1cb686414 100644 --- a/frontend/src/views/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/DashboardPage/DashboardPage.tsx @@ -36,7 +36,7 @@ import { UpgradePlanModal } from "@app/components/v2"; import { leaveConfirmDefaultMessage } from "@app/const"; -import { useOrganization, useSubscription,useWorkspace } from "@app/context"; +import { useOrganization, useSubscription, useWorkspace } from "@app/context"; import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks"; import { useBatchSecretsOp, @@ -340,9 +340,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { } }; - const onAppendSecret = () => { + const onAppendSecret = () => { setSearchFilter(""); - append(DEFAULT_SECRET_VALUE) + append(DEFAULT_SECRET_VALUE); }; const onSaveSecret = async ({ secrets: userSec = [], isSnapshotMode }: FormData) => { @@ -364,6 +364,10 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { ); // type check if (!selectedEnv?.slug) return; + if (batchedSecret.length === 0) { + reset(); + return; + } try { await batchSecretOp({ requests: batchedSecret, @@ -636,7 +640,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { handlePopUpOpen("secretSnapshots"); return; } - + handlePopUpOpen("upgradePlan"); }} leftIcon={} @@ -905,7 +909,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => { handlePopUpToggle("upgradePlan", isOpen)} - text={subscription.slug === null ? "You can perform point-in-time recovery under an Enterprise license" : "You can perform point-in-time recovery if you switch to Infisical's Team plan"} + text={ + subscription.slug === null + ? "You can perform point-in-time recovery under an Enterprise license" + : "You can perform point-in-time recovery if you switch to Infisical's Team plan" + } /> )}
diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx index fdb413226..fd79785d3 100644 --- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx +++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx @@ -1,9 +1,11 @@ /* eslint-disable react/jsx-no-useless-fragment */ -import { memo, useRef } from "react"; +import { memo, useEffect, useRef } from "react"; import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form"; import { + faCheck, faCodeBranch, faComment, + faCopy, faEllipsis, faInfoCircle, faPlus, @@ -30,6 +32,7 @@ import { TextArea, Tooltip } from "@app/components/v2"; +import { useToggle } from "@app/hooks"; import { WsTag } from "@app/hooks/api/types"; import { FormData, SecretActionType } from "../../DashboardPage.utils"; @@ -95,6 +98,16 @@ export const SecretInputRow = memo( name: `secrets.${index}.key`, disabled: isKeySubDisabled.current }); + const secValue = useWatch({ + control, + name: `secrets.${index}.value`, + disabled: isKeySubDisabled.current + }); + const secValueOverride = useWatch({ + control, + name: `secrets.${index}.valueOverride`, + disabled: isKeySubDisabled.current + }) const secId = useWatch({ control, name: `secrets.${index}._id` }); const tags = useWatch({ control, name: `secrets.${index}.tags`, defaultValue: [] }) || []; @@ -103,6 +116,21 @@ export const SecretInputRow = memo( {} ); + const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false); + + useEffect(() => { + let timer: NodeJS.Timeout; + if (isInviteLinkCopied) { + timer = setTimeout(() => setInviteLinkCopied.off(), 2000); + } + return () => clearTimeout(timer); + }, [isInviteLinkCopied]); + + const copyTokenToClipboard = () => { + navigator.clipboard.writeText((secValueOverride || secValue) as string); + setInviteLinkCopied.on(); + }; + // when secret is override by personal values const isOverridden = overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified; @@ -223,6 +251,19 @@ export const SecretInputRow = memo( {slug} ))} +
+ + + + + +
{!(isReadOnly || isAddOnly || isRollbackMode) && (
diff --git a/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx b/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx index 0e927d86c..4ce1bd083 100644 --- a/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx @@ -31,6 +31,8 @@ export const CloudIntegrationSection = ({ const isEmpty = !isLoading && !cloudIntegrations?.length; + const sortedCloudIntegrations = cloudIntegrations.sort((a, b) => a.name.localeCompare(b.name)); + return (
@@ -43,7 +45,7 @@ export const CloudIntegrationSection = ({ ))} {!isLoading && - cloudIntegrations?.map((cloudIntegration) => ( + sortedCloudIntegrations?.map((cloudIntegration) => (
null} role="button" diff --git a/frontend/src/views/IntegrationsPage/components/FrameworkIntegrationSection/FrameworkIntegrationSection.tsx b/frontend/src/views/IntegrationsPage/components/FrameworkIntegrationSection/FrameworkIntegrationSection.tsx index 9b567eaaa..d77b976bc 100644 --- a/frontend/src/views/IntegrationsPage/components/FrameworkIntegrationSection/FrameworkIntegrationSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/FrameworkIntegrationSection/FrameworkIntegrationSection.tsx @@ -12,6 +12,8 @@ type Props = { export const FrameworkIntegrationSection = ({ frameworks }: Props) => { const { t } = useTranslation(); + const sortedFrameworks = frameworks.sort((a, b) => a.name.localeCompare(b.name)); + return ( <>
@@ -22,7 +24,7 @@ export const FrameworkIntegrationSection = ({ frameworks }: Props) => { className="mx-6 mt-4 grid grid-flow-dense gap-3" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))" }} > - {frameworks.map((framework) => ( + {sortedFrameworks.map((framework) => ( { return (
{subscription && subscription?.slug !== "enterprise" && subscription?.slug !== "pro" && subscription?.slug !== "pro-annual" && ( -
+

Become Infisical

Unlimited members, projects, RBAC, smart alerts, and so much more

@@ -84,7 +84,7 @@ export const PreviewSection = () => {
)} {!isLoading && subscription && data && ( -
+

Current plan

diff --git a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx index cf622e69c..75f67aa6a 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx @@ -1,18 +1,59 @@ +import { Fragment } from "react"; import { useTranslation } from "react-i18next"; +import { Tab } from "@headlessui/react"; -import { ProjectTabGroup } from "./components"; +import NavHeader from "@app/components/navigation/NavHeader"; + +import { ProjectGeneralTab } from "./components/ProjectGeneralTab"; +import { ProjectServiceTokensTab } from "./components/ProjectServiceTokensTab"; +import { WebhooksTab } from "./components/WebhooksTab"; + +const tabs = [ + { name: "General", key: "tab-project-general" }, + { name: "Service Tokens", key: "tab-project-service-tokens" }, + { name: "Webhooks", key: "tab-project-webhooks" } +]; export const ProjectSettingsPage = () => { const { t } = useTranslation(); return ( -

-
-
-

- {t("settings.project.title")} -

+
+
+
+
- +
+

{t("settings.project.title")}

+
+ + + {tabs.map((tab) => ( + + {({ selected }) => ( + + )} + + ))} + + + + + + + + + + + + +
); diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectTabGroup/ProjectTabGroup.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectTabGroup/ProjectTabGroup.tsx deleted file mode 100644 index c18dcb4b7..000000000 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectTabGroup/ProjectTabGroup.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Fragment } from "react" -import { Tab } from "@headlessui/react" - -import { ProjectGeneralTab } from "../ProjectGeneralTab"; -import { ProjectServiceTokensTab } from "../ProjectServiceTokensTab"; - -const tabs = [ - { name: "General", key: "tab-project-general" }, - { name: "Service Tokens", key: "tab-project-service-tokens" } -]; - -export const ProjectTabGroup = () => { - return ( - - - {tabs.map((tab) => ( - - {({ selected }) => ( - - )} - - ))} - - - - - - - - - - - ); -} \ No newline at end of file diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectTabGroup/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectTabGroup/index.tsx deleted file mode 100644 index ac1f5c50d..000000000 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectTabGroup/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ProjectTabGroup } from "./ProjectTabGroup"; \ No newline at end of file diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/AddWebhookForm.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/AddWebhookForm.tsx new file mode 100644 index 000000000..a5f494318 --- /dev/null +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/AddWebhookForm.tsx @@ -0,0 +1,133 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import { + Button, + FormControl, + Input, + Modal, + ModalClose, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; + +const formSchema = yup.object({ + environment: yup.string().required().trim().label("Environment"), + webhookUrl: yup.string().url().required().trim().label("Webhook URL"), + webhookSecretKey: yup.string().trim().label("Secret Key"), + secretPath: yup.string().required().trim().label("Secret Path") +}); + +export type TFormSchema = yup.InferType; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + onCreateWebhook: (data: TFormSchema) => void; + environments?: Array<{ slug: string; name: string }>; +}; + +export const AddWebhookForm = ({ + isOpen, + onOpenChange, + onCreateWebhook, + environments = [] +}: Props) => { + const { + control, + handleSubmit, + register, + reset, + formState: { errors, isSubmitting } + } = useForm({ + resolver: yupResolver(formSchema) + }); + + useEffect(() => { + if (!isOpen) { + reset(); + } + }, [isOpen]); + + return ( + + +
+
+ ( + + + + )} + /> + + + + + + + + + +
+
+ + + + +
+
+
+
+ ); +}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/WebhooksTab.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/WebhooksTab.tsx new file mode 100644 index 000000000..76b928ace --- /dev/null +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/WebhooksTab.tsx @@ -0,0 +1,279 @@ +import { useTranslation } from "react-i18next"; +import { faInfoCircle, faPlug, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import dayjs from "dayjs"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { + Button, + DeleteActionModal, + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + THead, + Tooltip, + Tr +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { + useCreateWebhook, + useDeleteWebhook, + useGetWebhooks, + useTestWebhook, + useUpdateWebhook +} from "@app/hooks/api"; + +import { AddWebhookForm, TFormSchema } from "./AddWebhookForm"; + +export const WebhooksTab = () => { + const { t } = useTranslation(); + const { createNotification } = useNotificationContext(); + const { currentWorkspace } = useWorkspace(); + const workspaceId = currentWorkspace?._id || ""; + const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ + "addWebhook", + "deleteWebhook" + ] as const); + + const { data: webhooks, isLoading: isWebhooksLoading } = useGetWebhooks(workspaceId); + + // mutation + const { mutateAsync: createWebhook } = useCreateWebhook(); + const { + mutateAsync: testWebhook, + variables: testWebhookVars, + isLoading: isTestWebhookSubmitting + } = useTestWebhook(); + const { + mutateAsync: updateWebhook, + variables: updateWebhookVars, + isLoading: isUpdateWebhookSubmitting + } = useUpdateWebhook(); + const { mutateAsync: deleteWebhook } = useDeleteWebhook(); + + const handleWebhookCreate = async (data: TFormSchema) => { + try { + await createWebhook({ + ...data, + workspaceId + }); + handlePopUpClose("addWebhook"); + createNotification({ + type: "success", + text: "Successfully created webhook" + }); + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "Failed to create webhook" + }); + } + }; + + const handleWebhookDisable = async (webhookId: string, isDisabled: boolean) => { + try { + await updateWebhook({ + webhookId, + workspaceId, + isDisabled + }); + createNotification({ + type: "success", + text: "Successfully updated webhook" + }); + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "Failed to update webhook" + }); + } + }; + + const handleWebhookDelete = async () => { + try { + const webhookId = popUp?.deleteWebhook?.data as string; + await deleteWebhook({ + webhookId, + workspaceId + }); + handlePopUpClose("deleteWebhook"); + createNotification({ + type: "success", + text: "Successfully deleted webhook" + }); + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "Failed to delete webhook" + }); + } + }; + + const handleWebhookTest = async (webhookId: string) => { + try { + await testWebhook({ + webhookId, + workspaceId + }); + createNotification({ + type: "success", + text: "Successfully triggered webhook" + }); + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "Failed to trigger webhook" + }); + } + }; + + return ( +
+
+

{t("settings.webhooks.title")}

+ +
+

{t("settings.webhooks.description")}

+
+ + + + + + + + + + + + + {isWebhooksLoading && } + {!isWebhooksLoading && webhooks && webhooks?.length === 0 && ( + + + + )} + {!isWebhooksLoading && + webhooks?.map( + ({ + _id: id, + url, + environment, + secretPath, + lastStatus, + isDisabled, + updatedAt, + lastRunErrorMessage + }) => ( + + + + + + + + ) + )} + +
URLEnvironmentSecret PathStatusAction
+ +
+ {url} + {environment}{secretPath} + {!lastStatus ? ( + "-" + ) : ( +
+ {lastStatus}{" "} + +
+ Updated At: {dayjs(updatedAt).format("YYYY-MM-DD, hh:mm A")} +
+ {lastRunErrorMessage && ( +
+ Error: {lastRunErrorMessage} +
+ )} +
+ } + > + + + + )} +
+
+ + + +
+
+
+
+ handlePopUpToggle("addWebhook", isOpen)} + onCreateWebhook={handleWebhookCreate} + /> + handlePopUpToggle("deleteWebhook", isOpen)} + onClose={() => handlePopUpClose("deleteWebhook")} + onDeleteApproved={handleWebhookDelete} + /> +
+ ); +}; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/index.tsx new file mode 100644 index 000000000..2795bd02f --- /dev/null +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/WebhooksTab/index.tsx @@ -0,0 +1 @@ +export { WebhooksTab } from "./WebhooksTab"; diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx index b00af72c9..5f9e1013b 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/index.tsx @@ -4,6 +4,5 @@ export { E2EESection } from "./E2EESection"; export { EnvironmentSection } from "./EnvironmentSection"; export { ProjectIndexSecretsSection } from "./ProjectIndexSecretsSection"; export { ProjectNameChangeSection } from "./ProjectNameChangeSection"; -export { ProjectTabGroup } from "./ProjectTabGroup"; export { SecretTagsSection } from "./SecretTagsSection"; export { ServiceTokenSection } from "./ServiceTokenSection"; diff --git a/nginx/default.conf b/nginx/default.conf index f824372be..9c3607406 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -15,19 +15,19 @@ server { proxy_cookie_path / "/; HttpOnly; SameSite=strict"; } - location /git-app-api { - proxy_set_header X-Real-RIP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # location /git-app-api { + # proxy_set_header X-Real-RIP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - proxy_set_header X-NginX-Proxy true; + # proxy_set_header Host $http_host; + # proxy_set_header X-NginX-Proxy true; - proxy_pass http://git-app:3000/; - proxy_redirect off; - # proxy_redirect http://localhost:8080/ http://frontend.example.com/; + # proxy_pass http://git-app:3000/; + # proxy_redirect off; + # # proxy_redirect http://localhost:8080/ http://frontend.example.com/; - proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; - } + # proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; + # } location / { include /etc/nginx/mime.types;