From f9de9c4834c4aafa2895e42f30b837cc5ecaf10e Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Wed, 11 Feb 2026 09:34:06 +0100 Subject: [PATCH] Add webhooks Signed-off-by: Sacha Al Himdani --- .../core/WebhooksSettingsPageQuery.graphql.ts | 265 ++ ...ooksSettingsPage_createMutation.graphql.ts | 177 ++ ...ooksSettingsPage_deleteMutation.graphql.ts | 132 + ...SettingsPage_signingSecretQuery.graphql.ts | 124 + ...ooksSettingsPage_updateMutation.graphql.ts | 132 + .../organizations/settings/SettingsLayout.tsx | 5 + .../settings/WebhooksSettingsPageLoader.tsx | 37 + .../settings/WebhooksSettingsPage.tsx | 567 +++++ apps/console/src/routes.tsx | 7 + pkg/coredata/entity_type_reg.go | 9 + pkg/coredata/migrations/20260210T135740Z.sql | 52 + pkg/coredata/webhook_call.go | 87 + pkg/coredata/webhook_call_status.go | 64 + pkg/coredata/webhook_configuration.go | 411 ++++ .../webhook_configuration_order_field.go | 55 + pkg/coredata/webhook_event.go | 160 ++ pkg/coredata/webhook_event_status.go | 65 + pkg/coredata/webhook_event_type.go | 133 + pkg/probo/actions.go | 7 + pkg/probo/meeting_service.go | 14 + pkg/probo/policies.go | 1 + pkg/probo/service.go | 2 + pkg/probo/vendor_service.go | 23 +- pkg/probo/webhook_configuration_service.go | 290 +++ pkg/probod/notifications_config.go | 9 +- pkg/probod/probod.go | 18 + pkg/server/api/connect/v1/schema/schema.go | 3 + pkg/server/api/console/v1/schema.graphql | 106 + pkg/server/api/console/v1/schema/schema.go | 2171 +++++++++++++++++ pkg/server/api/console/v1/types/types.go | 48 + .../console/v1/types/webhook_configuration.go | 74 + pkg/server/api/console/v1/v1_resolver.go | 183 ++ pkg/webhook/data.go | 153 ++ pkg/webhook/sender.go | 352 +++ 34 files changed, 5932 insertions(+), 4 deletions(-) create mode 100644 apps/console/src/__generated__/core/WebhooksSettingsPageQuery.graphql.ts create mode 100644 apps/console/src/__generated__/core/WebhooksSettingsPage_createMutation.graphql.ts create mode 100644 apps/console/src/__generated__/core/WebhooksSettingsPage_deleteMutation.graphql.ts create mode 100644 apps/console/src/__generated__/core/WebhooksSettingsPage_signingSecretQuery.graphql.ts create mode 100644 apps/console/src/__generated__/core/WebhooksSettingsPage_updateMutation.graphql.ts create mode 100644 apps/console/src/pages/iam/organizations/settings/WebhooksSettingsPageLoader.tsx create mode 100644 apps/console/src/pages/organizations/settings/WebhooksSettingsPage.tsx create mode 100644 pkg/coredata/migrations/20260210T135740Z.sql create mode 100644 pkg/coredata/webhook_call.go create mode 100644 pkg/coredata/webhook_call_status.go create mode 100644 pkg/coredata/webhook_configuration.go create mode 100644 pkg/coredata/webhook_configuration_order_field.go create mode 100644 pkg/coredata/webhook_event.go create mode 100644 pkg/coredata/webhook_event_status.go create mode 100644 pkg/coredata/webhook_event_type.go create mode 100644 pkg/probo/webhook_configuration_service.go create mode 100644 pkg/server/api/console/v1/types/webhook_configuration.go create mode 100644 pkg/webhook/data.go create mode 100644 pkg/webhook/sender.go diff --git a/apps/console/src/__generated__/core/WebhooksSettingsPageQuery.graphql.ts b/apps/console/src/__generated__/core/WebhooksSettingsPageQuery.graphql.ts new file mode 100644 index 000000000..47a84b08f --- /dev/null +++ b/apps/console/src/__generated__/core/WebhooksSettingsPageQuery.graphql.ts @@ -0,0 +1,265 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type WebhookEventType = "MEETING_CREATED" | "MEETING_DELETED" | "MEETING_UPDATED" | "VENDOR_CREATED" | "VENDOR_DELETED" | "VENDOR_UPDATED"; +export type WebhooksSettingsPageQuery$variables = { + organizationId: string; +}; +export type WebhooksSettingsPageQuery$data = { + readonly organization: { + readonly __typename: "Organization"; + readonly id: string; + readonly webhookConfigurations: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly endpointUrl: string; + readonly id: string; + readonly selectedEvents: ReadonlyArray; + }; + }>; + }; + } | { + // This will never be '%other', but we need some + // value in case none of the concrete values match. + readonly __typename: "%other"; + }; +}; +export type WebhooksSettingsPageQuery = { + response: WebhooksSettingsPageQuery$data; + variables: WebhooksSettingsPageQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "organizationId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "organizationId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v4 = [ + { + "alias": null, + "args": null, + "concreteType": "WebhookConfigurationEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "WebhookConfiguration", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endpointUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "selectedEvents", + "storageKey": null + }, + (v2/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null + } +], +v5 = [ + { + "kind": "Literal", + "name": "first", + "value": 50 + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "WebhooksSettingsPageQuery", + "selections": [ + { + "kind": "RequiredField", + "field": { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + (v3/*: any*/), + { + "alias": "webhookConfigurations", + "args": null, + "concreteType": "WebhookConfigurationConnection", + "kind": "LinkedField", + "name": "__WebhooksSettingsPage_webhookConfigurations_connection", + "plural": false, + "selections": (v4/*: any*/), + "storageKey": null + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + }, + "action": "THROW" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "WebhooksSettingsPageQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v5/*: any*/), + "concreteType": "WebhookConfigurationConnection", + "kind": "LinkedField", + "name": "webhookConfigurations", + "plural": false, + "selections": (v4/*: any*/), + "storageKey": "webhookConfigurations(first:50)" + }, + { + "alias": null, + "args": (v5/*: any*/), + "filters": null, + "handle": "connection", + "key": "WebhooksSettingsPage_webhookConfigurations", + "kind": "LinkedHandle", + "name": "webhookConfigurations" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "e6aa5c6b8142b654e2a3b7bebec59f83", + "id": null, + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "forward", + "path": [ + "organization", + "webhookConfigurations" + ] + } + ] + }, + "name": "WebhooksSettingsPageQuery", + "operationKind": "query", + "text": "query WebhooksSettingsPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n webhookConfigurations(first: 50) {\n edges {\n node {\n id\n endpointUrl\n selectedEvents\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "2a68dcb3a875a982b7d8300aa72c012b"; + +export default node; diff --git a/apps/console/src/__generated__/core/WebhooksSettingsPage_createMutation.graphql.ts b/apps/console/src/__generated__/core/WebhooksSettingsPage_createMutation.graphql.ts new file mode 100644 index 000000000..3ba73a15f --- /dev/null +++ b/apps/console/src/__generated__/core/WebhooksSettingsPage_createMutation.graphql.ts @@ -0,0 +1,177 @@ +/** + * @generated SignedSource<<56bfc2d1746edae72b0da44526fab12f>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type WebhookEventType = "MEETING_CREATED" | "MEETING_DELETED" | "MEETING_UPDATED" | "VENDOR_CREATED" | "VENDOR_DELETED" | "VENDOR_UPDATED"; +export type CreateWebhookConfigurationInput = { + endpointUrl: string; + organizationId: string; + selectedEvents: ReadonlyArray; +}; +export type WebhooksSettingsPage_createMutation$variables = { + connections: ReadonlyArray; + input: CreateWebhookConfigurationInput; +}; +export type WebhooksSettingsPage_createMutation$data = { + readonly createWebhookConfiguration: { + readonly webhookConfigurationEdge: { + readonly node: { + readonly endpointUrl: string; + readonly id: string; + readonly selectedEvents: ReadonlyArray; + }; + }; + }; +}; +export type WebhooksSettingsPage_createMutation = { + response: WebhooksSettingsPage_createMutation$data; + variables: WebhooksSettingsPage_createMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "concreteType": "WebhookConfigurationEdge", + "kind": "LinkedField", + "name": "webhookConfigurationEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "WebhookConfiguration", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endpointUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "selectedEvents", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "WebhooksSettingsPage_createMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateWebhookConfigurationPayload", + "kind": "LinkedField", + "name": "createWebhookConfiguration", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "WebhooksSettingsPage_createMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateWebhookConfigurationPayload", + "kind": "LinkedField", + "name": "createWebhookConfiguration", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependEdge", + "key": "", + "kind": "LinkedHandle", + "name": "webhookConfigurationEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "0e88932f635b8f16e41f730abf9e05ff", + "id": null, + "metadata": {}, + "name": "WebhooksSettingsPage_createMutation", + "operationKind": "mutation", + "text": "mutation WebhooksSettingsPage_createMutation(\n $input: CreateWebhookConfigurationInput!\n) {\n createWebhookConfiguration(input: $input) {\n webhookConfigurationEdge {\n node {\n id\n endpointUrl\n selectedEvents\n }\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "4e1a7692c83c4dda0c2fc557f15d669d"; + +export default node; diff --git a/apps/console/src/__generated__/core/WebhooksSettingsPage_deleteMutation.graphql.ts b/apps/console/src/__generated__/core/WebhooksSettingsPage_deleteMutation.graphql.ts new file mode 100644 index 000000000..e16a8c38b --- /dev/null +++ b/apps/console/src/__generated__/core/WebhooksSettingsPage_deleteMutation.graphql.ts @@ -0,0 +1,132 @@ +/** + * @generated SignedSource<<86d94ecf595a0048971276541e185905>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteWebhookConfigurationInput = { + webhookConfigurationId: string; +}; +export type WebhooksSettingsPage_deleteMutation$variables = { + connections: ReadonlyArray; + input: DeleteWebhookConfigurationInput; +}; +export type WebhooksSettingsPage_deleteMutation$data = { + readonly deleteWebhookConfiguration: { + readonly deletedWebhookConfigurationId: string; + }; +}; +export type WebhooksSettingsPage_deleteMutation = { + response: WebhooksSettingsPage_deleteMutation$data; + variables: WebhooksSettingsPage_deleteMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deletedWebhookConfigurationId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "WebhooksSettingsPage_deleteMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteWebhookConfigurationPayload", + "kind": "LinkedField", + "name": "deleteWebhookConfiguration", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "WebhooksSettingsPage_deleteMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteWebhookConfigurationPayload", + "kind": "LinkedField", + "name": "deleteWebhookConfiguration", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "deletedWebhookConfigurationId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "ae53fa4e16e4491720c1fdfc52301c09", + "id": null, + "metadata": {}, + "name": "WebhooksSettingsPage_deleteMutation", + "operationKind": "mutation", + "text": "mutation WebhooksSettingsPage_deleteMutation(\n $input: DeleteWebhookConfigurationInput!\n) {\n deleteWebhookConfiguration(input: $input) {\n deletedWebhookConfigurationId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "632fce2d89360bed2d857dd3feb90b9d"; + +export default node; diff --git a/apps/console/src/__generated__/core/WebhooksSettingsPage_signingSecretQuery.graphql.ts b/apps/console/src/__generated__/core/WebhooksSettingsPage_signingSecretQuery.graphql.ts new file mode 100644 index 000000000..eac4448aa --- /dev/null +++ b/apps/console/src/__generated__/core/WebhooksSettingsPage_signingSecretQuery.graphql.ts @@ -0,0 +1,124 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type WebhooksSettingsPage_signingSecretQuery$variables = { + webhookConfigurationId: string; +}; +export type WebhooksSettingsPage_signingSecretQuery$data = { + readonly node: { + readonly signingSecret?: string; + }; +}; +export type WebhooksSettingsPage_signingSecretQuery = { + response: WebhooksSettingsPage_signingSecretQuery$data; + variables: WebhooksSettingsPage_signingSecretQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "webhookConfigurationId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "webhookConfigurationId" + } +], +v2 = { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "signingSecret", + "storageKey": null + } + ], + "type": "WebhookConfiguration", + "abstractKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "WebhooksSettingsPage_signingSecretQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/) + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "WebhooksSettingsPage_signingSecretQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "266c1519da14eb09ed28e26cc6394f23", + "id": null, + "metadata": {}, + "name": "WebhooksSettingsPage_signingSecretQuery", + "operationKind": "query", + "text": "query WebhooksSettingsPage_signingSecretQuery(\n $webhookConfigurationId: ID!\n) {\n node(id: $webhookConfigurationId) {\n __typename\n ... on WebhookConfiguration {\n signingSecret\n }\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "9d981d1a353a82f234e7ce79edb6d5d9"; + +export default node; diff --git a/apps/console/src/__generated__/core/WebhooksSettingsPage_updateMutation.graphql.ts b/apps/console/src/__generated__/core/WebhooksSettingsPage_updateMutation.graphql.ts new file mode 100644 index 000000000..a0f149ebe --- /dev/null +++ b/apps/console/src/__generated__/core/WebhooksSettingsPage_updateMutation.graphql.ts @@ -0,0 +1,132 @@ +/** + * @generated SignedSource<<3331839e2cc81d14ac1b2fcfe3fd84f1>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type WebhookEventType = "MEETING_CREATED" | "MEETING_DELETED" | "MEETING_UPDATED" | "VENDOR_CREATED" | "VENDOR_DELETED" | "VENDOR_UPDATED"; +export type UpdateWebhookConfigurationInput = { + endpointUrl?: string | null | undefined; + id: string; + selectedEvents?: ReadonlyArray | null | undefined; +}; +export type WebhooksSettingsPage_updateMutation$variables = { + input: UpdateWebhookConfigurationInput; +}; +export type WebhooksSettingsPage_updateMutation$data = { + readonly updateWebhookConfiguration: { + readonly webhookConfiguration: { + readonly endpointUrl: string; + readonly id: string; + readonly selectedEvents: ReadonlyArray; + readonly updatedAt: string; + }; + }; +}; +export type WebhooksSettingsPage_updateMutation = { + response: WebhooksSettingsPage_updateMutation$data; + variables: WebhooksSettingsPage_updateMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "UpdateWebhookConfigurationPayload", + "kind": "LinkedField", + "name": "updateWebhookConfiguration", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "WebhookConfiguration", + "kind": "LinkedField", + "name": "webhookConfiguration", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endpointUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "selectedEvents", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "WebhooksSettingsPage_updateMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "WebhooksSettingsPage_updateMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "399a35c005f10dd321784a7533fb4f80", + "id": null, + "metadata": {}, + "name": "WebhooksSettingsPage_updateMutation", + "operationKind": "mutation", + "text": "mutation WebhooksSettingsPage_updateMutation(\n $input: UpdateWebhookConfigurationInput!\n) {\n updateWebhookConfiguration(input: $input) {\n webhookConfiguration {\n id\n endpointUrl\n selectedEvents\n updatedAt\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "f974aae43d0ad8b5fe3cab60bd07a3ff"; + +export default node; diff --git a/apps/console/src/pages/iam/organizations/settings/SettingsLayout.tsx b/apps/console/src/pages/iam/organizations/settings/SettingsLayout.tsx index dd3be590a..ead22d4fd 100644 --- a/apps/console/src/pages/iam/organizations/settings/SettingsLayout.tsx +++ b/apps/console/src/pages/iam/organizations/settings/SettingsLayout.tsx @@ -2,6 +2,7 @@ import { useTranslate } from "@probo/i18n"; import { IconKey, IconLock, + IconSend, IconSettingsGear2, PageHeader, TabLink, @@ -32,6 +33,10 @@ export default function SettingsLayout() { {__("SCIM")} + + + {__("Webhooks")} + diff --git a/apps/console/src/pages/iam/organizations/settings/WebhooksSettingsPageLoader.tsx b/apps/console/src/pages/iam/organizations/settings/WebhooksSettingsPageLoader.tsx new file mode 100644 index 000000000..18130fdf8 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/settings/WebhooksSettingsPageLoader.tsx @@ -0,0 +1,37 @@ +import { useEffect } from "react"; +import { useQueryLoader } from "react-relay"; + +import type { WebhooksSettingsPageQuery } from "#/__generated__/core/WebhooksSettingsPageQuery.graphql"; +import { useOrganizationId } from "#/hooks/useOrganizationId"; +import { + WebhooksSettingsPage, + webhooksSettingsPageQuery, +} from "#/pages/organizations/settings/WebhooksSettingsPage"; +import { CoreRelayProvider } from "#/providers/CoreRelayProvider"; + +function WebhooksSettingsPageQueryLoader() { + const organizationId = useOrganizationId(); + const [queryRef, loadQuery] = useQueryLoader( + webhooksSettingsPageQuery, + ); + + useEffect(() => { + loadQuery({ + organizationId, + }); + }, [loadQuery, organizationId]); + + if (!queryRef) { + return null; + } + + return ; +} + +export default function WebhooksSettingsPageLoader() { + return ( + + + + ); +} diff --git a/apps/console/src/pages/organizations/settings/WebhooksSettingsPage.tsx b/apps/console/src/pages/organizations/settings/WebhooksSettingsPage.tsx new file mode 100644 index 000000000..37e230657 --- /dev/null +++ b/apps/console/src/pages/organizations/settings/WebhooksSettingsPage.tsx @@ -0,0 +1,567 @@ +import { useTranslate } from "@probo/i18n"; +import { + Button, + Card, + Checkbox, + Dialog, + DialogContent, + DialogFooter, + Field, + IconPencil, + IconPlusLarge, + IconSquareBehindSquare2, + IconTrashCan, + Input, + Label, + Spinner, + useDialogRef, + useToast, +} from "@probo/ui"; +import { useCallback, useState } from "react"; +import { type PreloadedQuery, usePreloadedQuery, useRelayEnvironment } from "react-relay"; +import { ConnectionHandler, fetchQuery, graphql } from "relay-runtime"; +import { z } from "zod"; + +import type { WebhooksSettingsPage_createMutation } from "#/__generated__/core/WebhooksSettingsPage_createMutation.graphql"; +import type { WebhooksSettingsPage_deleteMutation } from "#/__generated__/core/WebhooksSettingsPage_deleteMutation.graphql"; +import type { WebhooksSettingsPage_signingSecretQuery } from "#/__generated__/core/WebhooksSettingsPage_signingSecretQuery.graphql"; +import type { WebhooksSettingsPage_updateMutation } from "#/__generated__/core/WebhooksSettingsPage_updateMutation.graphql"; +import type { WebhooksSettingsPageQuery } from "#/__generated__/core/WebhooksSettingsPageQuery.graphql"; +import { useFormWithSchema } from "#/hooks/useFormWithSchema"; +import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; + +export const webhooksSettingsPageQuery = graphql` + query WebhooksSettingsPageQuery($organizationId: ID!) { + organization: node(id: $organizationId) @required(action: THROW) { + __typename + ... on Organization { + id + webhookConfigurations(first: 50) + @connection(key: "WebhooksSettingsPage_webhookConfigurations") { + edges { + node { + id + endpointUrl + selectedEvents + } + } + } + } + } + } +`; + +const createWebhookConfigurationMutation = graphql` + mutation WebhooksSettingsPage_createMutation( + $input: CreateWebhookConfigurationInput! + $connections: [ID!]! + ) { + createWebhookConfiguration(input: $input) { + webhookConfigurationEdge @prependEdge(connections: $connections) { + node { + id + endpointUrl + selectedEvents + } + } + } + } +`; + +const updateWebhookConfigurationMutation = graphql` + mutation WebhooksSettingsPage_updateMutation( + $input: UpdateWebhookConfigurationInput! + ) { + updateWebhookConfiguration(input: $input) { + webhookConfiguration { + id + endpointUrl + selectedEvents + updatedAt + } + } + } +`; + +const signingSecretQuery = graphql` + query WebhooksSettingsPage_signingSecretQuery($webhookConfigurationId: ID!) { + node(id: $webhookConfigurationId) { + ... on WebhookConfiguration { + signingSecret + } + } + } +`; + +const deleteWebhookConfigurationMutation = graphql` + mutation WebhooksSettingsPage_deleteMutation( + $input: DeleteWebhookConfigurationInput! + $connections: [ID!]! + ) { + deleteWebhookConfiguration(input: $input) { + deletedWebhookConfigurationId @deleteEdge(connections: $connections) + } + } +`; + +const EVENT_TYPES = [ + { value: "MEETING_CREATED", label: "meeting:created" }, + { value: "MEETING_UPDATED", label: "meeting:updated" }, + { value: "MEETING_DELETED", label: "meeting:deleted" }, + { value: "VENDOR_CREATED", label: "vendor:created" }, + { value: "VENDOR_UPDATED", label: "vendor:updated" }, + { value: "VENDOR_DELETED", label: "vendor:deleted" }, +] as const; + +type WebhookEventType = (typeof EVENT_TYPES)[number]["value"]; + +const WEBHOOK_EVENT_VALUES = EVENT_TYPES.map(e => e.value) as [ + WebhookEventType, + ...WebhookEventType[], +]; + +const webhookFormSchema = z.object({ + endpointUrl: z + .string() + .min(1, "Endpoint URL is required") + .url("Please enter a valid URL") + .refine( + (val) => { + try { + const url = new URL(val); + return url.protocol === "http:" || url.protocol === "https:"; + } catch { + return false; + } + }, + "URL must use http:// or https://", + ), + selectedEvents: z + .array(z.enum(WEBHOOK_EVENT_VALUES)) + .min(1, "At least one event must be selected"), +}); + +type WebhookFormData = z.infer; + +function WebhookFormDialog({ + mode, + initialValues, + onSubmit, + isSubmitting, + trigger, +}: { + mode: "create" | "edit"; + initialValues?: WebhookFormData; + onSubmit: (values: WebhookFormData) => void; + isSubmitting: boolean; + trigger: React.ReactNode; +}) { + const { __ } = useTranslate(); + const dialogRef = useDialogRef(); + const { register, handleSubmit, formState, setValue, watch, reset } + = useFormWithSchema(webhookFormSchema, { + defaultValues: { + endpointUrl: initialValues?.endpointUrl ?? "", + selectedEvents: initialValues?.selectedEvents ?? [], + }, + }); + + const selectedEvents = watch("selectedEvents"); + + const handleToggleEvent = (event: WebhookEventType) => { + const current = selectedEvents ?? []; + const next = current.includes(event) + ? current.filter(e => e !== event) + : [...current, event]; + setValue("selectedEvents", next, { shouldValidate: formState.isSubmitted }); + }; + + const onFormSubmit = (data: WebhookFormData) => { + onSubmit(data); + dialogRef.current?.close(); + reset(data); + }; + + return ( + +
void handleSubmit(onFormSubmit)(e)}> + +
+ + + +
+ +

+ {__("Select the events that will trigger this webhook.")} +

+
+ {EVENT_TYPES.map(event => ( + + ))} +
+ {formState.errors.selectedEvents?.message && ( +

+ {formState.errors.selectedEvents.message} +

+ )} +
+
+
+ + + +
+
+ ); +} + +export function WebhooksSettingsPage(props: { + queryRef: PreloadedQuery; +}) { + const { queryRef } = props; + const { __ } = useTranslate(); + const { toast } = useToast(); + const environment = useRelayEnvironment(); + const deleteDialogRef = useDialogRef(); + const [deletingId, setDeletingId] = useState(null); + const [revealedSecrets, setRevealedSecrets] = useState>({}); + const [loadingSecrets, setLoadingSecrets] = useState>(new Set()); + + const fetchSigningSecret = useCallback( + async (webhookConfigurationId: string): Promise => { + // Return cached secret if already fetched + if (revealedSecrets[webhookConfigurationId]) { + return revealedSecrets[webhookConfigurationId]; + } + + setLoadingSecrets(prev => new Set(prev).add(webhookConfigurationId)); + + try { + const data = await fetchQuery( + environment, + signingSecretQuery, + { webhookConfigurationId }, + ).toPromise(); + + const secret = data?.node?.signingSecret; + if (secret) { + setRevealedSecrets(prev => ({ ...prev, [webhookConfigurationId]: secret })); + return secret; + } + return null; + } catch { + toast({ + title: __("Error"), + description: __("Failed to load signing secret."), + variant: "error", + }); + return null; + } finally { + setLoadingSecrets((prev) => { + const next = new Set(prev); + next.delete(webhookConfigurationId); + return next; + }); + } + }, + [environment, revealedSecrets, toast, __], + ); + + const toggleRevealSecret = (id: string) => { + if (revealedSecrets[id]) { + setRevealedSecrets((prev) => { + const next = { ...prev }; + delete next[id]; + return next; + }); + } else { + void fetchSigningSecret(id); + } + }; + + const copyToClipboard = async (webhookConfigurationId: string, label: string) => { + const secret = await fetchSigningSecret(webhookConfigurationId); + if (secret) { + void navigator.clipboard.writeText(secret); + toast({ + title: __("Copied to clipboard"), + description: label, + variant: "success", + }); + } + }; + + const { organization } = usePreloadedQuery( + webhooksSettingsPageQuery, + queryRef, + ); + if (organization.__typename === "%other") { + throw new Error("Relay node is not an organization"); + } + + const [createWebhook, isCreating] + = useMutationWithToasts( + createWebhookConfigurationMutation, + { + successMessage: __("Webhook created successfully"), + errorMessage: __("Failed to create webhook"), + }, + ); + + const [updateWebhook, isUpdating] + = useMutationWithToasts( + updateWebhookConfigurationMutation, + { + successMessage: __("Webhook updated successfully"), + errorMessage: __("Failed to update webhook"), + }, + ); + + const [deleteWebhook, isDeleting] + = useMutationWithToasts( + deleteWebhookConfigurationMutation, + { + successMessage: __("Webhook deleted successfully"), + errorMessage: __("Failed to delete webhook"), + }, + ); + + const webhooks = organization.webhookConfigurations?.edges ?? []; + + const connectionId = ConnectionHandler.getConnectionID( + organization.id, + "WebhooksSettingsPage_webhookConfigurations", + ); + + const handleCreate = (values: WebhookFormData) => { + void createWebhook({ + variables: { + input: { + organizationId: organization.id, + endpointUrl: values.endpointUrl, + selectedEvents: values.selectedEvents, + }, + connections: [connectionId], + }, + }); + }; + + const handleUpdate = (id: string, values: WebhookFormData) => { + void updateWebhook({ + variables: { + input: { + id, + endpointUrl: values.endpointUrl, + selectedEvents: values.selectedEvents, + }, + }, + }); + }; + + const handleDelete = (id: string) => { + void deleteWebhook({ + variables: { + input: { + webhookConfigurationId: id, + }, + connections: [connectionId], + }, + onSuccess: () => { + setDeletingId(null); + deleteDialogRef.current?.close(); + }, + }); + }; + + return ( +
+
+
+

{__("Webhook Configurations")}

+

+ {__( + "Configure webhooks to receive notifications when events occur in your organization.", + )} +

+
+ + {__("Add Webhook Configuration")} + + )} + /> +
+ + {webhooks.length === 0 + ? ( + +
+

+ {__("No webhook configurations yet. Add one to get started.")} +

+
+
+ ) + : ( +
+ {webhooks.map(({ node: webhook }) => ( + +
+
+
+ +

+ {webhook.endpointUrl} +

+
+
+ +
+ + {revealedSecrets[webhook.id] + ? revealedSecrets[webhook.id] + : "••••••••••••••••••••••••••••••••"} + + +
+
+
+ +
+ {webhook.selectedEvents.map((event) => { + const eventLabel + = EVENT_TYPES.find(e => e.value === event)?.label ?? event; + return ( + + {eventLabel} + + ); + })} +
+
+
+
+ handleUpdate(webhook.id, values)} + isSubmitting={isUpdating} + trigger={( +
+
+
+ ))} +
+ )} + + + +

+ {__( + "Are you sure you want to delete this webhook configuration?", + )} +

+

+ {__("This action cannot be undone.")} +

+
+ + + +
+
+ ); +} diff --git a/apps/console/src/routes.tsx b/apps/console/src/routes.tsx index cce4319e2..638a7302c 100644 --- a/apps/console/src/routes.tsx +++ b/apps/console/src/routes.tsx @@ -205,6 +205,13 @@ const routes = [ import("./pages/iam/organizations/settings/SCIMSettingsPageLoader"), ), }, + { + path: "webhooks", + Component: lazy( + () => + import("./pages/iam/organizations/settings/WebhooksSettingsPageLoader"), + ), + }, ], }, ...peopleRoutes, diff --git a/pkg/coredata/entity_type_reg.go b/pkg/coredata/entity_type_reg.go index 5103faafa..fc7d3c441 100644 --- a/pkg/coredata/entity_type_reg.go +++ b/pkg/coredata/entity_type_reg.go @@ -79,6 +79,9 @@ const ( SCIMEventEntityType uint16 = 53 TokenEntityType uint16 = 54 SCIMBridgeEntityType uint16 = 55 + WebhookConfigurationEntityType uint16 = 56 + WebhookEventEntityType uint16 = 57 + WebhookCallEntityType uint16 = 58 ) func NewEntityFromID(id gid.GID) (any, bool) { @@ -191,6 +194,12 @@ func NewEntityFromID(id gid.GID) (any, bool) { return &Token{ID: id}, true case SCIMBridgeEntityType: return &SCIMBridge{ID: id}, true + case WebhookConfigurationEntityType: + return &WebhookConfiguration{ID: id}, true + case WebhookEventEntityType: + return &WebhookEvent{ID: id}, true + case WebhookCallEntityType: + return &WebhookCall{ID: id}, true default: return nil, false } diff --git a/pkg/coredata/migrations/20260210T135740Z.sql b/pkg/coredata/migrations/20260210T135740Z.sql new file mode 100644 index 000000000..6662a56e5 --- /dev/null +++ b/pkg/coredata/migrations/20260210T135740Z.sql @@ -0,0 +1,52 @@ +CREATE TYPE webhook_event_type AS ENUM ( + 'meeting:created', + 'meeting:updated', + 'meeting:deleted', + 'vendor:created', + 'vendor:updated', + 'vendor:deleted' +); + +CREATE TABLE webhook_configurations ( + id TEXT PRIMARY KEY, + tenant_id TEXT NOT NULL, + organization_id TEXT NOT NULL REFERENCES organizations(id) ON UPDATE CASCADE ON DELETE CASCADE, + endpoint_url TEXT NOT NULL, + selected_events webhook_event_type[] NOT NULL, + encrypted_signing_secret BYTEA NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL +); + +CREATE TYPE webhook_event_status AS ENUM ( + 'PENDING', + 'PROCESSING', + 'DELIVERED' +); + +CREATE TABLE webhook_events ( + id TEXT PRIMARY KEY, + tenant_id TEXT NOT NULL, + organization_id TEXT NOT NULL REFERENCES organizations(id) ON UPDATE CASCADE ON DELETE CASCADE, + event_type webhook_event_type NOT NULL, + status webhook_event_status NOT NULL, + data JSONB NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + processed_at TIMESTAMP WITH TIME ZONE +); + +CREATE TYPE webhook_call_status AS ENUM ( + 'SUCCEEDED', + 'FAILED' +); + +CREATE TABLE webhook_calls ( + id TEXT PRIMARY KEY, + tenant_id TEXT NOT NULL, + webhook_event_id TEXT NOT NULL REFERENCES webhook_events(id) ON UPDATE CASCADE ON DELETE CASCADE, + webhook_configuration_id TEXT NOT NULL REFERENCES webhook_configurations(id) ON UPDATE CASCADE ON DELETE CASCADE, + endpoint_url TEXT NOT NULL, + status webhook_call_status NOT NULL, + response JSONB, + created_at TIMESTAMP WITH TIME ZONE NOT NULL +); diff --git a/pkg/coredata/webhook_call.go b/pkg/coredata/webhook_call.go new file mode 100644 index 000000000..82aba8993 --- /dev/null +++ b/pkg/coredata/webhook_call.go @@ -0,0 +1,87 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/jackc/pgx/v5" + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/gid" +) + +type ( + WebhookCall struct { + ID gid.GID `db:"id"` + WebhookEventID gid.GID `db:"webhook_event_id"` + WebhookConfigurationID gid.GID `db:"webhook_configuration_id"` + EndpointURL string `db:"endpoint_url"` + Status WebhookCallStatus `db:"status"` + Response json.RawMessage `db:"response"` + CreatedAt time.Time `db:"created_at"` + } + + WebhookCalls []*WebhookCall +) + +func (w *WebhookCall) Insert( + ctx context.Context, + conn pg.Conn, + scope Scoper, +) error { + q := ` +INSERT INTO webhook_calls ( + id, + tenant_id, + webhook_event_id, + webhook_configuration_id, + endpoint_url, + status, + response, + created_at +) +VALUES ( + @id, + @tenant_id, + @webhook_event_id, + @webhook_configuration_id, + @endpoint_url, + @status, + @response, + @created_at +) +` + + args := pgx.StrictNamedArgs{ + "id": w.ID, + "tenant_id": scope.GetTenantID(), + "webhook_event_id": w.WebhookEventID, + "webhook_configuration_id": w.WebhookConfigurationID, + "endpoint_url": w.EndpointURL, + "status": w.Status, + "response": w.Response, + "created_at": w.CreatedAt, + } + + _, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot insert webhook call: %w", err) + } + + return nil +} diff --git a/pkg/coredata/webhook_call_status.go b/pkg/coredata/webhook_call_status.go new file mode 100644 index 000000000..034e37c3a --- /dev/null +++ b/pkg/coredata/webhook_call_status.go @@ -0,0 +1,64 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "database/sql/driver" + "fmt" +) + +type WebhookCallStatus string + +const ( + WebhookCallStatusSucceeded WebhookCallStatus = "SUCCEEDED" + WebhookCallStatusFailed WebhookCallStatus = "FAILED" +) + +func (s WebhookCallStatus) String() string { + return string(s) +} + +func (s WebhookCallStatus) IsValid() bool { + switch s { + case WebhookCallStatusSucceeded, WebhookCallStatusFailed: + return true + } + return false +} + +func (s WebhookCallStatus) MarshalText() ([]byte, error) { + return []byte(s.String()), nil +} + +func (s *WebhookCallStatus) UnmarshalText(text []byte) error { + *s = WebhookCallStatus(text) + if !s.IsValid() { + return fmt.Errorf("%s is not a valid WebhookCallStatus", string(text)) + } + return nil +} + +func (s *WebhookCallStatus) Scan(value any) error { + str, ok := value.(string) + if !ok { + return fmt.Errorf("unsupported type for WebhookCallStatus: %T", value) + } + + return s.UnmarshalText([]byte(str)) +} + +func (s WebhookCallStatus) Value() (driver.Value, error) { + return s.String(), nil +} diff --git a/pkg/coredata/webhook_configuration.go b/pkg/coredata/webhook_configuration.go new file mode 100644 index 000000000..c8ea7bccf --- /dev/null +++ b/pkg/coredata/webhook_configuration.go @@ -0,0 +1,411 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "context" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "maps" + "time" + + "github.com/jackc/pgx/v5" + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/crypto/cipher" + "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/page" +) + +type ( + WebhookConfiguration struct { + ID gid.GID `db:"id"` + OrganizationID gid.GID `db:"organization_id"` + EndpointURL string `db:"endpoint_url"` + SelectedEvents WebhookEventTypes `db:"selected_events"` + EncryptedSigningSecret []byte `db:"encrypted_signing_secret"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + } + + WebhookConfigurations []*WebhookConfiguration +) + +func (w *WebhookConfiguration) GenerateSigningSecret(encryptionKey cipher.EncryptionKey) (string, error) { + secret := make([]byte, 32) + if _, err := rand.Read(secret); err != nil { + return "", fmt.Errorf("cannot generate signing secret: %w", err) + } + + signingSecret := "whsec_" + hex.EncodeToString(secret) + + encrypted, err := cipher.Encrypt([]byte(signingSecret), encryptionKey) + if err != nil { + return "", fmt.Errorf("cannot encrypt signing secret: %w", err) + } + + w.EncryptedSigningSecret = encrypted + + return signingSecret, nil +} + +func (w *WebhookConfiguration) DecryptSigningSecret(encryptionKey cipher.EncryptionKey) (string, error) { + if len(w.EncryptedSigningSecret) == 0 { + return "", fmt.Errorf("no encrypted signing secret") + } + + plaintext, err := cipher.Decrypt(w.EncryptedSigningSecret, encryptionKey) + if err != nil { + return "", fmt.Errorf("cannot decrypt signing secret: %w", err) + } + + return string(plaintext), nil +} + +func (w WebhookConfiguration) CursorKey(orderBy WebhookConfigurationOrderField) page.CursorKey { + switch orderBy { + case WebhookConfigurationOrderFieldCreatedAt: + return page.NewCursorKey(w.ID, w.CreatedAt) + } + + panic(fmt.Sprintf("unsupported order by: %s", orderBy)) +} + +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (w *WebhookConfiguration) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM webhook_configurations WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, w.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query webhook configuration authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + +func (w *WebhookConfiguration) LoadByID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + webhookConfigurationID gid.GID, +) error { + q := ` +SELECT + id, + organization_id, + endpoint_url, + selected_events, + encrypted_signing_secret, + created_at, + updated_at +FROM + webhook_configurations +WHERE + %s + AND id = @webhook_configuration_id +LIMIT 1; +` + + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{"webhook_configuration_id": webhookConfigurationID} + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query webhook configurations: %w", err) + } + + wc, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[WebhookConfiguration]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrResourceNotFound + } + + return fmt.Errorf("cannot collect webhook configuration: %w", err) + } + + *w = wc + return nil +} + +func (w *WebhookConfigurations) LoadByOrganizationID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, + cursor *page.Cursor[WebhookConfigurationOrderField], +) error { + q := ` +SELECT + id, + organization_id, + endpoint_url, + selected_events, + encrypted_signing_secret, + created_at, + updated_at +FROM + webhook_configurations +WHERE + %s + AND organization_id = @organization_id + AND %s +` + q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) + + args := pgx.NamedArgs{"organization_id": organizationID} + maps.Copy(args, scope.SQLArguments()) + maps.Copy(args, cursor.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query webhook configurations: %w", err) + } + + configurations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[WebhookConfiguration]) + if err != nil { + return fmt.Errorf("cannot collect webhook configurations: %w", err) + } + + *w = configurations + return nil +} + +func (w *WebhookConfigurations) CountByOrganizationID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, +) (int, error) { + q := ` +SELECT + COUNT(*) +FROM + webhook_configurations +WHERE + %s + AND organization_id = @organization_id +` + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "organization_id": organizationID, + } + maps.Copy(args, scope.SQLArguments()) + + row := conn.QueryRow(ctx, q, args) + var count int + if err := row.Scan(&count); err != nil { + return 0, fmt.Errorf("cannot count webhook configurations: %w", err) + } + + return count, nil +} + +func (w *WebhookConfigurations) ExistsByOrganizationIDAndEventType( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, + eventType WebhookEventType, +) (bool, error) { + q := ` +SELECT EXISTS ( + SELECT 1 + FROM webhook_configurations + WHERE %s + AND organization_id = @organization_id + AND @event_type = ANY(selected_events) +) +` + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "organization_id": organizationID, + "event_type": eventType.String(), + } + maps.Copy(args, scope.SQLArguments()) + + var exists bool + if err := conn.QueryRow(ctx, q, args).Scan(&exists); err != nil { + return false, fmt.Errorf("cannot check webhook configuration existence: %w", err) + } + + return exists, nil +} + +func (w *WebhookConfiguration) Insert( + ctx context.Context, + conn pg.Conn, + scope Scoper, +) error { + q := ` +INSERT INTO + webhook_configurations ( + tenant_id, + id, + organization_id, + endpoint_url, + selected_events, + encrypted_signing_secret, + created_at, + updated_at + ) +VALUES ( + @tenant_id, + @webhook_configuration_id, + @organization_id, + @endpoint_url, + @selected_events, + @encrypted_signing_secret, + @created_at, + @updated_at +); +` + + args := pgx.StrictNamedArgs{ + "tenant_id": scope.GetTenantID(), + "webhook_configuration_id": w.ID, + "organization_id": w.OrganizationID, + "endpoint_url": w.EndpointURL, + "selected_events": w.SelectedEvents, + "encrypted_signing_secret": w.EncryptedSigningSecret, + "created_at": w.CreatedAt, + "updated_at": w.UpdatedAt, + } + _, err := conn.Exec(ctx, q, args) + + if err != nil { + return fmt.Errorf("cannot insert webhook configuration: %w", err) + } + + return nil +} + +func (w *WebhookConfiguration) Update( + ctx context.Context, + conn pg.Conn, + scope Scoper, +) error { + q := ` +UPDATE webhook_configurations +SET + endpoint_url = @endpoint_url, + selected_events = @selected_events, + updated_at = @updated_at +WHERE %s + AND id = @webhook_configuration_id +` + + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "webhook_configuration_id": w.ID, + "endpoint_url": w.EndpointURL, + "selected_events": w.SelectedEvents, + "updated_at": w.UpdatedAt, + } + maps.Copy(args, scope.SQLArguments()) + + result, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot update webhook configuration: %w", err) + } + + if result.RowsAffected() == 0 { + return ErrResourceNotFound + } + + return nil +} + +func (w *WebhookConfigurations) LoadMatchingByOrganizationIDAndEventType( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, + eventType WebhookEventType, +) error { + q := ` +SELECT + id, + organization_id, + endpoint_url, + selected_events, + encrypted_signing_secret, + created_at, + updated_at +FROM + webhook_configurations +WHERE + %s + AND organization_id = @organization_id + AND @event_type = ANY(selected_events) +` + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "organization_id": organizationID, + "event_type": eventType.String(), + } + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query matching webhook configurations: %w", err) + } + + configurations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[WebhookConfiguration]) + if err != nil { + return fmt.Errorf("cannot collect matching webhook configurations: %w", err) + } + + *w = configurations + return nil +} + +func (w *WebhookConfiguration) Delete( + ctx context.Context, + conn pg.Conn, + scope Scoper, +) error { + q := ` +DELETE FROM webhook_configurations +WHERE %s + AND id = @webhook_configuration_id +` + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "webhook_configuration_id": w.ID, + } + maps.Copy(args, scope.SQLArguments()) + + result, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot delete webhook configuration: %w", err) + } + + if result.RowsAffected() == 0 { + return ErrResourceNotFound + } + + return nil +} diff --git a/pkg/coredata/webhook_configuration_order_field.go b/pkg/coredata/webhook_configuration_order_field.go new file mode 100644 index 000000000..edf0adc63 --- /dev/null +++ b/pkg/coredata/webhook_configuration_order_field.go @@ -0,0 +1,55 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "fmt" +) + +type ( + WebhookConfigurationOrderField string +) + +const ( + WebhookConfigurationOrderFieldCreatedAt WebhookConfigurationOrderField = "CREATED_AT" +) + +func (p WebhookConfigurationOrderField) Column() string { + return string(p) +} + +func (p WebhookConfigurationOrderField) String() string { + return string(p) +} + +func (p WebhookConfigurationOrderField) IsValid() bool { + switch p { + case WebhookConfigurationOrderFieldCreatedAt: + return true + } + return false +} + +func (p WebhookConfigurationOrderField) MarshalText() ([]byte, error) { + return []byte(p.String()), nil +} + +func (p *WebhookConfigurationOrderField) UnmarshalText(text []byte) error { + *p = WebhookConfigurationOrderField(text) + if !p.IsValid() { + return fmt.Errorf("%s is not a valid WebhookConfigurationOrderField", string(text)) + } + return nil +} diff --git a/pkg/coredata/webhook_event.go b/pkg/coredata/webhook_event.go new file mode 100644 index 000000000..ff2123a42 --- /dev/null +++ b/pkg/coredata/webhook_event.go @@ -0,0 +1,160 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "maps" + "time" + + "github.com/jackc/pgx/v5" + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/gid" +) + +type ( + WebhookEvent struct { + ID gid.GID `db:"id"` + OrganizationID gid.GID `db:"organization_id"` + EventType WebhookEventType `db:"event_type"` + Status WebhookEventStatus `db:"status"` + Data json.RawMessage `db:"data"` + CreatedAt time.Time `db:"created_at"` + ProcessedAt *time.Time `db:"processed_at"` + } + + WebhookEvents []*WebhookEvent + +) + +func (w *WebhookEvent) Insert( + ctx context.Context, + conn pg.Conn, + scope Scoper, +) error { + q := ` +INSERT INTO webhook_events ( + id, + tenant_id, + organization_id, + event_type, + status, + data, + created_at +) +VALUES ( + @id, + @tenant_id, + @organization_id, + @event_type, + @status, + @data, + @created_at +) +` + + args := pgx.StrictNamedArgs{ + "id": w.ID, + "tenant_id": scope.GetTenantID(), + "organization_id": w.OrganizationID, + "event_type": w.EventType, + "status": w.Status, + "data": w.Data, + "created_at": w.CreatedAt, + } + + _, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot insert webhook event: %w", err) + } + + return nil +} + +func (w *WebhookEvent) LoadNextPendingForUpdate( + ctx context.Context, + conn pg.Conn, +) error { + q := ` +SELECT + id, + organization_id, + event_type, + status, + data, + created_at, + processed_at +FROM webhook_events +WHERE status = 'PENDING' +ORDER BY created_at ASC +LIMIT 1 +FOR UPDATE SKIP LOCKED +` + + rows, err := conn.Query(ctx, q) + if err != nil { + return fmt.Errorf("cannot query pending webhook events: %w", err) + } + + event, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[WebhookEvent]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrResourceNotFound + } + + return fmt.Errorf("cannot collect webhook event: %w", err) + } + + *w = event + return nil +} + +func (w *WebhookEvent) UpdateStatus( + ctx context.Context, + conn pg.Conn, + scope Scoper, +) error { + q := ` +UPDATE webhook_events +SET + status = @status, + processed_at = @processed_at +WHERE %s + AND id = @id +` + + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "id": w.ID, + "status": w.Status.String(), + "processed_at": w.ProcessedAt, + } + maps.Copy(args, scope.SQLArguments()) + + result, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot update webhook event: %w", err) + } + + if result.RowsAffected() == 0 { + return ErrResourceNotFound + } + + return nil +} diff --git a/pkg/coredata/webhook_event_status.go b/pkg/coredata/webhook_event_status.go new file mode 100644 index 000000000..80ebe4c8c --- /dev/null +++ b/pkg/coredata/webhook_event_status.go @@ -0,0 +1,65 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "database/sql/driver" + "fmt" +) + +type WebhookEventStatus string + +const ( + WebhookEventStatusPending WebhookEventStatus = "PENDING" + WebhookEventStatusProcessing WebhookEventStatus = "PROCESSING" + WebhookEventStatusDelivered WebhookEventStatus = "DELIVERED" +) + +func (s WebhookEventStatus) String() string { + return string(s) +} + +func (s WebhookEventStatus) IsValid() bool { + switch s { + case WebhookEventStatusPending, WebhookEventStatusProcessing, WebhookEventStatusDelivered: + return true + } + return false +} + +func (s WebhookEventStatus) MarshalText() ([]byte, error) { + return []byte(s.String()), nil +} + +func (s *WebhookEventStatus) UnmarshalText(text []byte) error { + *s = WebhookEventStatus(text) + if !s.IsValid() { + return fmt.Errorf("%s is not a valid WebhookEventStatus", string(text)) + } + return nil +} + +func (s *WebhookEventStatus) Scan(value any) error { + str, ok := value.(string) + if !ok { + return fmt.Errorf("unsupported type for WebhookEventStatus: %T", value) + } + + return s.UnmarshalText([]byte(str)) +} + +func (s WebhookEventStatus) Value() (driver.Value, error) { + return s.String(), nil +} diff --git a/pkg/coredata/webhook_event_type.go b/pkg/coredata/webhook_event_type.go new file mode 100644 index 000000000..d3baaca5d --- /dev/null +++ b/pkg/coredata/webhook_event_type.go @@ -0,0 +1,133 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "database/sql/driver" + "fmt" + "strings" +) + +type WebhookEventType string + +const ( + WebhookEventTypeMeetingCreated WebhookEventType = "meeting:created" + WebhookEventTypeMeetingUpdated WebhookEventType = "meeting:updated" + WebhookEventTypeMeetingDeleted WebhookEventType = "meeting:deleted" + WebhookEventTypeVendorCreated WebhookEventType = "vendor:created" + WebhookEventTypeVendorUpdated WebhookEventType = "vendor:updated" + WebhookEventTypeVendorDeleted WebhookEventType = "vendor:deleted" +) + +func (w WebhookEventType) String() string { + return string(w) +} + +func (w WebhookEventType) IsValid() bool { + switch w { + case WebhookEventTypeMeetingCreated, WebhookEventTypeMeetingUpdated, WebhookEventTypeMeetingDeleted, + WebhookEventTypeVendorCreated, WebhookEventTypeVendorUpdated, WebhookEventTypeVendorDeleted: + return true + } + return false +} + +func (w WebhookEventType) MarshalText() ([]byte, error) { + return []byte(w.String()), nil +} + +func (w *WebhookEventType) UnmarshalText(text []byte) error { + *w = WebhookEventType(text) + if !w.IsValid() { + return fmt.Errorf("%s is not a valid WebhookEventType", string(text)) + } + return nil +} + +func (w *WebhookEventType) Scan(value any) error { + var s string + switch v := value.(type) { + case string: + s = v + case []byte: + s = string(v) + default: + return fmt.Errorf("unsupported type for WebhookEventType: %T", value) + } + + return w.UnmarshalText([]byte(s)) +} + +func (w WebhookEventType) Value() (driver.Value, error) { + return w.String(), nil +} + +type WebhookEventTypes []WebhookEventType + +func (s *WebhookEventTypes) Scan(value any) error { + switch v := value.(type) { + case string: + return s.scanFromString(v) + case []byte: + return s.scanFromString(string(v)) + default: + return fmt.Errorf("unsupported type for WebhookEventTypes: %T", value) + } +} + +func (s *WebhookEventTypes) scanFromString(str string) error { + str = strings.TrimSpace(str) + if str == "{}" || str == "" { + *s = []WebhookEventType{} + return nil + } + + if strings.HasPrefix(str, "{") && strings.HasSuffix(str, "}") { + str = str[1 : len(str)-1] + } + + parts := strings.Split(str, ",") + result := make([]WebhookEventType, len(parts)) + + for i, part := range parts { + part = strings.TrimSpace(part) + + if strings.HasPrefix(part, `"`) && strings.HasSuffix(part, `"`) { + part = part[1 : len(part)-1] + } + + var et WebhookEventType + if err := et.Scan(part); err != nil { + return fmt.Errorf("invalid webhook event type in array: %s", part) + } + result[i] = et + } + + *s = result + return nil +} + +func (s WebhookEventTypes) Value() (driver.Value, error) { + if len(s) == 0 { + return "{}", nil + } + + values := make([]string, len(s)) + for i, et := range s { + values[i] = et.String() + } + + return "{" + strings.Join(values, ",") + "}", nil +} diff --git a/pkg/probo/actions.go b/pkg/probo/actions.go index 96d632711..9b0e0c50b 100644 --- a/pkg/probo/actions.go +++ b/pkg/probo/actions.go @@ -316,4 +316,11 @@ const ( ActionApplicabilityStatementCreate = "core:applicability-statement:create" ActionApplicabilityStatementUpdate = "core:applicability-statement:update" ActionApplicabilityStatementDelete = "core:applicability-statement:delete" + + // WebhookConfiguration actions + ActionWebhookConfigurationList = "core:webhook-configuration:list" + ActionWebhookConfigurationGet = "core:webhook-configuration:get" + ActionWebhookConfigurationCreate = "core:webhook-configuration:create" + ActionWebhookConfigurationUpdate = "core:webhook-configuration:update" + ActionWebhookConfigurationDelete = "core:webhook-configuration:delete" ) diff --git a/pkg/probo/meeting_service.go b/pkg/probo/meeting_service.go index 1cbf1d0be..eb5827396 100644 --- a/pkg/probo/meeting_service.go +++ b/pkg/probo/meeting_service.go @@ -23,7 +23,9 @@ import ( "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/page" + "go.probo.inc/probo/pkg/server/api/console/v1/types" "go.probo.inc/probo/pkg/validator" + "go.probo.inc/probo/pkg/webhook" ) type MeetingService struct { @@ -207,6 +209,10 @@ func (s MeetingService) Create( } } + if err := webhook.InsertEvent(ctx, conn, s.svc.scope, organization.ID, coredata.WebhookEventTypeMeetingCreated, types.NewMeeting(meeting)); err != nil { + return fmt.Errorf("cannot insert webhook event: %w", err) + } + return nil }, ) @@ -283,6 +289,10 @@ func (s MeetingService) Update( } } + if err := webhook.InsertEvent(ctx, conn, s.svc.scope, meeting.OrganizationID, coredata.WebhookEventTypeMeetingUpdated, types.NewMeeting(meeting)); err != nil { + return fmt.Errorf("cannot insert webhook event: %w", err) + } + return nil }, ) @@ -307,6 +317,10 @@ func (s MeetingService) Delete( return fmt.Errorf("cannot load meeting: %w", err) } + if err := webhook.InsertEvent(ctx, conn, s.svc.scope, meeting.OrganizationID, coredata.WebhookEventTypeMeetingDeleted, types.NewMeeting(meeting)); err != nil { + return fmt.Errorf("cannot insert webhook event: %w", err) + } + if err := meeting.Delete(ctx, conn, s.svc.scope); err != nil { return fmt.Errorf("cannot delete meeting: %w", err) } diff --git a/pkg/probo/policies.go b/pkg/probo/policies.go index d379a1f22..8ac03f78b 100644 --- a/pkg/probo/policies.go +++ b/pkg/probo/policies.go @@ -79,6 +79,7 @@ var ViewerPolicy = policy.NewPolicy( ActionRightsRequestGet, ActionRightsRequestList, ActionStateOfApplicabilityGet, ActionStateOfApplicabilityList, ActionApplicabilityStatementGet, ActionApplicabilityStatementList, + ActionWebhookConfigurationGet, ActionWebhookConfigurationList, ).WithSID("entity-read-access").When(organizationCondition), policy.Allow( diff --git a/pkg/probo/service.go b/pkg/probo/service.go index f61edbb57..772ad48d1 100644 --- a/pkg/probo/service.go +++ b/pkg/probo/service.go @@ -92,6 +92,7 @@ type ( Data *DatumService Audits *AuditService Meetings *MeetingService + WebhookConfigurations *WebhookConfigurationService Reports *ReportService TrustCenters *TrustCenterService TrustCenterAccesses *TrustCenterAccessService @@ -213,6 +214,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService { tenantService.Data = &DatumService{svc: tenantService} tenantService.Audits = &AuditService{svc: tenantService} tenantService.Meetings = &MeetingService{svc: tenantService} + tenantService.WebhookConfigurations = &WebhookConfigurationService{svc: tenantService} tenantService.Reports = &ReportService{svc: tenantService} tenantService.TrustCenters = &TrustCenterService{svc: tenantService} tenantService.TrustCenterAccesses = &TrustCenterAccessService{svc: tenantService} diff --git a/pkg/probo/vendor_service.go b/pkg/probo/vendor_service.go index 39dd33098..154dd50e0 100644 --- a/pkg/probo/vendor_service.go +++ b/pkg/probo/vendor_service.go @@ -23,7 +23,9 @@ import ( "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/page" + "go.probo.inc/probo/pkg/server/api/console/v1/types" "go.probo.inc/probo/pkg/validator" + "go.probo.inc/probo/pkg/webhook" ) type ( @@ -392,6 +394,10 @@ func (s VendorService) Update( return fmt.Errorf("cannot update vendor: %w", err) } + if err := webhook.InsertEvent(ctx, conn, s.svc.scope, vendor.OrganizationID, coredata.WebhookEventTypeVendorUpdated, types.NewVendor(vendor)); err != nil { + return fmt.Errorf("cannot insert webhook event: %w", err) + } + return nil }, ) @@ -427,10 +433,19 @@ func (s VendorService) Delete( ctx context.Context, vendorID gid.GID, ) error { - vendor := coredata.Vendor{ID: vendorID} - return s.svc.pg.WithConn( + vendor := &coredata.Vendor{} + + return s.svc.pg.WithTx( ctx, func(conn pg.Conn) error { + if err := vendor.LoadByID(ctx, conn, s.svc.scope, vendorID); err != nil { + return fmt.Errorf("cannot load vendor: %w", err) + } + + if err := webhook.InsertEvent(ctx, conn, s.svc.scope, vendor.OrganizationID, coredata.WebhookEventTypeVendorDeleted, types.NewVendor(vendor)); err != nil { + return fmt.Errorf("cannot insert webhook event: %w", err) + } + return vendor.Delete(ctx, conn, s.svc.scope) }, ) @@ -504,6 +519,10 @@ func (s VendorService) Create( return fmt.Errorf("cannot insert vendor: %w", err) } + if err := webhook.InsertEvent(ctx, conn, s.svc.scope, organization.ID, coredata.WebhookEventTypeVendorCreated, types.NewVendor(vendor)); err != nil { + return fmt.Errorf("cannot insert webhook event: %w", err) + } + return nil }, ) diff --git a/pkg/probo/webhook_configuration_service.go b/pkg/probo/webhook_configuration_service.go new file mode 100644 index 000000000..72f955ddd --- /dev/null +++ b/pkg/probo/webhook_configuration_service.go @@ -0,0 +1,290 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package probo + +import ( + "context" + "fmt" + "time" + + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/page" + "go.probo.inc/probo/pkg/validator" +) + +type WebhookConfigurationService struct { + svc *TenantService +} + +type ( + CreateWebhookConfigurationRequest struct { + OrganizationID gid.GID + EndpointURL string + SelectedEvents []coredata.WebhookEventType + } + + UpdateWebhookConfigurationRequest struct { + WebhookConfigurationID gid.GID + EndpointURL *string + SelectedEvents []coredata.WebhookEventType + } +) + +func (r *CreateWebhookConfigurationRequest) Validate() error { + v := validator.New() + + v.Check(r.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType)) + v.Check(r.EndpointURL, "endpoint_url", validator.Required(), validator.URL()) + + return v.Error() +} + +func (r *UpdateWebhookConfigurationRequest) Validate() error { + v := validator.New() + + v.Check(r.WebhookConfigurationID, "webhook_configuration_id", validator.Required(), validator.GID(coredata.WebhookConfigurationEntityType)) + v.Check(r.EndpointURL, "endpoint_url", validator.NotEmpty(), validator.URL()) + + return v.Error() +} + +func (s WebhookConfigurationService) ListForOrganizationID( + ctx context.Context, + organizationID gid.GID, + cursor *page.Cursor[coredata.WebhookConfigurationOrderField], +) (*page.Page[*coredata.WebhookConfiguration, coredata.WebhookConfigurationOrderField], error) { + var configurations coredata.WebhookConfigurations + organization := &coredata.Organization{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := organization.LoadByID(ctx, conn, s.svc.scope, organizationID); err != nil { + return fmt.Errorf("cannot load organization: %w", err) + } + + err := configurations.LoadByOrganizationID( + ctx, + conn, + s.svc.scope, + organization.ID, + cursor, + ) + if err != nil { + return fmt.Errorf("cannot load webhook configurations: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return page.NewPage(configurations, cursor), nil +} + +func (s WebhookConfigurationService) CountForOrganizationID( + ctx context.Context, + organizationID gid.GID, +) (int, error) { + var count int + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) (err error) { + configurations := &coredata.WebhookConfigurations{} + count, err = configurations.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) + if err != nil { + return fmt.Errorf("cannot count webhook configurations: %w", err) + } + + return nil + }, + ) + + if err != nil { + return 0, err + } + + return count, nil +} + +func (s WebhookConfigurationService) Get( + ctx context.Context, + webhookConfigurationID gid.GID, +) (*coredata.WebhookConfiguration, error) { + wc := &coredata.WebhookConfiguration{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := wc.LoadByID(ctx, conn, s.svc.scope, webhookConfigurationID); err != nil { + return fmt.Errorf("cannot load webhook configuration: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return wc, nil +} + +func (s WebhookConfigurationService) Create( + ctx context.Context, + req CreateWebhookConfigurationRequest, +) (*coredata.WebhookConfiguration, error) { + if err := req.Validate(); err != nil { + return nil, err + } + + now := time.Now() + var wc *coredata.WebhookConfiguration + organization := &coredata.Organization{} + + err := s.svc.pg.WithTx( + ctx, + func(conn pg.Conn) error { + if err := organization.LoadByID(ctx, conn, s.svc.scope, req.OrganizationID); err != nil { + return fmt.Errorf("cannot load organization: %w", err) + } + + wc = &coredata.WebhookConfiguration{ + ID: gid.New(organization.ID.TenantID(), coredata.WebhookConfigurationEntityType), + OrganizationID: organization.ID, + EndpointURL: req.EndpointURL, + SelectedEvents: req.SelectedEvents, + CreatedAt: now, + UpdatedAt: now, + } + + if _, err := wc.GenerateSigningSecret(s.svc.encryptionKey); err != nil { + return fmt.Errorf("cannot generate signing secret: %w", err) + } + + if err := wc.Insert(ctx, conn, s.svc.scope); err != nil { + return fmt.Errorf("cannot insert webhook configuration: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return wc, nil +} + +func (s WebhookConfigurationService) Update( + ctx context.Context, + req UpdateWebhookConfigurationRequest, +) (*coredata.WebhookConfiguration, error) { + if err := req.Validate(); err != nil { + return nil, err + } + + wc := &coredata.WebhookConfiguration{} + + err := s.svc.pg.WithTx( + ctx, + func(conn pg.Conn) error { + if err := wc.LoadByID(ctx, conn, s.svc.scope, req.WebhookConfigurationID); err != nil { + return fmt.Errorf("cannot load webhook configuration: %w", err) + } + + if req.EndpointURL != nil { + wc.EndpointURL = *req.EndpointURL + } + if req.SelectedEvents != nil { + wc.SelectedEvents = req.SelectedEvents + } + + wc.UpdatedAt = time.Now() + + if err := wc.Update(ctx, conn, s.svc.scope); err != nil { + return fmt.Errorf("cannot update webhook configuration: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return wc, nil +} + +func (s WebhookConfigurationService) GetSigningSecret( + ctx context.Context, + webhookConfigurationID gid.GID, +) (string, error) { + wc := &coredata.WebhookConfiguration{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := wc.LoadByID(ctx, conn, s.svc.scope, webhookConfigurationID); err != nil { + return fmt.Errorf("cannot load webhook configuration: %w", err) + } + + return nil + }, + ) + + if err != nil { + return "", err + } + + return wc.DecryptSigningSecret(s.svc.encryptionKey) +} + +func (s WebhookConfigurationService) Delete( + ctx context.Context, + webhookConfigurationID gid.GID, +) error { + wc := &coredata.WebhookConfiguration{ID: webhookConfigurationID} + + err := s.svc.pg.WithTx( + ctx, + func(conn pg.Conn) error { + if err := wc.LoadByID(ctx, conn, s.svc.scope, webhookConfigurationID); err != nil { + return fmt.Errorf("cannot load webhook configuration: %w", err) + } + + if err := wc.Delete(ctx, conn, s.svc.scope); err != nil { + return fmt.Errorf("cannot delete webhook configuration: %w", err) + } + + return nil + }, + ) + + if err != nil { + return err + } + + return nil +} diff --git a/pkg/probod/notifications_config.go b/pkg/probod/notifications_config.go index af0d2eee8..14d790ba2 100644 --- a/pkg/probod/notifications_config.go +++ b/pkg/probod/notifications_config.go @@ -15,6 +15,11 @@ package probod type notificationsConfig struct { - Mailer mailerConfig `json:"mailer"` - Slack slackConfig `json:"slack"` + Mailer mailerConfig `json:"mailer"` + Slack slackConfig `json:"slack"` + Webhook webhookConfig `json:"webhook"` +} + +type webhookConfig struct { + SenderInterval int `json:"sender-interval"` } diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index 90d6d7729..f9819f506 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -60,6 +60,7 @@ import ( "go.probo.inc/probo/pkg/server" "go.probo.inc/probo/pkg/slack" "go.probo.inc/probo/pkg/trust" + "go.probo.inc/probo/pkg/webhook" "golang.org/x/sync/errgroup" ) @@ -154,6 +155,9 @@ func New() *Implm { Slack: slackConfig{ SenderInterval: 60, }, + Webhook: webhookConfig{ + SenderInterval: 5, + }, }, CustomDomains: customDomainsConfig{ RenewalInterval: 3600, @@ -479,6 +483,19 @@ func (impl *Implm) Run( }, ) + webhookSenderCtx, stopWebhookSender := context.WithCancel(context.Background()) + webhookSender := webhook.NewSender(pgClient, l.Named("webhook-sender"), webhook.Config{ + Interval: time.Duration(impl.cfg.Notifications.Webhook.SenderInterval) * time.Second, + EncryptionKey: impl.cfg.EncryptionKey, + }) + wg.Go( + func() { + if err := webhookSender.Run(webhookSenderCtx); err != nil { + cancel(fmt.Errorf("webhook sender crashed: %w", err)) + } + }, + ) + exportJobExporterCtx, stopExportJobExporter := context.WithCancel(context.Background()) wg.Go( func() { @@ -511,6 +528,7 @@ func (impl *Implm) Run( stopMailer() stopSlackSender() + stopWebhookSender() stopExportJobExporter() stopIAMService() stopApiServer() diff --git a/pkg/server/api/connect/v1/schema/schema.go b/pkg/server/api/connect/v1/schema/schema.go index 125f15bb5..c4b99a3ae 100644 --- a/pkg/server/api/connect/v1/schema/schema.go +++ b/pkg/server/api/connect/v1/schema/schema.go @@ -2654,6 +2654,7 @@ type Mutation { updateSCIMBridge( input: UpdateSCIMBridgeInput! ): UpdateSCIMBridgePayload @session(required: PRESENT) + } type Identity implements Node { @@ -3499,6 +3500,8 @@ type RegenerateSCIMTokenPayload { type UpdateSCIMBridgePayload { scimBridge: SCIMBridge! } + + `, BuiltIn: false}, {Name: "../../../../gqlutils/directives/session/schema.graphql", Input: `# Session directive for GraphQL APIs # Include this schema in your gqlgen configuration to enable session-based access control. diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 205d1e678..18543b1bd 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -488,6 +488,16 @@ enum MeetingOrderField ) } +enum WebhookConfigurationOrderField + @goModel( + model: "go.probo.inc/probo/pkg/coredata.WebhookConfigurationOrderField" + ) { + CREATED_AT + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.WebhookConfigurationOrderFieldCreatedAt" + ) +} + enum RiskOrderField @goModel(model: "go.probo.inc/probo/pkg/coredata.RiskOrderField") { CREATED_AT @@ -1345,6 +1355,14 @@ input MeetingOrder field: MeetingOrderField! } +input WebhookConfigurationOrder + @goModel( + model: "go.probo.inc/probo/pkg/server/api/console/v1/types.WebhookConfigurationOrderBy" + ) { + direction: OrderDirection! + field: WebhookConfigurationOrderField! +} + input RiskOrder @goModel( model: "go.probo.inc/probo/pkg/server/api/console/v1/types.RiskOrderBy" @@ -1817,6 +1835,14 @@ type Organization implements Node { customDomain: CustomDomain @goField(forceResolver: true) + webhookConfigurations( + first: Int + after: CursorKey + last: Int + before: CursorKey + orderBy: WebhookConfigurationOrder + ): WebhookConfigurationConnection! @goField(forceResolver: true) + createdAt: Datetime! updatedAt: Datetime! @@ -2242,6 +2268,48 @@ type Meeting implements Node { permission(action: String!): Boolean! @goField(forceResolver: true) } +enum WebhookEventType + @goModel(model: "go.probo.inc/probo/pkg/coredata.WebhookEventType") { + MEETING_CREATED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeMeetingCreated") + MEETING_UPDATED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeMeetingUpdated") + MEETING_DELETED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeMeetingDeleted") + VENDOR_CREATED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeVendorCreated") + VENDOR_UPDATED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeVendorUpdated") + VENDOR_DELETED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeVendorDeleted") +} + +type WebhookConfiguration implements Node { + id: ID! + organization: Organization @goField(forceResolver: true) + endpointUrl: String! + signingSecret: String! @goField(forceResolver: true) + selectedEvents: [WebhookEventType!]! + createdAt: Datetime! + updatedAt: Datetime! + + permission(action: String!): Boolean! @goField(forceResolver: true) +} + +type WebhookConfigurationConnection + @goModel( + model: "go.probo.inc/probo/pkg/server/api/console/v1/types.WebhookConfigurationConnection" + ) { + edges: [WebhookConfigurationEdge!]! + pageInfo: PageInfo! + totalCount: Int! @goField(forceResolver: true) +} + +type WebhookConfigurationEdge { + cursor: CursorKey! + node: WebhookConfiguration! +} + type StateOfApplicability implements Node { id: ID! name: String! @@ -3270,6 +3338,16 @@ type Mutation { createMeeting(input: CreateMeetingInput!): CreateMeetingPayload! updateMeeting(input: UpdateMeetingInput!): UpdateMeetingPayload! deleteMeeting(input: DeleteMeetingInput!): DeleteMeetingPayload! + # WebhookConfiguration mutations + createWebhookConfiguration( + input: CreateWebhookConfigurationInput! + ): CreateWebhookConfigurationPayload! + updateWebhookConfiguration( + input: UpdateWebhookConfigurationInput! + ): UpdateWebhookConfigurationPayload! + deleteWebhookConfiguration( + input: DeleteWebhookConfigurationInput! + ): DeleteWebhookConfigurationPayload! # StateOfApplicability mutations createStateOfApplicability( input: CreateStateOfApplicabilityInput! @@ -4621,6 +4699,34 @@ type DeleteMeetingPayload { deletedMeetingId: ID! } +input CreateWebhookConfigurationInput { + organizationId: ID! + endpointUrl: String! + selectedEvents: [WebhookEventType!]! +} + +input UpdateWebhookConfigurationInput { + id: ID! + endpointUrl: String + selectedEvents: [WebhookEventType!] +} + +input DeleteWebhookConfigurationInput { + webhookConfigurationId: ID! +} + +type CreateWebhookConfigurationPayload { + webhookConfigurationEdge: WebhookConfigurationEdge! +} + +type UpdateWebhookConfigurationPayload { + webhookConfiguration: WebhookConfiguration! +} + +type DeleteWebhookConfigurationPayload { + deletedWebhookConfigurationId: ID! +} + type CreateStateOfApplicabilityPayload { stateOfApplicabilityEdge: StateOfApplicabilityEdge! } diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index bde68d222..e5d01382f 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -119,6 +119,8 @@ type ResolverRoot interface { VendorRiskAssessment() VendorRiskAssessmentResolver VendorService() VendorServiceResolver Viewer() ViewerResolver + WebhookConfiguration() WebhookConfigurationResolver + WebhookConfigurationConnection() WebhookConfigurationConnectionResolver } type DirectiveRoot struct { @@ -445,6 +447,10 @@ type ComplexityRoot struct { VendorServiceEdge func(childComplexity int) int } + CreateWebhookConfigurationPayload struct { + WebhookConfigurationEdge func(childComplexity int) int + } + CustomDomain struct { CreatedAt func(childComplexity int) int DNSRecords func(childComplexity int) int @@ -690,6 +696,10 @@ type ComplexityRoot struct { DeletedVendorServiceID func(childComplexity int) int } + DeleteWebhookConfigurationPayload struct { + DeletedWebhookConfigurationID func(childComplexity int) int + } + Document struct { Classification func(childComplexity int) int Controls func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) int @@ -971,6 +981,7 @@ type ComplexityRoot struct { CreateVendorContact func(childComplexity int, input types.CreateVendorContactInput) int CreateVendorRiskAssessment func(childComplexity int, input types.CreateVendorRiskAssessmentInput) int CreateVendorService func(childComplexity int, input types.CreateVendorServiceInput) int + CreateWebhookConfiguration func(childComplexity int, input types.CreateWebhookConfigurationInput) int DeleteApplicabilityStatement func(childComplexity int, input types.DeleteApplicabilityStatementInput) int DeleteAsset func(childComplexity int, input types.DeleteAssetInput) int DeleteAudit func(childComplexity int, input types.DeleteAuditInput) int @@ -1013,6 +1024,7 @@ type ComplexityRoot struct { DeleteVendorContact func(childComplexity int, input types.DeleteVendorContactInput) int DeleteVendorDataPrivacyAgreement func(childComplexity int, input types.DeleteVendorDataPrivacyAgreementInput) int DeleteVendorService func(childComplexity int, input types.DeleteVendorServiceInput) int + DeleteWebhookConfiguration func(childComplexity int, input types.DeleteWebhookConfigurationInput) int ExportDataProtectionImpactAssessmentsPDF func(childComplexity int, input types.ExportDataProtectionImpactAssessmentsPDFInput) int ExportDocumentVersionPDF func(childComplexity int, input types.ExportDocumentVersionPDFInput) int ExportFramework func(childComplexity int, input types.ExportFrameworkInput) int @@ -1060,6 +1072,7 @@ type ComplexityRoot struct { UpdateVendorContact func(childComplexity int, input types.UpdateVendorContactInput) int UpdateVendorDataPrivacyAgreement func(childComplexity int, input types.UpdateVendorDataPrivacyAgreementInput) int UpdateVendorService func(childComplexity int, input types.UpdateVendorServiceInput) int + UpdateWebhookConfiguration func(childComplexity int, input types.UpdateWebhookConfigurationInput) int UploadAuditReport func(childComplexity int, input types.UploadAuditReportInput) int UploadMeasureEvidence func(childComplexity int, input types.UploadMeasureEvidenceInput) int UploadTrustCenterNda func(childComplexity int, input types.UploadTrustCenterNDAInput) int @@ -1166,6 +1179,7 @@ type ComplexityRoot struct { TrustCenterFiles func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.OrderBy[coredata.TrustCenterFileOrderField]) int UpdatedAt func(childComplexity int) int Vendors func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy, filter *types.VendorFilter) int + WebhookConfigurations func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.WebhookConfigurationOrderBy) int WebsiteURL func(childComplexity int) int } @@ -1723,6 +1737,10 @@ type ComplexityRoot struct { VendorService func(childComplexity int) int } + UpdateWebhookConfigurationPayload struct { + WebhookConfiguration func(childComplexity int) int + } + UploadAuditReportPayload struct { Audit func(childComplexity int) int } @@ -1910,6 +1928,28 @@ type ComplexityRoot struct { SignableDocument func(childComplexity int, id gid.GID) int SignableDocuments func(childComplexity int, organizationID gid.GID, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentOrderBy) int } + + WebhookConfiguration struct { + CreatedAt func(childComplexity int) int + EndpointURL func(childComplexity int) int + ID func(childComplexity int) int + Organization func(childComplexity int) int + Permission func(childComplexity int, action string) int + SelectedEvents func(childComplexity int) int + SigningSecret func(childComplexity int) int + UpdatedAt func(childComplexity int) int + } + + WebhookConfigurationConnection struct { + Edges func(childComplexity int) int + PageInfo func(childComplexity int) int + TotalCount func(childComplexity int) int + } + + WebhookConfigurationEdge struct { + Cursor func(childComplexity int) int + Node func(childComplexity int) int + } } type ApplicabilityStatementResolver interface { @@ -2153,6 +2193,9 @@ type MutationResolver interface { CreateMeeting(ctx context.Context, input types.CreateMeetingInput) (*types.CreateMeetingPayload, error) UpdateMeeting(ctx context.Context, input types.UpdateMeetingInput) (*types.UpdateMeetingPayload, error) DeleteMeeting(ctx context.Context, input types.DeleteMeetingInput) (*types.DeleteMeetingPayload, error) + CreateWebhookConfiguration(ctx context.Context, input types.CreateWebhookConfigurationInput) (*types.CreateWebhookConfigurationPayload, error) + UpdateWebhookConfiguration(ctx context.Context, input types.UpdateWebhookConfigurationInput) (*types.UpdateWebhookConfigurationPayload, error) + DeleteWebhookConfiguration(ctx context.Context, input types.DeleteWebhookConfigurationInput) (*types.DeleteWebhookConfigurationPayload, error) CreateStateOfApplicability(ctx context.Context, input types.CreateStateOfApplicabilityInput) (*types.CreateStateOfApplicabilityPayload, error) UpdateStateOfApplicability(ctx context.Context, input types.UpdateStateOfApplicabilityInput) (*types.UpdateStateOfApplicabilityPayload, error) DeleteStateOfApplicability(ctx context.Context, input types.DeleteStateOfApplicabilityInput) (*types.DeleteStateOfApplicabilityPayload, error) @@ -2266,6 +2309,7 @@ type OrganizationResolver interface { TrustCenterFiles(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.OrderBy[coredata.TrustCenterFileOrderField]) (*types.TrustCenterFileConnection, error) TrustCenter(ctx context.Context, obj *types.Organization) (*types.TrustCenter, error) CustomDomain(ctx context.Context, obj *types.Organization) (*types.CustomDomain, error) + WebhookConfigurations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.WebhookConfigurationOrderBy) (*types.WebhookConfigurationConnection, error) Permission(ctx context.Context, obj *types.Organization, action string) (bool, error) } @@ -2461,6 +2505,16 @@ type ViewerResolver interface { SignableDocuments(ctx context.Context, obj *types.Viewer, organizationID gid.GID, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentOrderBy) (*types.SignableDocumentConnection, error) SignableDocument(ctx context.Context, obj *types.Viewer, id gid.GID) (*types.SignableDocument, error) } +type WebhookConfigurationResolver interface { + Organization(ctx context.Context, obj *types.WebhookConfiguration) (*types.Organization, error) + + SigningSecret(ctx context.Context, obj *types.WebhookConfiguration) (string, error) + + Permission(ctx context.Context, obj *types.WebhookConfiguration, action string) (bool, error) +} +type WebhookConfigurationConnectionResolver interface { + TotalCount(ctx context.Context, obj *types.WebhookConfigurationConnection) (int, error) +} type executableSchema struct { schema *ast.Schema @@ -3475,6 +3529,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.CreateVendorServicePayload.VendorServiceEdge(childComplexity), true + case "CreateWebhookConfigurationPayload.webhookConfigurationEdge": + if e.complexity.CreateWebhookConfigurationPayload.WebhookConfigurationEdge == nil { + break + } + + return e.complexity.CreateWebhookConfigurationPayload.WebhookConfigurationEdge(childComplexity), true + case "CustomDomain.createdAt": if e.complexity.CustomDomain.CreatedAt == nil { break @@ -4115,6 +4176,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.DeleteVendorServicePayload.DeletedVendorServiceID(childComplexity), true + case "DeleteWebhookConfigurationPayload.deletedWebhookConfigurationId": + if e.complexity.DeleteWebhookConfigurationPayload.DeletedWebhookConfigurationID == nil { + break + } + + return e.complexity.DeleteWebhookConfigurationPayload.DeletedWebhookConfigurationID(childComplexity), true + case "Document.classification": if e.complexity.Document.Classification == nil { break @@ -5507,6 +5575,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Mutation.CreateVendorService(childComplexity, args["input"].(types.CreateVendorServiceInput)), true + case "Mutation.createWebhookConfiguration": + if e.complexity.Mutation.CreateWebhookConfiguration == nil { + break + } + + args, err := ec.field_Mutation_createWebhookConfiguration_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.CreateWebhookConfiguration(childComplexity, args["input"].(types.CreateWebhookConfigurationInput)), true case "Mutation.deleteApplicabilityStatement": if e.complexity.Mutation.DeleteApplicabilityStatement == nil { break @@ -5969,6 +6048,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Mutation.DeleteVendorService(childComplexity, args["input"].(types.DeleteVendorServiceInput)), true + case "Mutation.deleteWebhookConfiguration": + if e.complexity.Mutation.DeleteWebhookConfiguration == nil { + break + } + + args, err := ec.field_Mutation_deleteWebhookConfiguration_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.DeleteWebhookConfiguration(childComplexity, args["input"].(types.DeleteWebhookConfigurationInput)), true case "Mutation.exportDataProtectionImpactAssessmentsPDF": if e.complexity.Mutation.ExportDataProtectionImpactAssessmentsPDF == nil { break @@ -6486,6 +6576,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Mutation.UpdateVendorService(childComplexity, args["input"].(types.UpdateVendorServiceInput)), true + case "Mutation.updateWebhookConfiguration": + if e.complexity.Mutation.UpdateWebhookConfiguration == nil { + break + } + + args, err := ec.field_Mutation_updateWebhookConfiguration_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.UpdateWebhookConfiguration(childComplexity, args["input"].(types.UpdateWebhookConfigurationInput)), true case "Mutation.uploadAuditReport": if e.complexity.Mutation.UploadAuditReport == nil { break @@ -7163,6 +7264,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Organization.Vendors(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.VendorOrderBy), args["filter"].(*types.VendorFilter)), true + case "Organization.webhookConfigurations": + if e.complexity.Organization.WebhookConfigurations == nil { + break + } + + args, err := ec.field_Organization_webhookConfigurations_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Organization.WebhookConfigurations(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.WebhookConfigurationOrderBy)), true case "Organization.websiteUrl": if e.complexity.Organization.WebsiteURL == nil { break @@ -9196,6 +9308,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.UpdateVendorServicePayload.VendorService(childComplexity), true + case "UpdateWebhookConfigurationPayload.webhookConfiguration": + if e.complexity.UpdateWebhookConfigurationPayload.WebhookConfiguration == nil { + break + } + + return e.complexity.UpdateWebhookConfigurationPayload.WebhookConfiguration(childComplexity), true + case "UploadAuditReportPayload.audit": if e.complexity.UploadAuditReportPayload.Audit == nil { break @@ -9981,6 +10100,92 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Viewer.SignableDocuments(childComplexity, args["organizationId"].(gid.GID), args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.DocumentOrderBy)), true + case "WebhookConfiguration.createdAt": + if e.complexity.WebhookConfiguration.CreatedAt == nil { + break + } + + return e.complexity.WebhookConfiguration.CreatedAt(childComplexity), true + case "WebhookConfiguration.endpointUrl": + if e.complexity.WebhookConfiguration.EndpointURL == nil { + break + } + + return e.complexity.WebhookConfiguration.EndpointURL(childComplexity), true + case "WebhookConfiguration.id": + if e.complexity.WebhookConfiguration.ID == nil { + break + } + + return e.complexity.WebhookConfiguration.ID(childComplexity), true + case "WebhookConfiguration.organization": + if e.complexity.WebhookConfiguration.Organization == nil { + break + } + + return e.complexity.WebhookConfiguration.Organization(childComplexity), true + case "WebhookConfiguration.permission": + if e.complexity.WebhookConfiguration.Permission == nil { + break + } + + args, err := ec.field_WebhookConfiguration_permission_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.WebhookConfiguration.Permission(childComplexity, args["action"].(string)), true + case "WebhookConfiguration.selectedEvents": + if e.complexity.WebhookConfiguration.SelectedEvents == nil { + break + } + + return e.complexity.WebhookConfiguration.SelectedEvents(childComplexity), true + case "WebhookConfiguration.signingSecret": + if e.complexity.WebhookConfiguration.SigningSecret == nil { + break + } + + return e.complexity.WebhookConfiguration.SigningSecret(childComplexity), true + case "WebhookConfiguration.updatedAt": + if e.complexity.WebhookConfiguration.UpdatedAt == nil { + break + } + + return e.complexity.WebhookConfiguration.UpdatedAt(childComplexity), true + + case "WebhookConfigurationConnection.edges": + if e.complexity.WebhookConfigurationConnection.Edges == nil { + break + } + + return e.complexity.WebhookConfigurationConnection.Edges(childComplexity), true + case "WebhookConfigurationConnection.pageInfo": + if e.complexity.WebhookConfigurationConnection.PageInfo == nil { + break + } + + return e.complexity.WebhookConfigurationConnection.PageInfo(childComplexity), true + case "WebhookConfigurationConnection.totalCount": + if e.complexity.WebhookConfigurationConnection.TotalCount == nil { + break + } + + return e.complexity.WebhookConfigurationConnection.TotalCount(childComplexity), true + + case "WebhookConfigurationEdge.cursor": + if e.complexity.WebhookConfigurationEdge.Cursor == nil { + break + } + + return e.complexity.WebhookConfigurationEdge.Cursor(childComplexity), true + case "WebhookConfigurationEdge.node": + if e.complexity.WebhookConfigurationEdge.Node == nil { + break + } + + return e.complexity.WebhookConfigurationEdge.Node(childComplexity), true + } return 0, false } @@ -10041,6 +10246,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputCreateVendorInput, ec.unmarshalInputCreateVendorRiskAssessmentInput, ec.unmarshalInputCreateVendorServiceInput, + ec.unmarshalInputCreateWebhookConfigurationInput, ec.unmarshalInputDataProtectionImpactAssessmentFilter, ec.unmarshalInputDataProtectionImpactAssessmentOrder, ec.unmarshalInputDatumFilter, @@ -10087,6 +10293,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputDeleteVendorDataPrivacyAgreementInput, ec.unmarshalInputDeleteVendorInput, ec.unmarshalInputDeleteVendorServiceInput, + ec.unmarshalInputDeleteWebhookConfigurationInput, ec.unmarshalInputDocumentFilter, ec.unmarshalInputDocumentOrder, ec.unmarshalInputDocumentVersionFilter, @@ -10167,6 +10374,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputUpdateVendorDataPrivacyAgreementInput, ec.unmarshalInputUpdateVendorInput, ec.unmarshalInputUpdateVendorServiceInput, + ec.unmarshalInputUpdateWebhookConfigurationInput, ec.unmarshalInputUploadAuditReportInput, ec.unmarshalInputUploadMeasureEvidenceInput, ec.unmarshalInputUploadTrustCenterNDAInput, @@ -10179,6 +10387,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputVendorOrder, ec.unmarshalInputVendorRiskAssessmentOrder, ec.unmarshalInputVendorServiceOrder, + ec.unmarshalInputWebhookConfigurationOrder, ) first := true @@ -10766,6 +10975,16 @@ enum MeetingOrderField ) } +enum WebhookConfigurationOrderField + @goModel( + model: "go.probo.inc/probo/pkg/coredata.WebhookConfigurationOrderField" + ) { + CREATED_AT + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.WebhookConfigurationOrderFieldCreatedAt" + ) +} + enum RiskOrderField @goModel(model: "go.probo.inc/probo/pkg/coredata.RiskOrderField") { CREATED_AT @@ -11623,6 +11842,14 @@ input MeetingOrder field: MeetingOrderField! } +input WebhookConfigurationOrder + @goModel( + model: "go.probo.inc/probo/pkg/server/api/console/v1/types.WebhookConfigurationOrderBy" + ) { + direction: OrderDirection! + field: WebhookConfigurationOrderField! +} + input RiskOrder @goModel( model: "go.probo.inc/probo/pkg/server/api/console/v1/types.RiskOrderBy" @@ -12095,6 +12322,14 @@ type Organization implements Node { customDomain: CustomDomain @goField(forceResolver: true) + webhookConfigurations( + first: Int + after: CursorKey + last: Int + before: CursorKey + orderBy: WebhookConfigurationOrder + ): WebhookConfigurationConnection! @goField(forceResolver: true) + createdAt: Datetime! updatedAt: Datetime! @@ -12520,6 +12755,48 @@ type Meeting implements Node { permission(action: String!): Boolean! @goField(forceResolver: true) } +enum WebhookEventType + @goModel(model: "go.probo.inc/probo/pkg/coredata.WebhookEventType") { + MEETING_CREATED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeMeetingCreated") + MEETING_UPDATED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeMeetingUpdated") + MEETING_DELETED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeMeetingDeleted") + VENDOR_CREATED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeVendorCreated") + VENDOR_UPDATED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeVendorUpdated") + VENDOR_DELETED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.WebhookEventTypeVendorDeleted") +} + +type WebhookConfiguration implements Node { + id: ID! + organization: Organization @goField(forceResolver: true) + endpointUrl: String! + signingSecret: String! @goField(forceResolver: true) + selectedEvents: [WebhookEventType!]! + createdAt: Datetime! + updatedAt: Datetime! + + permission(action: String!): Boolean! @goField(forceResolver: true) +} + +type WebhookConfigurationConnection + @goModel( + model: "go.probo.inc/probo/pkg/server/api/console/v1/types.WebhookConfigurationConnection" + ) { + edges: [WebhookConfigurationEdge!]! + pageInfo: PageInfo! + totalCount: Int! @goField(forceResolver: true) +} + +type WebhookConfigurationEdge { + cursor: CursorKey! + node: WebhookConfiguration! +} + type StateOfApplicability implements Node { id: ID! name: String! @@ -13548,6 +13825,16 @@ type Mutation { createMeeting(input: CreateMeetingInput!): CreateMeetingPayload! updateMeeting(input: UpdateMeetingInput!): UpdateMeetingPayload! deleteMeeting(input: DeleteMeetingInput!): DeleteMeetingPayload! + # WebhookConfiguration mutations + createWebhookConfiguration( + input: CreateWebhookConfigurationInput! + ): CreateWebhookConfigurationPayload! + updateWebhookConfiguration( + input: UpdateWebhookConfigurationInput! + ): UpdateWebhookConfigurationPayload! + deleteWebhookConfiguration( + input: DeleteWebhookConfigurationInput! + ): DeleteWebhookConfigurationPayload! # StateOfApplicability mutations createStateOfApplicability( input: CreateStateOfApplicabilityInput! @@ -14899,6 +15186,34 @@ type DeleteMeetingPayload { deletedMeetingId: ID! } +input CreateWebhookConfigurationInput { + organizationId: ID! + endpointUrl: String! + selectedEvents: [WebhookEventType!]! +} + +input UpdateWebhookConfigurationInput { + id: ID! + endpointUrl: String + selectedEvents: [WebhookEventType!] +} + +input DeleteWebhookConfigurationInput { + webhookConfigurationId: ID! +} + +type CreateWebhookConfigurationPayload { + webhookConfigurationEdge: WebhookConfigurationEdge! +} + +type UpdateWebhookConfigurationPayload { + webhookConfiguration: WebhookConfiguration! +} + +type DeleteWebhookConfigurationPayload { + deletedWebhookConfigurationId: ID! +} + type CreateStateOfApplicabilityPayload { stateOfApplicabilityEdge: StateOfApplicabilityEdge! } @@ -16702,6 +17017,17 @@ func (ec *executionContext) field_Mutation_createVendor_args(ctx context.Context return args, nil } +func (ec *executionContext) field_Mutation_createWebhookConfiguration_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNCreateWebhookConfigurationInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateWebhookConfigurationInput) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_deleteApplicabilityStatement_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -17164,6 +17490,17 @@ func (ec *executionContext) field_Mutation_deleteVendor_args(ctx context.Context return args, nil } +func (ec *executionContext) field_Mutation_deleteWebhookConfiguration_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNDeleteWebhookConfigurationInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteWebhookConfigurationInput) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_exportDataProtectionImpactAssessmentsPDF_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -17681,6 +18018,17 @@ func (ec *executionContext) field_Mutation_updateVendor_args(ctx context.Context return args, nil } +func (ec *executionContext) field_Mutation_updateWebhookConfiguration_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNUpdateWebhookConfigurationInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateWebhookConfigurationInput) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_uploadAuditReport_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -18563,6 +18911,37 @@ func (ec *executionContext) field_Organization_vendors_args(ctx context.Context, return args, nil } +func (ec *executionContext) field_Organization_webhookConfigurations_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "first", ec.unmarshalOInt2ᚖint) + if err != nil { + return nil, err + } + args["first"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "after", ec.unmarshalOCursorKey2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋpageᚐCursorKey) + if err != nil { + return nil, err + } + args["after"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "last", ec.unmarshalOInt2ᚖint) + if err != nil { + return nil, err + } + args["last"] = arg2 + arg3, err := graphql.ProcessArgField(ctx, rawArgs, "before", ec.unmarshalOCursorKey2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋpageᚐCursorKey) + if err != nil { + return nil, err + } + args["before"] = arg3 + arg4, err := graphql.ProcessArgField(ctx, rawArgs, "orderBy", ec.unmarshalOWebhookConfigurationOrder2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐWebhookConfigurationOrderBy) + if err != nil { + return nil, err + } + args["orderBy"] = arg4 + return args, nil +} + func (ec *executionContext) field_ProcessingActivity_permission_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -19378,6 +19757,17 @@ func (ec *executionContext) field_Viewer_signableDocuments_args(ctx context.Cont return args, nil } +func (ec *executionContext) field_WebhookConfiguration_permission_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "action", ec.unmarshalNString2string) + if err != nil { + return nil, err + } + args["action"] = arg0 + return args, nil +} + func (ec *executionContext) field___Directive_args_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -20380,6 +20770,8 @@ func (ec *executionContext) fieldContext_Asset_organization(_ context.Context, f return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -20829,6 +21221,8 @@ func (ec *executionContext) fieldContext_Audit_organization(_ context.Context, f return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -21799,6 +22193,8 @@ func (ec *executionContext) fieldContext_ContinualImprovement_organization(_ con return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -22450,6 +22846,8 @@ func (ec *executionContext) fieldContext_Control_organization(_ context.Context, return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -24982,6 +25380,41 @@ func (ec *executionContext) fieldContext_CreateVendorServicePayload_vendorServic return fc, nil } +func (ec *executionContext) _CreateWebhookConfigurationPayload_webhookConfigurationEdge(ctx context.Context, field graphql.CollectedField, obj *types.CreateWebhookConfigurationPayload) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_CreateWebhookConfigurationPayload_webhookConfigurationEdge, + func(ctx context.Context) (any, error) { + return obj.WebhookConfigurationEdge, nil + }, + nil, + ec.marshalNWebhookConfigurationEdge2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐWebhookConfigurationEdge, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_CreateWebhookConfigurationPayload_webhookConfigurationEdge(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CreateWebhookConfigurationPayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "cursor": + return ec.fieldContext_WebhookConfigurationEdge_cursor(ctx, field) + case "node": + return ec.fieldContext_WebhookConfigurationEdge_node(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type WebhookConfigurationEdge", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _CustomDomain_id(ctx context.Context, field graphql.CollectedField, obj *types.CustomDomain) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -25103,6 +25536,8 @@ func (ec *executionContext) fieldContext_CustomDomain_organization(_ context.Con return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -25698,6 +26133,8 @@ func (ec *executionContext) fieldContext_DataProtectionImpactAssessment_organiza return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -26450,6 +26887,8 @@ func (ec *executionContext) fieldContext_Datum_organization(_ context.Context, f return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -28251,6 +28690,35 @@ func (ec *executionContext) fieldContext_DeleteVendorServicePayload_deletedVendo return fc, nil } +func (ec *executionContext) _DeleteWebhookConfigurationPayload_deletedWebhookConfigurationId(ctx context.Context, field graphql.CollectedField, obj *types.DeleteWebhookConfigurationPayload) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_DeleteWebhookConfigurationPayload_deletedWebhookConfigurationId, + func(ctx context.Context) (any, error) { + return obj.DeletedWebhookConfigurationID, nil + }, + nil, + ec.marshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_DeleteWebhookConfigurationPayload_deletedWebhookConfigurationId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DeleteWebhookConfigurationPayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Document_id(ctx context.Context, field graphql.CollectedField, obj *types.Document) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -28599,6 +29067,8 @@ func (ec *executionContext) fieldContext_Document_organization(_ context.Context return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -31426,6 +31896,8 @@ func (ec *executionContext) fieldContext_Framework_organization(_ context.Contex return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -32893,6 +33365,8 @@ func (ec *executionContext) fieldContext_Meeting_organization(_ context.Context, return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -36730,6 +37204,141 @@ func (ec *executionContext) fieldContext_Mutation_deleteMeeting(ctx context.Cont return fc, nil } +func (ec *executionContext) _Mutation_createWebhookConfiguration(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Mutation_createWebhookConfiguration, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.Mutation().CreateWebhookConfiguration(ctx, fc.Args["input"].(types.CreateWebhookConfigurationInput)) + }, + nil, + ec.marshalNCreateWebhookConfigurationPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateWebhookConfigurationPayload, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Mutation_createWebhookConfiguration(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "webhookConfigurationEdge": + return ec.fieldContext_CreateWebhookConfigurationPayload_webhookConfigurationEdge(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type CreateWebhookConfigurationPayload", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_createWebhookConfiguration_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_updateWebhookConfiguration(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Mutation_updateWebhookConfiguration, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.Mutation().UpdateWebhookConfiguration(ctx, fc.Args["input"].(types.UpdateWebhookConfigurationInput)) + }, + nil, + ec.marshalNUpdateWebhookConfigurationPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateWebhookConfigurationPayload, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Mutation_updateWebhookConfiguration(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "webhookConfiguration": + return ec.fieldContext_UpdateWebhookConfigurationPayload_webhookConfiguration(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type UpdateWebhookConfigurationPayload", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_updateWebhookConfiguration_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_deleteWebhookConfiguration(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Mutation_deleteWebhookConfiguration, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.Mutation().DeleteWebhookConfiguration(ctx, fc.Args["input"].(types.DeleteWebhookConfigurationInput)) + }, + nil, + ec.marshalNDeleteWebhookConfigurationPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteWebhookConfigurationPayload, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Mutation_deleteWebhookConfiguration(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "deletedWebhookConfigurationId": + return ec.fieldContext_DeleteWebhookConfigurationPayload_deletedWebhookConfigurationId(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DeleteWebhookConfigurationPayload", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_deleteWebhookConfiguration_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_createStateOfApplicability(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -39584,6 +40193,8 @@ func (ec *executionContext) fieldContext_Nonconformity_organization(_ context.Co return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -40414,6 +41025,8 @@ func (ec *executionContext) fieldContext_Obligation_organization(_ context.Conte return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -42533,6 +43146,55 @@ func (ec *executionContext) fieldContext_Organization_customDomain(_ context.Con return fc, nil } +func (ec *executionContext) _Organization_webhookConfigurations(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Organization_webhookConfigurations, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.Organization().WebhookConfigurations(ctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.WebhookConfigurationOrderBy)) + }, + nil, + ec.marshalNWebhookConfigurationConnection2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐWebhookConfigurationConnection, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Organization_webhookConfigurations(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Organization", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "edges": + return ec.fieldContext_WebhookConfigurationConnection_edges(ctx, field) + case "pageInfo": + return ec.fieldContext_WebhookConfigurationConnection_pageInfo(ctx, field) + case "totalCount": + return ec.fieldContext_WebhookConfigurationConnection_totalCount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type WebhookConfigurationConnection", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Organization_webhookConfigurations_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Organization_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -42985,6 +43647,8 @@ func (ec *executionContext) fieldContext_ProcessingActivity_organization(_ conte return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -45358,6 +46022,8 @@ func (ec *executionContext) fieldContext_RightsRequest_organization(_ context.Co return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -46382,6 +47048,8 @@ func (ec *executionContext) fieldContext_Risk_organization(_ context.Context, fi return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -47818,6 +48486,8 @@ func (ec *executionContext) fieldContext_Snapshot_organization(_ context.Context return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -48424,6 +49094,8 @@ func (ec *executionContext) fieldContext_StateOfApplicability_organization(_ con return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -49140,6 +49812,8 @@ func (ec *executionContext) fieldContext_Task_organization(_ context.Context, fi return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -49755,6 +50429,8 @@ func (ec *executionContext) fieldContext_TransferImpactAssessment_organization(_ return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -50521,6 +51197,8 @@ func (ec *executionContext) fieldContext_TrustCenter_organization(_ context.Cont return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -52052,6 +52730,8 @@ func (ec *executionContext) fieldContext_TrustCenterFile_organization(_ context. return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -54508,6 +55188,53 @@ func (ec *executionContext) fieldContext_UpdateVendorServicePayload_vendorServic return fc, nil } +func (ec *executionContext) _UpdateWebhookConfigurationPayload_webhookConfiguration(ctx context.Context, field graphql.CollectedField, obj *types.UpdateWebhookConfigurationPayload) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_UpdateWebhookConfigurationPayload_webhookConfiguration, + func(ctx context.Context) (any, error) { + return obj.WebhookConfiguration, nil + }, + nil, + ec.marshalNWebhookConfiguration2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐWebhookConfiguration, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_UpdateWebhookConfigurationPayload_webhookConfiguration(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "UpdateWebhookConfigurationPayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_WebhookConfiguration_id(ctx, field) + case "organization": + return ec.fieldContext_WebhookConfiguration_organization(ctx, field) + case "endpointUrl": + return ec.fieldContext_WebhookConfiguration_endpointUrl(ctx, field) + case "signingSecret": + return ec.fieldContext_WebhookConfiguration_signingSecret(ctx, field) + case "selectedEvents": + return ec.fieldContext_WebhookConfiguration_selectedEvents(ctx, field) + case "createdAt": + return ec.fieldContext_WebhookConfiguration_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_WebhookConfiguration_updatedAt(ctx, field) + case "permission": + return ec.fieldContext_WebhookConfiguration_permission(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type WebhookConfiguration", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _UploadAuditReportPayload_audit(ctx context.Context, field graphql.CollectedField, obj *types.UploadAuditReportPayload) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -55031,6 +55758,8 @@ func (ec *executionContext) fieldContext_Vendor_organization(_ context.Context, return ec.fieldContext_Organization_trustCenter(ctx, field) case "customDomain": return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) case "createdAt": return ec.fieldContext_Organization_createdAt(ctx, field) case "updatedAt": @@ -58992,6 +59721,507 @@ func (ec *executionContext) fieldContext_Viewer_signableDocument(ctx context.Con return fc, nil } +func (ec *executionContext) _WebhookConfiguration_id(ctx context.Context, field graphql.CollectedField, obj *types.WebhookConfiguration) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_WebhookConfiguration_id, + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + ec.marshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_WebhookConfiguration_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "WebhookConfiguration", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _WebhookConfiguration_organization(ctx context.Context, field graphql.CollectedField, obj *types.WebhookConfiguration) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_WebhookConfiguration_organization, + func(ctx context.Context) (any, error) { + return ec.resolvers.WebhookConfiguration().Organization(ctx, obj) + }, + nil, + ec.marshalOOrganization2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐOrganization, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_WebhookConfiguration_organization(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "WebhookConfiguration", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Organization_id(ctx, field) + case "name": + return ec.fieldContext_Organization_name(ctx, field) + case "logoUrl": + return ec.fieldContext_Organization_logoUrl(ctx, field) + case "horizontalLogoUrl": + return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) + case "description": + return ec.fieldContext_Organization_description(ctx, field) + case "websiteUrl": + return ec.fieldContext_Organization_websiteUrl(ctx, field) + case "email": + return ec.fieldContext_Organization_email(ctx, field) + case "headquarterAddress": + return ec.fieldContext_Organization_headquarterAddress(ctx, field) + case "context": + return ec.fieldContext_Organization_context(ctx, field) + case "profiles": + return ec.fieldContext_Organization_profiles(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) + case "frameworks": + return ec.fieldContext_Organization_frameworks(ctx, field) + case "controls": + return ec.fieldContext_Organization_controls(ctx, field) + case "vendors": + return ec.fieldContext_Organization_vendors(ctx, field) + case "documents": + return ec.fieldContext_Organization_documents(ctx, field) + case "meetings": + return ec.fieldContext_Organization_meetings(ctx, field) + case "statesOfApplicability": + return ec.fieldContext_Organization_statesOfApplicability(ctx, field) + case "measures": + return ec.fieldContext_Organization_measures(ctx, field) + case "risks": + return ec.fieldContext_Organization_risks(ctx, field) + case "tasks": + return ec.fieldContext_Organization_tasks(ctx, field) + case "assets": + return ec.fieldContext_Organization_assets(ctx, field) + case "data": + return ec.fieldContext_Organization_data(ctx, field) + case "audits": + return ec.fieldContext_Organization_audits(ctx, field) + case "nonconformities": + return ec.fieldContext_Organization_nonconformities(ctx, field) + case "obligations": + return ec.fieldContext_Organization_obligations(ctx, field) + case "continualImprovements": + return ec.fieldContext_Organization_continualImprovements(ctx, field) + case "rightsRequests": + return ec.fieldContext_Organization_rightsRequests(ctx, field) + case "processingActivities": + return ec.fieldContext_Organization_processingActivities(ctx, field) + case "dataProtectionImpactAssessments": + return ec.fieldContext_Organization_dataProtectionImpactAssessments(ctx, field) + case "transferImpactAssessments": + return ec.fieldContext_Organization_transferImpactAssessments(ctx, field) + case "snapshots": + return ec.fieldContext_Organization_snapshots(ctx, field) + case "trustCenterFiles": + return ec.fieldContext_Organization_trustCenterFiles(ctx, field) + case "trustCenter": + return ec.fieldContext_Organization_trustCenter(ctx, field) + case "customDomain": + return ec.fieldContext_Organization_customDomain(ctx, field) + case "webhookConfigurations": + return ec.fieldContext_Organization_webhookConfigurations(ctx, field) + case "createdAt": + return ec.fieldContext_Organization_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_Organization_updatedAt(ctx, field) + case "permission": + return ec.fieldContext_Organization_permission(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _WebhookConfiguration_endpointUrl(ctx context.Context, field graphql.CollectedField, obj *types.WebhookConfiguration) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_WebhookConfiguration_endpointUrl, + func(ctx context.Context) (any, error) { + return obj.EndpointURL, nil + }, + nil, + ec.marshalNString2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_WebhookConfiguration_endpointUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "WebhookConfiguration", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _WebhookConfiguration_signingSecret(ctx context.Context, field graphql.CollectedField, obj *types.WebhookConfiguration) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_WebhookConfiguration_signingSecret, + func(ctx context.Context) (any, error) { + return ec.resolvers.WebhookConfiguration().SigningSecret(ctx, obj) + }, + nil, + ec.marshalNString2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_WebhookConfiguration_signingSecret(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "WebhookConfiguration", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _WebhookConfiguration_selectedEvents(ctx context.Context, field graphql.CollectedField, obj *types.WebhookConfiguration) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_WebhookConfiguration_selectedEvents, + func(ctx context.Context) (any, error) { + return obj.SelectedEvents, nil + }, + nil, + ec.marshalNWebhookEventType2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookEventTypeᚄ, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_WebhookConfiguration_selectedEvents(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "WebhookConfiguration", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type WebhookEventType does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _WebhookConfiguration_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.WebhookConfiguration) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_WebhookConfiguration_createdAt, + func(ctx context.Context) (any, error) { + return obj.CreatedAt, nil + }, + nil, + ec.marshalNDatetime2timeᚐTime, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_WebhookConfiguration_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "WebhookConfiguration", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Datetime does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _WebhookConfiguration_updatedAt(ctx context.Context, field graphql.CollectedField, obj *types.WebhookConfiguration) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_WebhookConfiguration_updatedAt, + func(ctx context.Context) (any, error) { + return obj.UpdatedAt, nil + }, + nil, + ec.marshalNDatetime2timeᚐTime, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_WebhookConfiguration_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "WebhookConfiguration", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Datetime does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _WebhookConfiguration_permission(ctx context.Context, field graphql.CollectedField, obj *types.WebhookConfiguration) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_WebhookConfiguration_permission, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.WebhookConfiguration().Permission(ctx, obj, fc.Args["action"].(string)) + }, + nil, + ec.marshalNBoolean2bool, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_WebhookConfiguration_permission(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "WebhookConfiguration", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_WebhookConfiguration_permission_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _WebhookConfigurationConnection_edges(ctx context.Context, field graphql.CollectedField, obj *types.WebhookConfigurationConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_WebhookConfigurationConnection_edges, + func(ctx context.Context) (any, error) { + return obj.Edges, nil + }, + nil, + ec.marshalNWebhookConfigurationEdge2ᚕᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐWebhookConfigurationEdgeᚄ, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_WebhookConfigurationConnection_edges(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "WebhookConfigurationConnection", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "cursor": + return ec.fieldContext_WebhookConfigurationEdge_cursor(ctx, field) + case "node": + return ec.fieldContext_WebhookConfigurationEdge_node(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type WebhookConfigurationEdge", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _WebhookConfigurationConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *types.WebhookConfigurationConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_WebhookConfigurationConnection_pageInfo, + func(ctx context.Context) (any, error) { + return obj.PageInfo, nil + }, + nil, + ec.marshalNPageInfo2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐPageInfo, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_WebhookConfigurationConnection_pageInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "WebhookConfigurationConnection", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "hasNextPage": + return ec.fieldContext_PageInfo_hasNextPage(ctx, field) + case "hasPreviousPage": + return ec.fieldContext_PageInfo_hasPreviousPage(ctx, field) + case "startCursor": + return ec.fieldContext_PageInfo_startCursor(ctx, field) + case "endCursor": + return ec.fieldContext_PageInfo_endCursor(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type PageInfo", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _WebhookConfigurationConnection_totalCount(ctx context.Context, field graphql.CollectedField, obj *types.WebhookConfigurationConnection) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_WebhookConfigurationConnection_totalCount, + func(ctx context.Context) (any, error) { + return ec.resolvers.WebhookConfigurationConnection().TotalCount(ctx, obj) + }, + nil, + ec.marshalNInt2int, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_WebhookConfigurationConnection_totalCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "WebhookConfigurationConnection", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _WebhookConfigurationEdge_cursor(ctx context.Context, field graphql.CollectedField, obj *types.WebhookConfigurationEdge) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_WebhookConfigurationEdge_cursor, + func(ctx context.Context) (any, error) { + return obj.Cursor, nil + }, + nil, + ec.marshalNCursorKey2goᚗproboᚗincᚋproboᚋpkgᚋpageᚐCursorKey, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_WebhookConfigurationEdge_cursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "WebhookConfigurationEdge", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type CursorKey does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _WebhookConfigurationEdge_node(ctx context.Context, field graphql.CollectedField, obj *types.WebhookConfigurationEdge) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_WebhookConfigurationEdge_node, + func(ctx context.Context) (any, error) { + return obj.Node, nil + }, + nil, + ec.marshalNWebhookConfiguration2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐWebhookConfiguration, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_WebhookConfigurationEdge_node(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "WebhookConfigurationEdge", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_WebhookConfiguration_id(ctx, field) + case "organization": + return ec.fieldContext_WebhookConfiguration_organization(ctx, field) + case "endpointUrl": + return ec.fieldContext_WebhookConfiguration_endpointUrl(ctx, field) + case "signingSecret": + return ec.fieldContext_WebhookConfiguration_signingSecret(ctx, field) + case "selectedEvents": + return ec.fieldContext_WebhookConfiguration_selectedEvents(ctx, field) + case "createdAt": + return ec.fieldContext_WebhookConfiguration_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_WebhookConfiguration_updatedAt(ctx, field) + case "permission": + return ec.fieldContext_WebhookConfiguration_permission(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type WebhookConfiguration", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -63158,6 +64388,47 @@ func (ec *executionContext) unmarshalInputCreateVendorServiceInput(ctx context.C return it, nil } +func (ec *executionContext) unmarshalInputCreateWebhookConfigurationInput(ctx context.Context, obj any) (types.CreateWebhookConfigurationInput, error) { + var it types.CreateWebhookConfigurationInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"organizationId", "endpointUrl", "selectedEvents"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "organizationId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("organizationId")) + data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.OrganizationID = data + case "endpointUrl": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("endpointUrl")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.EndpointURL = data + case "selectedEvents": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("selectedEvents")) + data, err := ec.unmarshalNWebhookEventType2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookEventTypeᚄ(ctx, v) + if err != nil { + return it, err + } + it.SelectedEvents = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputDataProtectionImpactAssessmentFilter(ctx context.Context, obj any) (types.DataProtectionImpactAssessmentFilter, error) { var it types.DataProtectionImpactAssessmentFilter asMap := map[string]any{} @@ -64470,6 +65741,33 @@ func (ec *executionContext) unmarshalInputDeleteVendorServiceInput(ctx context.C return it, nil } +func (ec *executionContext) unmarshalInputDeleteWebhookConfigurationInput(ctx context.Context, obj any) (types.DeleteWebhookConfigurationInput, error) { + var it types.DeleteWebhookConfigurationInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"webhookConfigurationId"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "webhookConfigurationId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("webhookConfigurationId")) + data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.WebhookConfigurationID = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputDocumentFilter(ctx context.Context, obj any) (types.DocumentFilter, error) { var it types.DocumentFilter asMap := map[string]any{} @@ -68079,6 +69377,47 @@ func (ec *executionContext) unmarshalInputUpdateVendorServiceInput(ctx context.C return it, nil } +func (ec *executionContext) unmarshalInputUpdateWebhookConfigurationInput(ctx context.Context, obj any) (types.UpdateWebhookConfigurationInput, error) { + var it types.UpdateWebhookConfigurationInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"id", "endpointUrl", "selectedEvents"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "id": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.ID = data + case "endpointUrl": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("endpointUrl")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.EndpointURL = data + case "selectedEvents": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("selectedEvents")) + data, err := ec.unmarshalOWebhookEventType2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookEventTypeᚄ(ctx, v) + if err != nil { + return it, err + } + it.SelectedEvents = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputUploadAuditReportInput(ctx context.Context, obj any) (types.UploadAuditReportInput, error) { var it types.UploadAuditReportInput asMap := map[string]any{} @@ -68550,6 +69889,40 @@ func (ec *executionContext) unmarshalInputVendorServiceOrder(ctx context.Context return it, nil } +func (ec *executionContext) unmarshalInputWebhookConfigurationOrder(ctx context.Context, obj any) (types.WebhookConfigurationOrderBy, error) { + var it types.WebhookConfigurationOrderBy + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"direction", "field"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "direction": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("direction")) + data, err := ec.unmarshalNOrderDirection2goᚗproboᚗincᚋproboᚋpkgᚋpageᚐOrderDirection(ctx, v) + if err != nil { + return it, err + } + it.Direction = data + case "field": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("field")) + data, err := ec.unmarshalNWebhookConfigurationOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookConfigurationOrderField(ctx, v) + if err != nil { + return it, err + } + it.Field = data + } + } + + return it, nil +} + // endregion **************************** input.gotpl ***************************** // region ************************** interface.gotpl *************************** @@ -68558,6 +69931,13 @@ func (ec *executionContext) _Node(ctx context.Context, sel ast.SelectionSet, obj switch obj := (obj).(type) { case nil: return graphql.Null + case types.WebhookConfiguration: + return ec._WebhookConfiguration(ctx, sel, &obj) + case *types.WebhookConfiguration: + if obj == nil { + return graphql.Null + } + return ec._WebhookConfiguration(ctx, sel, obj) case types.VendorService: return ec._VendorService(ctx, sel, &obj) case *types.VendorService: @@ -72489,6 +73869,45 @@ func (ec *executionContext) _CreateVendorServicePayload(ctx context.Context, sel return out } +var createWebhookConfigurationPayloadImplementors = []string{"CreateWebhookConfigurationPayload"} + +func (ec *executionContext) _CreateWebhookConfigurationPayload(ctx context.Context, sel ast.SelectionSet, obj *types.CreateWebhookConfigurationPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, createWebhookConfigurationPayloadImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("CreateWebhookConfigurationPayload") + case "webhookConfigurationEdge": + out.Values[i] = ec._CreateWebhookConfigurationPayload_webhookConfigurationEdge(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var customDomainImplementors = []string{"CustomDomain", "Node"} func (ec *executionContext) _CustomDomain(ctx context.Context, sel ast.SelectionSet, obj *types.CustomDomain) graphql.Marshaler { @@ -74953,6 +76372,45 @@ func (ec *executionContext) _DeleteVendorServicePayload(ctx context.Context, sel return out } +var deleteWebhookConfigurationPayloadImplementors = []string{"DeleteWebhookConfigurationPayload"} + +func (ec *executionContext) _DeleteWebhookConfigurationPayload(ctx context.Context, sel ast.SelectionSet, obj *types.DeleteWebhookConfigurationPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, deleteWebhookConfigurationPayloadImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DeleteWebhookConfigurationPayload") + case "deletedWebhookConfigurationId": + out.Values[i] = ec._DeleteWebhookConfigurationPayload_deletedWebhookConfigurationId(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var documentImplementors = []string{"Document", "Node"} func (ec *executionContext) _Document(ctx context.Context, sel ast.SelectionSet, obj *types.Document) graphql.Marshaler { @@ -78488,6 +79946,27 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "createWebhookConfiguration": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_createWebhookConfiguration(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "updateWebhookConfiguration": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_updateWebhookConfiguration(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deleteWebhookConfiguration": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_deleteWebhookConfiguration(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "createStateOfApplicability": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_createStateOfApplicability(ctx, field) @@ -80602,6 +82081,42 @@ func (ec *executionContext) _Organization(ctx context.Context, sel ast.Selection continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "webhookConfigurations": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Organization_webhookConfigurations(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "createdAt": out.Values[i] = ec._Organization_createdAt(ctx, field, obj) @@ -86960,6 +88475,45 @@ func (ec *executionContext) _UpdateVendorServicePayload(ctx context.Context, sel return out } +var updateWebhookConfigurationPayloadImplementors = []string{"UpdateWebhookConfigurationPayload"} + +func (ec *executionContext) _UpdateWebhookConfigurationPayload(ctx context.Context, sel ast.SelectionSet, obj *types.UpdateWebhookConfigurationPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, updateWebhookConfigurationPayloadImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("UpdateWebhookConfigurationPayload") + case "webhookConfiguration": + out.Values[i] = ec._UpdateWebhookConfigurationPayload_webhookConfiguration(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var uploadAuditReportPayloadImplementors = []string{"UploadAuditReportPayload"} func (ec *executionContext) _UploadAuditReportPayload(ctx context.Context, sel ast.SelectionSet, obj *types.UploadAuditReportPayload) graphql.Marshaler { @@ -89131,6 +90685,294 @@ func (ec *executionContext) _Viewer(ctx context.Context, sel ast.SelectionSet, o return out } +var webhookConfigurationImplementors = []string{"WebhookConfiguration", "Node"} + +func (ec *executionContext) _WebhookConfiguration(ctx context.Context, sel ast.SelectionSet, obj *types.WebhookConfiguration) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, webhookConfigurationImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("WebhookConfiguration") + case "id": + out.Values[i] = ec._WebhookConfiguration_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "organization": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._WebhookConfiguration_organization(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "endpointUrl": + out.Values[i] = ec._WebhookConfiguration_endpointUrl(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "signingSecret": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._WebhookConfiguration_signingSecret(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "selectedEvents": + out.Values[i] = ec._WebhookConfiguration_selectedEvents(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "createdAt": + out.Values[i] = ec._WebhookConfiguration_createdAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "updatedAt": + out.Values[i] = ec._WebhookConfiguration_updatedAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "permission": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._WebhookConfiguration_permission(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var webhookConfigurationConnectionImplementors = []string{"WebhookConfigurationConnection"} + +func (ec *executionContext) _WebhookConfigurationConnection(ctx context.Context, sel ast.SelectionSet, obj *types.WebhookConfigurationConnection) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, webhookConfigurationConnectionImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("WebhookConfigurationConnection") + case "edges": + out.Values[i] = ec._WebhookConfigurationConnection_edges(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "pageInfo": + out.Values[i] = ec._WebhookConfigurationConnection_pageInfo(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "totalCount": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._WebhookConfigurationConnection_totalCount(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var webhookConfigurationEdgeImplementors = []string{"WebhookConfigurationEdge"} + +func (ec *executionContext) _WebhookConfigurationEdge(ctx context.Context, sel ast.SelectionSet, obj *types.WebhookConfigurationEdge) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, webhookConfigurationEdgeImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("WebhookConfigurationEdge") + case "cursor": + out.Values[i] = ec._WebhookConfigurationEdge_cursor(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "node": + out.Values[i] = ec._WebhookConfigurationEdge_node(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var __DirectiveImplementors = []string{"__Directive"} func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler { @@ -91620,6 +93462,25 @@ func (ec *executionContext) marshalNCreateVendorServicePayload2ᚖgoᚗproboᚗi return ec._CreateVendorServicePayload(ctx, sel, v) } +func (ec *executionContext) unmarshalNCreateWebhookConfigurationInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateWebhookConfigurationInput(ctx context.Context, v any) (types.CreateWebhookConfigurationInput, error) { + res, err := ec.unmarshalInputCreateWebhookConfigurationInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNCreateWebhookConfigurationPayload2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateWebhookConfigurationPayload(ctx context.Context, sel ast.SelectionSet, v types.CreateWebhookConfigurationPayload) graphql.Marshaler { + return ec._CreateWebhookConfigurationPayload(ctx, sel, &v) +} + +func (ec *executionContext) marshalNCreateWebhookConfigurationPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateWebhookConfigurationPayload(ctx context.Context, sel ast.SelectionSet, v *types.CreateWebhookConfigurationPayload) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._CreateWebhookConfigurationPayload(ctx, sel, v) +} + func (ec *executionContext) unmarshalNCursorKey2goᚗproboᚗincᚋproboᚋpkgᚋpageᚐCursorKey(ctx context.Context, v any) (page.CursorKey, error) { res, err := cursor.UnmarshalCursorKeyScalar(v) return res, graphql.ErrorOnPath(ctx, err) @@ -92792,6 +94653,25 @@ func (ec *executionContext) marshalNDeleteVendorServicePayload2ᚖgoᚗproboᚗi return ec._DeleteVendorServicePayload(ctx, sel, v) } +func (ec *executionContext) unmarshalNDeleteWebhookConfigurationInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteWebhookConfigurationInput(ctx context.Context, v any) (types.DeleteWebhookConfigurationInput, error) { + res, err := ec.unmarshalInputDeleteWebhookConfigurationInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNDeleteWebhookConfigurationPayload2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteWebhookConfigurationPayload(ctx context.Context, sel ast.SelectionSet, v types.DeleteWebhookConfigurationPayload) graphql.Marshaler { + return ec._DeleteWebhookConfigurationPayload(ctx, sel, &v) +} + +func (ec *executionContext) marshalNDeleteWebhookConfigurationPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteWebhookConfigurationPayload(ctx context.Context, sel ast.SelectionSet, v *types.DeleteWebhookConfigurationPayload) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DeleteWebhookConfigurationPayload(ctx, sel, v) +} + func (ec *executionContext) marshalNDocument2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDocument(ctx context.Context, sel ast.SelectionSet, v types.Document) graphql.Marshaler { return ec._Document(ctx, sel, &v) } @@ -97210,6 +99090,25 @@ func (ec *executionContext) marshalNUpdateVendorServicePayload2ᚖgoᚗproboᚗi return ec._UpdateVendorServicePayload(ctx, sel, v) } +func (ec *executionContext) unmarshalNUpdateWebhookConfigurationInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateWebhookConfigurationInput(ctx context.Context, v any) (types.UpdateWebhookConfigurationInput, error) { + res, err := ec.unmarshalInputUpdateWebhookConfigurationInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNUpdateWebhookConfigurationPayload2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateWebhookConfigurationPayload(ctx context.Context, sel ast.SelectionSet, v types.UpdateWebhookConfigurationPayload) graphql.Marshaler { + return ec._UpdateWebhookConfigurationPayload(ctx, sel, &v) +} + +func (ec *executionContext) marshalNUpdateWebhookConfigurationPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateWebhookConfigurationPayload(ctx context.Context, sel ast.SelectionSet, v *types.UpdateWebhookConfigurationPayload) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._UpdateWebhookConfigurationPayload(ctx, sel, v) +} + func (ec *executionContext) unmarshalNUpload2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx context.Context, v any) (graphql.Upload, error) { res, err := graphql.UnmarshalUpload(v) return res, graphql.ErrorOnPath(ctx, err) @@ -97980,6 +99879,205 @@ func (ec *executionContext) marshalNViewer2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋs return ec._Viewer(ctx, sel, v) } +func (ec *executionContext) marshalNWebhookConfiguration2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐWebhookConfiguration(ctx context.Context, sel ast.SelectionSet, v *types.WebhookConfiguration) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._WebhookConfiguration(ctx, sel, v) +} + +func (ec *executionContext) marshalNWebhookConfigurationConnection2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐWebhookConfigurationConnection(ctx context.Context, sel ast.SelectionSet, v types.WebhookConfigurationConnection) graphql.Marshaler { + return ec._WebhookConfigurationConnection(ctx, sel, &v) +} + +func (ec *executionContext) marshalNWebhookConfigurationConnection2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐWebhookConfigurationConnection(ctx context.Context, sel ast.SelectionSet, v *types.WebhookConfigurationConnection) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._WebhookConfigurationConnection(ctx, sel, v) +} + +func (ec *executionContext) marshalNWebhookConfigurationEdge2ᚕᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐWebhookConfigurationEdgeᚄ(ctx context.Context, sel ast.SelectionSet, v []*types.WebhookConfigurationEdge) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNWebhookConfigurationEdge2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐWebhookConfigurationEdge(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNWebhookConfigurationEdge2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐWebhookConfigurationEdge(ctx context.Context, sel ast.SelectionSet, v *types.WebhookConfigurationEdge) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._WebhookConfigurationEdge(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNWebhookConfigurationOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookConfigurationOrderField(ctx context.Context, v any) (coredata.WebhookConfigurationOrderField, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNWebhookConfigurationOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookConfigurationOrderField[tmp] + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNWebhookConfigurationOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookConfigurationOrderField(ctx context.Context, sel ast.SelectionSet, v coredata.WebhookConfigurationOrderField) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(marshalNWebhookConfigurationOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookConfigurationOrderField[v]) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +var ( + unmarshalNWebhookConfigurationOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookConfigurationOrderField = map[string]coredata.WebhookConfigurationOrderField{ + "CREATED_AT": coredata.WebhookConfigurationOrderFieldCreatedAt, + } + marshalNWebhookConfigurationOrderField2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookConfigurationOrderField = map[coredata.WebhookConfigurationOrderField]string{ + coredata.WebhookConfigurationOrderFieldCreatedAt: "CREATED_AT", + } +) + +func (ec *executionContext) unmarshalNWebhookEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookEventType(ctx context.Context, v any) (coredata.WebhookEventType, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNWebhookEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookEventType[tmp] + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNWebhookEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookEventType(ctx context.Context, sel ast.SelectionSet, v coredata.WebhookEventType) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(marshalNWebhookEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookEventType[v]) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +var ( + unmarshalNWebhookEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookEventType = map[string]coredata.WebhookEventType{ + "MEETING_CREATED": coredata.WebhookEventTypeMeetingCreated, + "MEETING_UPDATED": coredata.WebhookEventTypeMeetingUpdated, + "MEETING_DELETED": coredata.WebhookEventTypeMeetingDeleted, + "VENDOR_CREATED": coredata.WebhookEventTypeVendorCreated, + "VENDOR_UPDATED": coredata.WebhookEventTypeVendorUpdated, + "VENDOR_DELETED": coredata.WebhookEventTypeVendorDeleted, + } + marshalNWebhookEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookEventType = map[coredata.WebhookEventType]string{ + coredata.WebhookEventTypeMeetingCreated: "MEETING_CREATED", + coredata.WebhookEventTypeMeetingUpdated: "MEETING_UPDATED", + coredata.WebhookEventTypeMeetingDeleted: "MEETING_DELETED", + coredata.WebhookEventTypeVendorCreated: "VENDOR_CREATED", + coredata.WebhookEventTypeVendorUpdated: "VENDOR_UPDATED", + coredata.WebhookEventTypeVendorDeleted: "VENDOR_DELETED", + } +) + +func (ec *executionContext) unmarshalNWebhookEventType2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookEventTypeᚄ(ctx context.Context, v any) ([]coredata.WebhookEventType, error) { + var vSlice []any + vSlice = graphql.CoerceList(v) + var err error + res := make([]coredata.WebhookEventType, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNWebhookEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookEventType(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalNWebhookEventType2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookEventTypeᚄ(ctx context.Context, sel ast.SelectionSet, v []coredata.WebhookEventType) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNWebhookEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookEventType(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler { return ec.___Directive(ctx, sel, &v) } @@ -100056,6 +102154,79 @@ func (ec *executionContext) unmarshalOVendorServiceOrder2ᚖgoᚗproboᚗincᚋp return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalOWebhookConfigurationOrder2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐWebhookConfigurationOrderBy(ctx context.Context, v any) (*types.WebhookConfigurationOrderBy, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputWebhookConfigurationOrder(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) unmarshalOWebhookEventType2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookEventTypeᚄ(ctx context.Context, v any) ([]coredata.WebhookEventType, error) { + if v == nil { + return nil, nil + } + var vSlice []any + vSlice = graphql.CoerceList(v) + var err error + res := make([]coredata.WebhookEventType, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNWebhookEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookEventType(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalOWebhookEventType2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookEventTypeᚄ(ctx context.Context, sel ast.SelectionSet, v []coredata.WebhookEventType) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNWebhookEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐWebhookEventType(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index 17f5e7143..ebb053c4b 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -693,6 +693,16 @@ type CreateVendorServicePayload struct { VendorServiceEdge *VendorServiceEdge `json:"vendorServiceEdge"` } +type CreateWebhookConfigurationInput struct { + OrganizationID gid.GID `json:"organizationId"` + EndpointURL string `json:"endpointUrl"` + SelectedEvents []coredata.WebhookEventType `json:"selectedEvents"` +} + +type CreateWebhookConfigurationPayload struct { + WebhookConfigurationEdge *WebhookConfigurationEdge `json:"webhookConfigurationEdge"` +} + type CustomDomain struct { ID gid.GID `json:"id"` Organization *Organization `json:"organization"` @@ -1103,6 +1113,14 @@ type DeleteVendorServicePayload struct { DeletedVendorServiceID gid.GID `json:"deletedVendorServiceId"` } +type DeleteWebhookConfigurationInput struct { + WebhookConfigurationID gid.GID `json:"webhookConfigurationId"` +} + +type DeleteWebhookConfigurationPayload struct { + DeletedWebhookConfigurationID gid.GID `json:"deletedWebhookConfigurationId"` +} + type Document struct { ID gid.GID `json:"id"` Title string `json:"title"` @@ -1499,6 +1517,7 @@ type Organization struct { TrustCenterFiles *TrustCenterFileConnection `json:"trustCenterFiles"` TrustCenter *TrustCenter `json:"trustCenter,omitempty"` CustomDomain *CustomDomain `json:"customDomain,omitempty"` + WebhookConfigurations *WebhookConfigurationConnection `json:"webhookConfigurations"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` Permission bool `json:"permission"` @@ -2342,6 +2361,16 @@ type UpdateVendorServicePayload struct { VendorService *VendorService `json:"vendorService"` } +type UpdateWebhookConfigurationInput struct { + ID gid.GID `json:"id"` + EndpointURL *string `json:"endpointUrl,omitempty"` + SelectedEvents []coredata.WebhookEventType `json:"selectedEvents,omitempty"` +} + +type UpdateWebhookConfigurationPayload struct { + WebhookConfiguration *WebhookConfiguration `json:"webhookConfiguration"` +} + type UploadAuditReportInput struct { AuditID gid.GID `json:"auditId"` File graphql.Upload `json:"file"` @@ -2593,3 +2622,22 @@ type Viewer struct { SignableDocuments *SignableDocumentConnection `json:"signableDocuments"` SignableDocument *SignableDocument `json:"signableDocument,omitempty"` } + +type WebhookConfiguration struct { + ID gid.GID `json:"id"` + Organization *Organization `json:"organization,omitempty"` + EndpointURL string `json:"endpointUrl"` + SigningSecret string `json:"signingSecret"` + SelectedEvents []coredata.WebhookEventType `json:"selectedEvents"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + Permission bool `json:"permission"` +} + +func (WebhookConfiguration) IsNode() {} +func (this WebhookConfiguration) GetID() gid.GID { return this.ID } + +type WebhookConfigurationEdge struct { + Cursor page.CursorKey `json:"cursor"` + Node *WebhookConfiguration `json:"node"` +} diff --git a/pkg/server/api/console/v1/types/webhook_configuration.go b/pkg/server/api/console/v1/types/webhook_configuration.go new file mode 100644 index 000000000..907a7fc07 --- /dev/null +++ b/pkg/server/api/console/v1/types/webhook_configuration.go @@ -0,0 +1,74 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package types + +import ( + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/page" +) + +type ( + WebhookConfigurationOrderBy OrderBy[coredata.WebhookConfigurationOrderField] + + WebhookConfigurationConnection struct { + TotalCount int + Edges []*WebhookConfigurationEdge + PageInfo PageInfo + + Resolver any + ParentID gid.GID + } +) + +func NewWebhookConfigurationConnection( + p *page.Page[*coredata.WebhookConfiguration, coredata.WebhookConfigurationOrderField], + parentType any, + parentID gid.GID, +) *WebhookConfigurationConnection { + var edges = make([]*WebhookConfigurationEdge, len(p.Data)) + + for i := range edges { + edges[i] = NewWebhookConfigurationEdge(p.Data[i], p.Cursor.OrderBy.Field) + } + + return &WebhookConfigurationConnection{ + Edges: edges, + PageInfo: *NewPageInfo(p), + + Resolver: parentType, + ParentID: parentID, + } +} + +func NewWebhookConfigurationEdge(wc *coredata.WebhookConfiguration, orderBy coredata.WebhookConfigurationOrderField) *WebhookConfigurationEdge { + return &WebhookConfigurationEdge{ + Cursor: wc.CursorKey(orderBy), + Node: NewWebhookConfiguration(wc), + } +} + +func NewWebhookConfiguration(wc *coredata.WebhookConfiguration) *WebhookConfiguration { + return &WebhookConfiguration{ + ID: wc.ID, + Organization: &Organization{ + ID: wc.OrganizationID, + }, + EndpointURL: wc.EndpointURL, + SelectedEvents: wc.SelectedEvents, + CreatedAt: wc.CreatedAt, + UpdatedAt: wc.UpdatedAt, + } +} diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 7b0745fc2..077696ac7 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -3678,6 +3678,77 @@ func (r *mutationResolver) DeleteMeeting(ctx context.Context, input types.Delete }, nil } +// CreateWebhookConfiguration is the resolver for the createWebhookConfiguration field. +func (r *mutationResolver) CreateWebhookConfiguration(ctx context.Context, input types.CreateWebhookConfigurationInput) (*types.CreateWebhookConfigurationPayload, error) { + if err := r.authorize(ctx, input.OrganizationID, probo.ActionWebhookConfigurationCreate); err != nil { + return nil, err + } + + prb := r.ProboService(ctx, input.OrganizationID.TenantID()) + + wc, err := prb.WebhookConfigurations.Create( + ctx, + probo.CreateWebhookConfigurationRequest{ + OrganizationID: input.OrganizationID, + EndpointURL: input.EndpointURL, + SelectedEvents: input.SelectedEvents, + }, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot create webhook configuration", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return &types.CreateWebhookConfigurationPayload{ + WebhookConfigurationEdge: types.NewWebhookConfigurationEdge(wc, coredata.WebhookConfigurationOrderFieldCreatedAt), + }, nil +} + +// UpdateWebhookConfiguration is the resolver for the updateWebhookConfiguration field. +func (r *mutationResolver) UpdateWebhookConfiguration(ctx context.Context, input types.UpdateWebhookConfigurationInput) (*types.UpdateWebhookConfigurationPayload, error) { + if err := r.authorize(ctx, input.ID, probo.ActionWebhookConfigurationUpdate); err != nil { + return nil, err + } + + prb := r.ProboService(ctx, input.ID.TenantID()) + + wc, err := prb.WebhookConfigurations.Update( + ctx, + probo.UpdateWebhookConfigurationRequest{ + WebhookConfigurationID: input.ID, + EndpointURL: input.EndpointURL, + SelectedEvents: input.SelectedEvents, + }, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot update webhook configuration", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return &types.UpdateWebhookConfigurationPayload{ + WebhookConfiguration: types.NewWebhookConfiguration(wc), + }, nil +} + +// DeleteWebhookConfiguration is the resolver for the deleteWebhookConfiguration field. +func (r *mutationResolver) DeleteWebhookConfiguration(ctx context.Context, input types.DeleteWebhookConfigurationInput) (*types.DeleteWebhookConfigurationPayload, error) { + if err := r.authorize(ctx, input.WebhookConfigurationID, probo.ActionWebhookConfigurationDelete); err != nil { + return nil, err + } + + prb := r.ProboService(ctx, input.WebhookConfigurationID.TenantID()) + + err := prb.WebhookConfigurations.Delete(ctx, input.WebhookConfigurationID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot delete webhook configuration", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return &types.DeleteWebhookConfigurationPayload{ + DeletedWebhookConfigurationID: input.WebhookConfigurationID, + }, nil +} + // CreateStateOfApplicability is the resolver for the createStateOfApplicability field. func (r *mutationResolver) CreateStateOfApplicability(ctx context.Context, input types.CreateStateOfApplicabilityInput) (*types.CreateStateOfApplicabilityPayload, error) { if err := r.authorize(ctx, input.OrganizationID, probo.ActionStateOfApplicabilityCreate); err != nil { @@ -6313,6 +6384,36 @@ func (r *organizationResolver) CustomDomain(ctx context.Context, obj *types.Orga return types.NewCustomDomain(domain, r.customDomainCname), nil } +// WebhookConfigurations is the resolver for the webhookConfigurations field. +func (r *organizationResolver) WebhookConfigurations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.WebhookConfigurationOrderBy) (*types.WebhookConfigurationConnection, error) { + if err := r.authorize(ctx, obj.ID, probo.ActionWebhookConfigurationList); err != nil { + return nil, err + } + + prb := r.ProboService(ctx, obj.ID.TenantID()) + + pageOrderBy := page.OrderBy[coredata.WebhookConfigurationOrderField]{ + Field: coredata.WebhookConfigurationOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if orderBy != nil { + pageOrderBy = page.OrderBy[coredata.WebhookConfigurationOrderField]{ + Field: orderBy.Field, + Direction: orderBy.Direction, + } + } + + cursor := types.NewCursor(first, after, last, before, pageOrderBy) + + page, err := prb.WebhookConfigurations.ListForOrganizationID(ctx, obj.ID, cursor) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot list organization webhook configurations", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return types.NewWebhookConfigurationConnection(page, r, obj.ID), nil +} + // Permission is the resolver for the permission field. func (r *organizationResolver) Permission(ctx context.Context, obj *types.Organization, action string) (bool, error) { return r.Resolver.Permission(ctx, obj, action) @@ -6761,6 +6862,15 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error } return types.NewStateOfApplicability(stateOfApplicability), nil } + case coredata.WebhookConfigurationEntityType: + action = probo.ActionWebhookConfigurationGet + loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { + wc, err := prb.WebhookConfigurations.Get(ctx, id) + if err != nil { + return nil, err + } + return types.NewWebhookConfiguration(wc), nil + } default: } @@ -8449,6 +8559,67 @@ func (r *viewerResolver) SignableDocument(ctx context.Context, obj *types.Viewer }, nil } +// Organization is the resolver for the organization field. +func (r *webhookConfigurationResolver) Organization(ctx context.Context, obj *types.WebhookConfiguration) (*types.Organization, error) { + if err := r.authorize(ctx, obj.ID, probo.ActionOrganizationGet); err != nil { + return nil, err + } + + prb := r.ProboService(ctx, obj.ID.TenantID()) + + organization, err := prb.Organizations.Get(ctx, obj.Organization.ID) + if err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return nil, gqlutils.NotFound(ctx, err) + } + + r.logger.ErrorCtx(ctx, "cannot load organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return types.NewOrganization(organization), nil +} + +// SigningSecret is the resolver for the signingSecret field. +func (r *webhookConfigurationResolver) SigningSecret(ctx context.Context, obj *types.WebhookConfiguration) (string, error) { + prb := r.ProboService(ctx, obj.ID.TenantID()) + + signingSecret, err := prb.WebhookConfigurations.GetSigningSecret(ctx, obj.ID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot get signing secret", log.Error(err)) + return "", gqlutils.Internal(ctx) + } + + return signingSecret, nil +} + +// Permission is the resolver for the permission field. +func (r *webhookConfigurationResolver) Permission(ctx context.Context, obj *types.WebhookConfiguration, action string) (bool, error) { + return r.Resolver.Permission(ctx, obj, action) +} + +// TotalCount is the resolver for the totalCount field. +func (r *webhookConfigurationConnectionResolver) TotalCount(ctx context.Context, obj *types.WebhookConfigurationConnection) (int, error) { + if err := r.authorize(ctx, obj.ParentID, probo.ActionWebhookConfigurationList); err != nil { + return 0, err + } + + prb := r.ProboService(ctx, obj.ParentID.TenantID()) + + switch obj.Resolver.(type) { + case *organizationResolver: + count, err := prb.WebhookConfigurations.CountForOrganizationID(ctx, obj.ParentID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot count webhook configurations", log.Error(err)) + return 0, gqlutils.Internal(ctx) + } + return count, nil + } + + r.logger.ErrorCtx(ctx, "unsupported resolver for webhook configuration connection", log.String("resolver", fmt.Sprintf("%T", obj.Resolver))) + return 0, gqlutils.Internal(ctx) +} + // ApplicabilityStatement returns schema.ApplicabilityStatementResolver implementation. func (r *Resolver) ApplicabilityStatement() schema.ApplicabilityStatementResolver { return &applicabilityStatementResolver{r} @@ -8751,6 +8922,16 @@ func (r *Resolver) VendorService() schema.VendorServiceResolver { return &vendor // Viewer returns schema.ViewerResolver implementation. func (r *Resolver) Viewer() schema.ViewerResolver { return &viewerResolver{r} } +// WebhookConfiguration returns schema.WebhookConfigurationResolver implementation. +func (r *Resolver) WebhookConfiguration() schema.WebhookConfigurationResolver { + return &webhookConfigurationResolver{r} +} + +// WebhookConfigurationConnection returns schema.WebhookConfigurationConnectionResolver implementation. +func (r *Resolver) WebhookConfigurationConnection() schema.WebhookConfigurationConnectionResolver { + return &webhookConfigurationConnectionResolver{r} +} + type applicabilityStatementResolver struct{ *Resolver } type applicabilityStatementConnectionResolver struct{ *Resolver } type assetResolver struct{ *Resolver } @@ -8823,3 +9004,5 @@ type vendorDataPrivacyAgreementResolver struct{ *Resolver } type vendorRiskAssessmentResolver struct{ *Resolver } type vendorServiceResolver struct{ *Resolver } type viewerResolver struct{ *Resolver } +type webhookConfigurationResolver struct{ *Resolver } +type webhookConfigurationConnectionResolver struct{ *Resolver } diff --git a/pkg/webhook/data.go b/pkg/webhook/data.go new file mode 100644 index 000000000..b8d0fa5db --- /dev/null +++ b/pkg/webhook/data.go @@ -0,0 +1,153 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package webhook + +import ( + "context" + "encoding" + "encoding/json" + "fmt" + "reflect" + "strings" + "time" + + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/gid" +) + +var ( + jsonMarshalerType = reflect.TypeOf((*json.Marshaler)(nil)).Elem() + textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem() +) + +func InsertEvent( + ctx context.Context, + conn pg.Conn, + scope coredata.Scoper, + organizationID gid.GID, + eventType coredata.WebhookEventType, + data any, +) error { + var configs coredata.WebhookConfigurations + exists, err := configs.ExistsByOrganizationIDAndEventType(ctx, conn, scope, organizationID, eventType) + if err != nil { + return fmt.Errorf("cannot check webhook configurations: %w", err) + } + + if !exists { + return nil + } + + raw, err := MarshalData(data) + if err != nil { + return fmt.Errorf("cannot marshal webhook event data: %w", err) + } + + event := &coredata.WebhookEvent{ + ID: gid.New(scope.GetTenantID(), coredata.WebhookEventEntityType), + OrganizationID: organizationID, + EventType: eventType, + Status: coredata.WebhookEventStatusPending, + Data: raw, + CreatedAt: time.Now(), + } + + if err = event.Insert(ctx, conn, scope); err != nil { + return fmt.Errorf("cannot insert webhook event: %w", err) + } + + return nil +} + +func MarshalData(v any) (json.RawMessage, error) { + raw, err := json.Marshal(v) + if err != nil { + return nil, fmt.Errorf("cannot marshal webhook data: %w", err) + } + + var m map[string]any + if err := json.Unmarshal(raw, &m); err != nil { + return nil, fmt.Errorf("cannot unmarshal webhook data: %w", err) + } + + for _, key := range nestedFieldKeys(v) { + delete(m, key) + } + + delete(m, "permission") + + data, err := json.Marshal(m) + if err != nil { + return nil, fmt.Errorf("cannot re-marshal webhook data: %w", err) + } + + return data, nil +} + +func nestedFieldKeys(v any) []string { + t := reflect.TypeOf(v) + for t.Kind() == reflect.Pointer { + t = t.Elem() + } + + if t.Kind() != reflect.Struct { + return nil + } + + var keys []string + for i := range t.NumField() { + field := t.Field(i) + + tag := field.Tag.Get("json") + if tag == "" || tag == "-" { + continue + } + + jsonKey, _, _ := strings.Cut(tag, ",") + + if isNestedType(field.Type) { + keys = append(keys, jsonKey) + } + } + + return keys +} + +func isNestedType(t reflect.Type) bool { + for t.Kind() == reflect.Pointer { + t = t.Elem() + } + + if t.Kind() == reflect.Slice { + return isNestedType(t.Elem()) + } + + if t.Kind() != reflect.Struct { + return false + } + + ptrType := reflect.PointerTo(t) + + if t.Implements(jsonMarshalerType) || ptrType.Implements(jsonMarshalerType) { + return false + } + + if t.Implements(textMarshalerType) || ptrType.Implements(textMarshalerType) { + return false + } + + return true +} diff --git a/pkg/webhook/sender.go b/pkg/webhook/sender.go new file mode 100644 index 000000000..ec8b581ce --- /dev/null +++ b/pkg/webhook/sender.go @@ -0,0 +1,352 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package webhook + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + "sync" + "time" + + "go.gearno.de/kit/httpclient" + "go.gearno.de/kit/log" + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/crypto/cipher" + "go.probo.inc/probo/pkg/gid" +) + +type ( + Sender struct { + pg *pg.Client + logger *log.Logger + httpClient *http.Client + encryptionKey cipher.EncryptionKey + cache sync.Map + interval time.Duration + timeout time.Duration + } + + Config struct { + Interval time.Duration + Timeout time.Duration + EncryptionKey cipher.EncryptionKey + } +) + +const maxResponseBodySize = 64 * 1024 // 64KB + +func NewSender(pg *pg.Client, logger *log.Logger, cfg Config) *Sender { + if cfg.Interval <= 0 { + cfg.Interval = 5 * time.Second + } + + if cfg.Timeout <= 0 { + cfg.Timeout = 30 * time.Second + } + + return &Sender{ + pg: pg, + logger: logger, + httpClient: httpclient.DefaultPooledClient(httpclient.WithLogger(logger)), + encryptionKey: cfg.EncryptionKey, + interval: cfg.Interval, + timeout: cfg.Timeout, + } +} + +func (s *Sender) Run(ctx context.Context) error { + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(s.interval): + if err := s.processEvents(ctx); err != nil { + s.logger.ErrorCtx(ctx, "cannot process webhook events", log.Error(err)) + } + } + } +} + +func (s *Sender) processEvents(ctx context.Context) error { + for { + event, err := s.claimNextEvent(ctx) + if err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return nil + } + return err + } + + s.processEvent(ctx, event) + } +} + +func (s *Sender) claimNextEvent(ctx context.Context) (*coredata.WebhookEvent, error) { + var event coredata.WebhookEvent + + err := s.pg.WithTx(ctx, func(tx pg.Conn) error { + if err := event.LoadNextPendingForUpdate(ctx, tx); err != nil { + return err + } + + scope := coredata.NewScopeFromObjectID(event.ID) + + event.Status = coredata.WebhookEventStatusProcessing + + if err := event.UpdateStatus(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot update webhook event to processing: %w", err) + } + + return nil + }) + + if err != nil { + return nil, err + } + + return &event, nil +} + +func (s *Sender) processEvent(ctx context.Context, event *coredata.WebhookEvent) { + scope := coredata.NewScopeFromObjectID(event.ID) + + var configs coredata.WebhookConfigurations + err := s.pg.WithConn(ctx, func(conn pg.Conn) error { + return configs.LoadMatchingByOrganizationIDAndEventType( + ctx, conn, scope, event.OrganizationID, event.EventType, + ) + }) + if err != nil { + s.logger.ErrorCtx(ctx, "cannot load matching webhook configurations", + log.Error(err), + log.String("event_id", event.ID.String()), + ) + return + } + + for _, config := range configs { + s.deliverToConfiguration(ctx, event, config, scope) + } + + now := time.Now() + event.Status = coredata.WebhookEventStatusDelivered + event.ProcessedAt = &now + + err = s.pg.WithConn(ctx, func(conn pg.Conn) error { + return event.UpdateStatus(ctx, conn, scope) + }) + if err != nil { + s.logger.ErrorCtx(ctx, "cannot update webhook event to delivered", + log.Error(err), + log.String("event_id", event.ID.String()), + ) + } +} + +func (s *Sender) deliverToConfiguration( + ctx context.Context, + event *coredata.WebhookEvent, + config *coredata.WebhookConfiguration, + scope coredata.Scoper, +) { + callID := gid.New(event.ID.TenantID(), coredata.WebhookCallEntityType) + + signingSecret, err := s.getSigningSecret(config.ID.String(), config.EncryptedSigningSecret) + if err != nil { + s.logger.ErrorCtx(ctx, "cannot get signing secret", + log.Error(err), + log.String("event_id", event.ID.String()), + log.String("configuration_id", config.ID.String()), + ) + s.recordCall(ctx, callID, event, config, scope, coredata.WebhookCallStatusFailed, nil) + return + } + + response, sendErr := s.doHTTPCall(ctx, callID, config.EndpointURL, event, config.ID, signingSecret) + + callStatus := coredata.WebhookCallStatusSucceeded + if sendErr != nil { + callStatus = coredata.WebhookCallStatusFailed + s.logger.ErrorCtx(ctx, "error delivering webhook event", + log.Error(sendErr), + log.String("event_id", event.ID.String()), + log.String("endpoint_url", config.EndpointURL), + ) + } + + s.recordCall(ctx, callID, event, config, scope, callStatus, response) +} + +func (s *Sender) getSigningSecret(webhookConfigurationID string, encryptedSigningSecret []byte) (string, error) { + if cached, ok := s.cache.Load(webhookConfigurationID); ok { + return cached.(string), nil + } + + plaintext, err := cipher.Decrypt(encryptedSigningSecret, s.encryptionKey) + if err != nil { + return "", fmt.Errorf("cannot decrypt signing secret: %w", err) + } + + signingSecret := string(plaintext) + s.cache.Store(webhookConfigurationID, signingSecret) + + return signingSecret, nil +} + +func (s *Sender) doHTTPCall( + ctx context.Context, + callID gid.GID, + endpointURL string, + event *coredata.WebhookEvent, + configurationID gid.GID, + signingSecret string, +) (json.RawMessage, error) { + payload := map[string]any{ + "eventId": event.ID.String(), + "callId": callID.String(), + "configurationId": configurationID.String(), + "eventType": event.EventType.String(), + "createdAt": event.CreatedAt, + "data": event.Data, + } + + body, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("cannot marshal webhook payload: %w", err) + } + + reqCtx, cancel := context.WithTimeout(ctx, s.timeout) + defer cancel() + + req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, endpointURL, bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("cannot create request: %w", err) + } + + timestamp := strconv.FormatInt(time.Now().Unix(), 10) + signature := computeSignature(signingSecret, timestamp, body) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Probo-Webhook-Event", event.EventType.String()) + req.Header.Set("X-Probo-Webhook-Timestamp", timestamp) + req.Header.Set("X-Probo-Webhook-Signature", signature) + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("cannot send request: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + respBody, _ := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodySize)) + + response := buildResponseJSON(resp, respBody) + + switch resp.StatusCode { + case http.StatusOK, + http.StatusCreated, + http.StatusAccepted, + http.StatusNoContent: + return response, nil + default: + return response, fmt.Errorf("webhook endpoint returned status %d", resp.StatusCode) + } +} + +func (s *Sender) recordCall( + ctx context.Context, + callID gid.GID, + event *coredata.WebhookEvent, + config *coredata.WebhookConfiguration, + scope coredata.Scoper, + status coredata.WebhookCallStatus, + response json.RawMessage, +) { + call := coredata.WebhookCall{ + ID: callID, + WebhookEventID: event.ID, + WebhookConfigurationID: config.ID, + EndpointURL: config.EndpointURL, + Status: status, + Response: response, + CreatedAt: time.Now(), + } + + err := s.pg.WithConn(ctx, func(conn pg.Conn) error { + return call.Insert(ctx, conn, scope) + }) + if err != nil { + s.logger.ErrorCtx(ctx, "cannot insert webhook call", + log.Error(err), + log.String("event_id", event.ID.String()), + log.String("configuration_id", config.ID.String()), + ) + } +} + +func buildResponseJSON(resp *http.Response, body []byte) json.RawMessage { + headers := make(map[string]any, len(resp.Header)) + for k, v := range resp.Header { + if len(v) == 1 { + headers[k] = v[0] + } else { + headers[k] = v + } + } + + var bodyValue any + if json.Valid(body) { + bodyValue = json.RawMessage(body) + } else { + bodyValue = string(body) + } + + respObj := map[string]any{ + "proto": resp.Proto, + "status_code": resp.StatusCode, + "headers": headers, + "body": bodyValue, + } + + if len(resp.Trailer) > 0 { + trailers := make(map[string]any, len(resp.Trailer)) + for k, v := range resp.Trailer { + if len(v) == 1 { + trailers[k] = v[0] + } else { + trailers[k] = v + } + } + respObj["trailers"] = trailers + } + + data, _ := json.Marshal(respObj) + return data +} + +func computeSignature(signingSecret, timestamp string, body []byte) string { + h := hmac.New(sha256.New, []byte(signingSecret)) + _, _ = fmt.Fprintf(h, "%s:%s", timestamp, body) + return hex.EncodeToString(h.Sum(nil)) +}