diff --git a/apps/console/src/components/organizations/SlackConnection.tsx b/apps/console/src/components/organizations/SlackConnection.tsx new file mode 100644 index 000000000..a7105d1f3 --- /dev/null +++ b/apps/console/src/components/organizations/SlackConnection.tsx @@ -0,0 +1,87 @@ +import { Badge, Button, Card } from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import { sprintf } from "@probo/helpers"; +import type { TrustCenterGraphQuery$data } from "/hooks/graph/__generated__/TrustCenterGraphQuery.graphql"; + +type Props = { + organizationId: string; + slackConnections: NonNullable["edges"][number]["node"][]; +}; + +export function SlackConnections({ organizationId, slackConnections: connectedSlackConnections }: Props) { + const { __, dateTimeFormat } = useTranslate(); + + const slackConnectionDefinitions = [ + { + id: "SLACK", + name: "Slack", + protocol: "OAUTH2", + description: __("Manage your trust center access with slack"), + }, + ]; + + const slackConnections = slackConnectionDefinitions.map((def) => { + const connected = connectedSlackConnections.find((c) => c.id); + return { + ...def, + createdAt: connected?.createdAt, + channel: connected?.channel, + channelId: connected?.channelId, + }; + }); + + const getUrl = (provider: string) => { + const baseUrl = import.meta.env.VITE_API_URL || window.location.origin; + const url = new URL("/api/console/v1/connectors/initiate", baseUrl); + url.searchParams.append("organization_id", organizationId); + url.searchParams.append("provider", provider); + const trustCenterUrl = `/organizations/${organizationId}/trust-center`; + url.searchParams.append("continue", trustCenterUrl); + const finalUrl = url.toString(); + return finalUrl; + }; + + return ( +
+ {slackConnections.map((slackConnection) => ( + +
+ +
+
+

{slackConnection.name}

+

+ {slackConnection.createdAt ? ( + <> + {sprintf( + __("Connected on %s"), + dateTimeFormat(slackConnection.createdAt) + )} + {slackConnection.channel && ( + <> + {" • "} + {sprintf(__("Channel: %s"), slackConnection.channel)} + + )} + + ) : ( + slackConnection.description + )} +

+
+ {slackConnection.createdAt ? ( +
+ + {__("Connected")} + +
+ ) : ( + + )} +
+ ))} +
+ ); +} diff --git a/apps/console/src/hooks/graph/TrustCenterGraph.ts b/apps/console/src/hooks/graph/TrustCenterGraph.ts index 334625568..715f90b8b 100644 --- a/apps/console/src/hooks/graph/TrustCenterGraph.ts +++ b/apps/console/src/hooks/graph/TrustCenterGraph.ts @@ -57,6 +57,17 @@ export const trustCenterQuery = graphql` } } } + slackConnections(first: 100) { + edges { + node { + id + channel + channelId + createdAt + updatedAt + } + } + } } } } diff --git a/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts index 34c67d5d1..5f51da2a5 100644 --- a/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -72,29 +72,22 @@ v6 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "type", + "name": "createdAt", "storageKey": null }, v7 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "createdAt", - "storageKey": null -}, -v8 = { "alias": null, "args": null, "kind": "ScalarField", "name": "updatedAt", "storageKey": null }, -v9 = { +v8 = { "kind": "Literal", "name": "first", "value": 20 }, -v10 = { +v9 = { "kind": "Literal", "name": "orderBy", "value": { @@ -102,39 +95,39 @@ v10 = { "field": "CREATED_AT" } }, -v11 = [ - (v9/*: any*/), - (v10/*: any*/) +v10 = [ + (v8/*: any*/), + (v9/*: any*/) ], -v12 = { +v11 = { "alias": null, "args": null, "kind": "ScalarField", "name": "totalCount", "storageKey": null }, -v13 = { +v12 = { "alias": null, "args": null, "kind": "ScalarField", "name": "fullName", "storageKey": null }, -v14 = { +v13 = { "alias": null, "args": null, "kind": "ScalarField", "name": "role", "storageKey": null }, -v15 = { +v14 = { "alias": null, "args": null, "kind": "ScalarField", "name": "cursor", "storageKey": null }, -v16 = { +v15 = { "alias": null, "args": null, "concreteType": "PageInfo", @@ -173,7 +166,7 @@ v16 = { ], "storageKey": null }, -v17 = { +v16 = { "kind": "ClientExtension", "selections": [ { @@ -185,7 +178,7 @@ v17 = { } ] }, -v18 = [ +v17 = [ { "kind": "Literal", "name": "filter", @@ -196,8 +189,8 @@ v18 = [ ] } }, - (v9/*: any*/), - (v10/*: any*/) + (v8/*: any*/), + (v9/*: any*/) ]; return { "fragment": { @@ -332,7 +325,13 @@ return { "name": "dnsRecords", "plural": true, "selections": [ - (v6/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "type", + "storageKey": null + }, (v3/*: any*/), { "alias": null, @@ -358,8 +357,8 @@ return { ], "storageKey": null }, + (v6/*: any*/), (v7/*: any*/), - (v8/*: any*/), { "alias": null, "args": null, @@ -370,60 +369,17 @@ return { ], "storageKey": null }, + (v6/*: any*/), (v7/*: any*/), - (v8/*: any*/), { "alias": null, - "args": [ - { - "kind": "Literal", - "name": "first", - "value": 100 - } - ], - "concreteType": "ConnectorConnection", - "kind": "LinkedField", - "name": "connectors", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "ConnectorEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Connector", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v2/*: any*/), - (v3/*: any*/), - (v6/*: any*/), - (v7/*: any*/) - ], - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": "connectors(first:100)" - }, - { - "alias": null, - "args": (v11/*: any*/), + "args": (v10/*: any*/), "concreteType": "MembershipConnection", "kind": "LinkedField", "name": "memberships", "plural": false, "selections": [ - (v12/*: any*/), + (v11/*: any*/), { "alias": null, "args": null, @@ -441,7 +397,7 @@ return { "plural": false, "selections": [ (v2/*: any*/), - (v13/*: any*/), + (v12/*: any*/), { "alias": null, "args": null, @@ -449,24 +405,24 @@ return { "name": "emailAddress", "storageKey": null }, - (v14/*: any*/), - (v7/*: any*/), + (v13/*: any*/), + (v6/*: any*/), (v4/*: any*/) ], "storageKey": null }, - (v15/*: any*/) + (v14/*: any*/) ], "storageKey": null }, - (v16/*: any*/), - (v17/*: any*/) + (v15/*: any*/), + (v16/*: any*/) ], "storageKey": "memberships(first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" }, { "alias": null, - "args": (v11/*: any*/), + "args": (v10/*: any*/), "filters": [ "orderBy" ], @@ -477,13 +433,13 @@ return { }, { "alias": null, - "args": (v18/*: any*/), + "args": (v17/*: any*/), "concreteType": "InvitationConnection", "kind": "LinkedField", "name": "invitations", "plural": false, "selections": [ - (v12/*: any*/), + (v11/*: any*/), { "alias": null, "args": null, @@ -502,8 +458,8 @@ return { "selections": [ (v2/*: any*/), (v5/*: any*/), + (v12/*: any*/), (v13/*: any*/), - (v14/*: any*/), { "alias": null, "args": null, @@ -525,23 +481,23 @@ return { "name": "acceptedAt", "storageKey": null }, - (v7/*: any*/), + (v6/*: any*/), (v4/*: any*/) ], "storageKey": null }, - (v15/*: any*/) + (v14/*: any*/) ], "storageKey": null }, - (v16/*: any*/), - (v17/*: any*/) + (v15/*: any*/), + (v16/*: any*/) ], "storageKey": "invitations(filter:{\"statuses\":[\"PENDING\",\"EXPIRED\"]},first:20,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" }, { "alias": null, - "args": (v18/*: any*/), + "args": (v17/*: any*/), "filters": [ "orderBy", "filter" @@ -561,12 +517,12 @@ return { ] }, "params": { - "cacheID": "daa371367edee014ac05cb9712ce360d", + "cacheID": "3d38000d9d2105ef8ae3d56edf31b372", "id": null, "metadata": {}, "name": "OrganizationGraph_ViewQuery", "operationKind": "query", - "text": "query OrganizationGraph_ViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...SettingsPageFragment\n ...SettingsPageMembershipsFragment\n ...SettingsPageInvitationsFragment\n }\n id\n }\n}\n\nfragment SettingsPageFragment on Organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n customDomain {\n id\n domain\n sslStatus\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n }\n createdAt\n updatedAt\n connectors(first: 100) {\n edges {\n node {\n id\n name\n type\n createdAt\n }\n }\n }\n}\n\nfragment SettingsPageInvitationsFragment on Organization {\n invitations(first: 20, orderBy: {direction: ASC, field: CREATED_AT}, filter: {statuses: [PENDING, EXPIRED]}) {\n totalCount\n edges {\n node {\n id\n email\n fullName\n role\n status\n expiresAt\n acceptedAt\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment SettingsPageMembershipsFragment on Organization {\n memberships(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n fullName\n emailAddress\n role\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" + "text": "query OrganizationGraph_ViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...SettingsPageFragment\n ...SettingsPageMembershipsFragment\n ...SettingsPageInvitationsFragment\n }\n id\n }\n}\n\nfragment SettingsPageFragment on Organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n customDomain {\n id\n domain\n sslStatus\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n }\n createdAt\n updatedAt\n}\n\nfragment SettingsPageInvitationsFragment on Organization {\n invitations(first: 20, orderBy: {direction: ASC, field: CREATED_AT}, filter: {statuses: [PENDING, EXPIRED]}) {\n totalCount\n edges {\n node {\n id\n email\n fullName\n role\n status\n expiresAt\n acceptedAt\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment SettingsPageMembershipsFragment on Organization {\n memberships(first: 20, orderBy: {direction: ASC, field: CREATED_AT}) {\n totalCount\n edges {\n node {\n id\n fullName\n emailAddress\n role\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" } }; })(); diff --git a/apps/console/src/hooks/graph/__generated__/TrustCenterGraphQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/TrustCenterGraphQuery.graphql.ts index 64c53ac94..f0bbb3c90 100644 --- a/apps/console/src/hooks/graph/__generated__/TrustCenterGraphQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/TrustCenterGraphQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<8e92507c4ea06441e81f19bdcd6d4feb>> + * @generated SignedSource<<6d55d37aa2ddfae16f8fc829840113d4>> * @lightSyntaxTransform * @nogrep */ @@ -37,6 +37,17 @@ export type TrustCenterGraphQuery$data = { }; readonly id?: string; readonly name?: string; + readonly slackConnections?: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly channel: string | null | undefined; + readonly channelId: string | null | undefined; + readonly createdAt: any; + readonly id: string; + readonly updatedAt: any; + }; + }>; + }; readonly trustCenter?: { readonly active: boolean; readonly createdAt: any; @@ -248,6 +259,56 @@ v10 = [ (v7/*: any*/) ], v11 = { + "alias": null, + "args": (v10/*: any*/), + "concreteType": "SlackConnectionConnection", + "kind": "LinkedField", + "name": "slackConnections", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SlackConnectionEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SlackConnection", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "channel", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "channelId", + "storageKey": null + }, + (v5/*: any*/), + (v6/*: any*/) + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "slackConnections(first:100)" +}, +v12 = { "alias": null, "args": null, "kind": "ScalarField", @@ -392,7 +453,8 @@ return { } ], "storageKey": "vendors(first:100)" - } + }, + (v11/*: any*/) ], "type": "Organization", "abstractKey": null @@ -472,7 +534,7 @@ return { "name": "documentType", "storageKey": null }, - (v11/*: any*/), + (v12/*: any*/), { "alias": null, "args": [ @@ -589,7 +651,7 @@ return { "name": "state", "storageKey": null }, - (v11/*: any*/), + (v12/*: any*/), (v5/*: any*/) ], "storageKey": null @@ -650,7 +712,8 @@ return { } ], "storageKey": "vendors(first:100)" - } + }, + (v11/*: any*/) ], "type": "Organization", "abstractKey": null @@ -661,16 +724,16 @@ return { ] }, "params": { - "cacheID": "89f1657de6e2c073609a2c69d7b2d747", + "cacheID": "06cd2015e9fc2e65d35ae3153e587c58", "id": null, "metadata": {}, "name": "TrustCenterGraphQuery", "operationKind": "query", - "text": "query TrustCenterGraphQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n customDomain {\n id\n domain\n }\n trustCenter {\n id\n active\n ndaFileName\n ndaFileUrl\n createdAt\n updatedAt\n references(first: 100, orderBy: {field: CREATED_AT, direction: DESC}) {\n edges {\n node {\n id\n name\n description\n websiteUrl\n logoUrl\n createdAt\n updatedAt\n }\n }\n }\n }\n documents(first: 100) {\n edges {\n node {\n id\n ...TrustCenterDocumentsCardFragment\n }\n }\n }\n audits(first: 100) {\n edges {\n node {\n id\n ...TrustCenterAuditsCardFragment\n }\n }\n }\n vendors(first: 100) {\n edges {\n node {\n id\n ...TrustCenterVendorsCardFragment\n }\n }\n }\n }\n id\n }\n}\n\nfragment TrustCenterAuditsCardFragment on Audit {\n id\n name\n framework {\n name\n id\n }\n validFrom\n validUntil\n state\n trustCenterVisibility\n createdAt\n}\n\nfragment TrustCenterDocumentsCardFragment on Document {\n id\n title\n createdAt\n documentType\n trustCenterVisibility\n versions(first: 1) {\n edges {\n node {\n id\n status\n }\n }\n }\n}\n\nfragment TrustCenterVendorsCardFragment on Vendor {\n id\n name\n category\n description\n showOnTrustCenter\n createdAt\n}\n" + "text": "query TrustCenterGraphQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n customDomain {\n id\n domain\n }\n trustCenter {\n id\n active\n ndaFileName\n ndaFileUrl\n createdAt\n updatedAt\n references(first: 100, orderBy: {field: CREATED_AT, direction: DESC}) {\n edges {\n node {\n id\n name\n description\n websiteUrl\n logoUrl\n createdAt\n updatedAt\n }\n }\n }\n }\n documents(first: 100) {\n edges {\n node {\n id\n ...TrustCenterDocumentsCardFragment\n }\n }\n }\n audits(first: 100) {\n edges {\n node {\n id\n ...TrustCenterAuditsCardFragment\n }\n }\n }\n vendors(first: 100) {\n edges {\n node {\n id\n ...TrustCenterVendorsCardFragment\n }\n }\n }\n slackConnections(first: 100) {\n edges {\n node {\n id\n channel\n channelId\n createdAt\n updatedAt\n }\n }\n }\n }\n id\n }\n}\n\nfragment TrustCenterAuditsCardFragment on Audit {\n id\n name\n framework {\n name\n id\n }\n validFrom\n validUntil\n state\n trustCenterVisibility\n createdAt\n}\n\nfragment TrustCenterDocumentsCardFragment on Document {\n id\n title\n createdAt\n documentType\n trustCenterVisibility\n versions(first: 1) {\n edges {\n node {\n id\n status\n }\n }\n }\n}\n\nfragment TrustCenterVendorsCardFragment on Vendor {\n id\n name\n category\n description\n showOnTrustCenter\n createdAt\n}\n" } }; })(); -(node as any).hash = "60725f4e10d6e720a09a883430854ada"; +(node as any).hash = "7ef9a5adc70dff604a149ea156e52edc"; export default node; diff --git a/apps/console/src/pages/organizations/SettingsPage.tsx b/apps/console/src/pages/organizations/SettingsPage.tsx index 9970ba993..3e4468ad7 100644 --- a/apps/console/src/pages/organizations/SettingsPage.tsx +++ b/apps/console/src/pages/organizations/SettingsPage.tsx @@ -32,10 +32,7 @@ import { organizationViewQuery } from "/hooks/graph/OrganizationGraph"; import { graphql } from "relay-runtime"; import { SortableTable, SortableTh } from "/components/SortableTable"; import clsx from "clsx"; -import type { - SettingsPageFragment$data, - SettingsPageFragment$key, -} from "./__generated__/SettingsPageFragment.graphql"; +import type { SettingsPageFragment$key } from "./__generated__/SettingsPageFragment.graphql"; import type { SettingsPageMembershipsFragment$data, SettingsPageMembershipsFragment$key @@ -98,16 +95,6 @@ const organizationFragment = graphql` } createdAt updatedAt - connectors(first: 100) { - edges { - node { - id - name - type - createdAt - } - } - } } `; @@ -702,17 +689,6 @@ export default function SettingsPage({ queryRef }: Props) { - {/* Integrations */} -
-

{__("Integrations")}

- - edge.node)} - /> - -
-

{__("Custom Domain")}

[]; -}) { - const { __, dateTimeFormat } = useTranslate(); - const fakeconnectors = [ - { - id: "github", - name: "GitHub", - type: "oauth2", - createdAt: new Date(), - }, - ] satisfies typeof props.connectors; - const connectors = [ - { - id: "github", - name: "GitHub", - type: "oauth2", - description: __("Connect to GitHub repositories and issues"), - ...fakeconnectors.find((connector) => connector.id === "github"), - }, - { - id: "slack", - name: "Slack", - type: "oauth2", - description: __("Connect to Slack workspace and channels"), - ...fakeconnectors.find((connector) => connector.id === "slack"), - }, - ]; - - const getUrl = (connectorId: string) => { - const baseUrl = import.meta.env.VITE_API_URL || window.location.origin; - const url = new URL("/api/console/v1/connectors/initiate", baseUrl); - url.searchParams.append("organization_id", props.organizationId); - url.searchParams.append("connector_id", connectorId); - url.searchParams.append("continue", window.location.href); - return url.toString(); - }; - - return ( -
- {connectors.map((connector) => ( - -
- -
-
-

{connector.name}

-

- {connector.createdAt - ? sprintf( - __("Connected on %s"), - dateTimeFormat(connector.createdAt) - ) - : connector.description} -

-
- {connector.createdAt ? ( -
- - {__("Connected")} - -
- ) : ( - - )} -
- ))} -
- ); -} - const removeMemberMutation = graphql` mutation SettingsPage_RemoveMemberMutation( $input: RemoveMemberInput! diff --git a/apps/console/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts b/apps/console/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts index 95346b07a..20fabeda0 100644 --- a/apps/console/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts +++ b/apps/console/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<8477f42a02a102a4ce8540b794ac4289>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -12,16 +12,6 @@ import { ReaderFragment } from 'relay-runtime'; export type SSLStatus = "ACTIVE" | "EXPIRED" | "FAILED" | "PENDING" | "PROVISIONING" | "RENEWING"; import { FragmentRefs } from "relay-runtime"; export type SettingsPageFragment$data = { - readonly connectors: { - readonly edges: ReadonlyArray<{ - readonly node: { - readonly createdAt: any; - readonly id: string; - readonly name: string; - readonly type: string; - }; - }>; - }; readonly createdAt: any; readonly customDomain: { readonly createdAt: any; @@ -70,20 +60,13 @@ v1 = { "storageKey": null }, v2 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "type", - "storageKey": null -}, -v3 = { "alias": null, "args": null, "kind": "ScalarField", "name": "createdAt", "storageKey": null }, -v4 = { +v3 = { "alias": null, "args": null, "kind": "ScalarField", @@ -171,7 +154,13 @@ return { "name": "dnsRecords", "plural": true, "selections": [ - (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "type", + "storageKey": null + }, (v1/*: any*/), { "alias": null, @@ -197,8 +186,8 @@ return { ], "storageKey": null }, + (v2/*: any*/), (v3/*: any*/), - (v4/*: any*/), { "alias": null, "args": null, @@ -209,57 +198,14 @@ return { ], "storageKey": null }, - (v3/*: any*/), - (v4/*: any*/), - { - "alias": null, - "args": [ - { - "kind": "Literal", - "name": "first", - "value": 100 - } - ], - "concreteType": "ConnectorConnection", - "kind": "LinkedField", - "name": "connectors", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "ConnectorEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Connector", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v0/*: any*/), - (v1/*: any*/), - (v2/*: any*/), - (v3/*: any*/) - ], - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": "connectors(first:100)" - } + (v2/*: any*/), + (v3/*: any*/) ], "type": "Organization", "abstractKey": null }; })(); -(node as any).hash = "b3b152b507befd6b6918a12972e05f14"; +(node as any).hash = "1e64bcccf8ef3a8ead79b3446e8a3ccd"; export default node; diff --git a/apps/console/src/pages/organizations/trustCenter/TrustCenterPage.tsx b/apps/console/src/pages/organizations/trustCenter/TrustCenterPage.tsx index aa25d1520..59942ad9a 100644 --- a/apps/console/src/pages/organizations/trustCenter/TrustCenterPage.tsx +++ b/apps/console/src/pages/organizations/trustCenter/TrustCenterPage.tsx @@ -20,6 +20,7 @@ import { useState } from "react"; import { useOrganizationId } from "/hooks/useOrganizationId"; import { Outlet, useLocation, Link } from "react-router"; import { TrustCenterReferencesSection } from "/components/trustCenter/TrustCenterReferencesSection"; +import { SlackConnections } from "../../../components/organizations/SlackConnection"; type Props = { queryRef: PreloadedQuery; @@ -257,6 +258,16 @@ export default function TrustCenterPage({ queryRef }: Props) { )} +
+

{__("Integrations")}

+ + edge.node) ?? []} + /> + +
+
. +// +// 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 connector + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/getprobo/probo/pkg/gid" +) + +type ( + SlackConnection struct { + OAuth2Connection + Settings SlackSettings `json:"settings"` + } + + SlackSettings struct { + WebhookURL string `json:"webhook_url,omitempty"` // Encrypted + Channel string `json:"channel,omitempty"` + ChannelID string `json:"channel_id,omitempty"` + } + + IncomingWebhook struct { + URL string `json:"url"` + Channel string `json:"channel"` + ChannelID string `json:"channel_id"` + } + + SlackTokenResponse struct { + IncomingWebhook *IncomingWebhook `json:"incoming_webhook,omitempty"` + } +) + +const ( + SlackProvider = "SLACK" +) + +var _ Connection = (*SlackConnection)(nil) + +func (c *SlackConnection) Type() ProtocolType { + return ProtocolOAuth2 +} + +func (c *SlackConnection) Client(ctx context.Context) (*http.Client, error) { + return c.OAuth2Connection.Client(ctx) +} + +func (c SlackConnection) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Type string `json:"type"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token,omitempty"` + ExpiresAt time.Time `json:"expires_at"` + TokenType string `json:"token_type"` + Scope string `json:"scope,omitempty"` + WebhookURL string `json:"webhook_url,omitempty"` + }{ + Type: string(ProtocolOAuth2), + AccessToken: c.OAuth2Connection.AccessToken, + RefreshToken: c.OAuth2Connection.RefreshToken, + ExpiresAt: c.OAuth2Connection.ExpiresAt, + TokenType: c.OAuth2Connection.TokenType, + Scope: c.OAuth2Connection.Scope, + WebhookURL: c.Settings.WebhookURL, + }) +} + +func (c *SlackConnection) UnmarshalJSON(data []byte) error { + aux := &struct { + Type string `json:"type"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token,omitempty"` + ExpiresAt time.Time `json:"expires_at"` + TokenType string `json:"token_type"` + Scope string `json:"scope,omitempty"` + WebhookURL string `json:"webhook_url,omitempty"` + }{} + + if err := json.Unmarshal(data, aux); err != nil { + return err + } + + c.OAuth2Connection = OAuth2Connection{ + AccessToken: aux.AccessToken, + RefreshToken: aux.RefreshToken, + ExpiresAt: aux.ExpiresAt, + TokenType: aux.TokenType, + Scope: aux.Scope, + } + c.Settings.WebhookURL = aux.WebhookURL + + return nil +} + +func ParseSlackTokenResponse(body []byte, oauth2Conn OAuth2Connection, organizationID gid.GID) (*SlackConnection, *gid.GID, error) { + var slackResponse SlackTokenResponse + var buf bytes.Buffer + buf.Write(body) + if err := json.NewDecoder(&buf).Decode(&slackResponse); err != nil { + return nil, nil, fmt.Errorf("cannot decode Slack token response: %w", err) + } + + if slackResponse.IncomingWebhook == nil { + return nil, nil, fmt.Errorf("incoming webhook is required for Slack") + } + + settings := SlackSettings{ + WebhookURL: slackResponse.IncomingWebhook.URL, + Channel: slackResponse.IncomingWebhook.Channel, + ChannelID: slackResponse.IncomingWebhook.ChannelID, + } + + return &SlackConnection{ + OAuth2Connection: oauth2Conn, + Settings: settings, + }, &organizationID, nil +} diff --git a/pkg/coredata/connector.go b/pkg/coredata/connector.go index 2aba7123a..ab5346a8b 100644 --- a/pkg/coredata/connector.go +++ b/pkg/coredata/connector.go @@ -31,33 +31,230 @@ import ( type ( Connector struct { - ID gid.GID `db:"id"` - OrganizationID gid.GID `db:"organization_id"` - Name string `db:"name"` - Type connector.ProtocolType `db:"type"` - Connection connector.Connection `db:"-"` - EncryptedConnection []byte `db:"encrypted_connection"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + ID gid.GID `db:"id"` + OrganizationID gid.GID `db:"organization_id"` + Provider ConnectorProvider `db:"provider"` + Protocol ConnectorProtocol `db:"protocol"` + Settings map[string]any `db:"settings"` + Connection connector.Connection `db:"-"` + EncryptedConnection []byte `db:"encrypted_connection"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } Connectors []*Connector ) -func (c *Connectors) LoadWithoutDecryptedConnectionByOrganizationID( +func (c *Connector) CursorKey(orderBy ConnectorOrderField) page.CursorKey { + switch orderBy { + case ConnectorOrderFieldCreatedAt: + return page.CursorKey{ID: c.ID, Value: c.CreatedAt} + case ConnectorOrderFieldProvider: + return page.CursorKey{ID: c.ID, Value: c.Provider} + } + + panic(fmt.Sprintf("unsupported order by: %s", orderBy)) +} + +func (c *Connectors) LoadByOrganizationID( ctx context.Context, conn pg.Conn, scope Scoper, organizationID gid.GID, cursor *page.Cursor[ConnectorOrderField], encryptionKey cipher.EncryptionKey, + filter *ConnectorProviderFilter, +) error { + if err := c.loadByOrganizationIDWithPagination(ctx, conn, scope, organizationID, cursor, filter); err != nil { + return fmt.Errorf("cannot load connectors by organization ID: %w", err) + } + + if err := c.decryptConnections(encryptionKey); err != nil { + return fmt.Errorf("cannot decrypt connections: %w", err) + } + + return nil +} + +func (c *Connectors) LoadAllByOrganizationID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, + encryptionKey cipher.EncryptionKey, +) error { + if err := c.loadAllByOrganizationID(ctx, conn, scope, organizationID); err != nil { + return fmt.Errorf("cannot load all connectors by organization ID: %w", err) + } + + if err := c.decryptConnections(encryptionKey); err != nil { + return fmt.Errorf("cannot decrypt connections: %w", err) + } + + return nil +} + +func (c *Connectors) LoadAllByOrganizationIDProtocolAndProvider( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, + protocol ConnectorProtocol, + provider ConnectorProvider, + encryptionKey cipher.EncryptionKey, +) error { + if err := c.loadAllByOrganizationIDProtocolAndProvider(ctx, conn, scope, organizationID, protocol, provider); err != nil { + return fmt.Errorf("cannot load all connectors by organization ID, protocol and provider: %w", err) + } + + if err := c.decryptConnections(encryptionKey); err != nil { + return fmt.Errorf("cannot decrypt connections: %w", err) + } + + return nil +} + +func (c *Connectors) LoadByOrganizationIDWithoutDecryptedConnection( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, + cursor *page.Cursor[ConnectorOrderField], + filter *ConnectorProviderFilter, +) error { + return c.loadByOrganizationIDWithPagination(ctx, conn, scope, organizationID, cursor, filter) +} + +func (c *Connectors) LoadAllByOrganizationIDWithoutDecryptedConnection( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, +) error { + return c.loadAllByOrganizationID(ctx, conn, scope, organizationID) +} + +func (c *Connector) Insert( + ctx context.Context, + conn pg.Conn, + scope Scoper, + encryptionKey cipher.EncryptionKey, +) error { + q := ` +INSERT INTO connectors ( + id, + tenant_id, + organization_id, + provider, + protocol, + settings, + encrypted_connection, + created_at, + updated_at +) VALUES ( + @id, + @tenant_id, + @organization_id, + @provider, + @protocol, + @settings, + @encrypted_connection, + @created_at, + @updated_at +) +` + + if c.Connection == nil { + return fmt.Errorf("connection is nil") + } + + c.extractSlackSettings() + + connection, err := json.Marshal(c.Connection) + if err != nil { + return fmt.Errorf("cannot marshal connection: %w", err) + } + + encryptedConnection, err := cipher.Encrypt(connection, encryptionKey) + if err != nil { + return fmt.Errorf("cannot encrypt connection: %w", err) + } + + args := pgx.StrictNamedArgs{ + "id": c.ID, + "tenant_id": scope.GetTenantID(), + "organization_id": c.OrganizationID, + "provider": c.Provider, + "protocol": c.Protocol, + "settings": c.Settings, + "encrypted_connection": encryptedConnection, + "created_at": c.CreatedAt, + "updated_at": c.UpdatedAt, + } + + _, err = conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot insert connector: %w", err) + } + + c.EncryptedConnection = encryptedConnection + c.populateSlackSettings() + + return nil +} + +func (c *Connector) populateSlackSettings() { + if c.Provider != ConnectorProviderSlack { + return + } + + slackConn, ok := c.Connection.(*connector.SlackConnection) + if !ok { + return + } + + if channel, ok := c.Settings["channel"].(string); ok { + slackConn.Settings.Channel = channel + } + if channelID, ok := c.Settings["channel_id"].(string); ok { + slackConn.Settings.ChannelID = channelID + } +} + +func (c *Connector) extractSlackSettings() { + if c.Provider != ConnectorProviderSlack { + return + } + + slackConn, ok := c.Connection.(*connector.SlackConnection) + if !ok { + return + } + + c.Settings = make(map[string]any) + if slackConn.Settings.Channel != "" { + c.Settings["channel"] = slackConn.Settings.Channel + } + if slackConn.Settings.ChannelID != "" { + c.Settings["channel_id"] = slackConn.Settings.ChannelID + } +} + +func (c *Connectors) loadByOrganizationIDWithPagination( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, + cursor *page.Cursor[ConnectorOrderField], + filter *ConnectorProviderFilter, ) error { q := ` SELECT id, organization_id, - name, - type, + provider, + protocol, + settings, encrypted_connection, created_at, updated_at @@ -67,12 +264,14 @@ WHERE %s AND organization_id = @organization_id AND %s + AND %s ` - q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) + q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment()) args := pgx.StrictNamedArgs{"organization_id": organizationID} maps.Copy(args, scope.SQLArguments()) + maps.Copy(args, filter.SQLArguments()) maps.Copy(args, cursor.SQLArguments()) rows, err := conn.Query(ctx, q, args) @@ -90,110 +289,122 @@ WHERE return nil } -func (c *Connector) CursorKey(orderBy ConnectorOrderField) page.CursorKey { - switch orderBy { - case ConnectorOrderFieldCreatedAt: - return page.CursorKey{ID: c.ID, Value: c.CreatedAt} - case ConnectorOrderFieldName: - return page.CursorKey{ID: c.ID, Value: c.Name} - } - - panic(fmt.Sprintf("unsupported order by: %s", orderBy)) -} - -func (c *Connector) Upsert( +func (c *Connectors) loadAllByOrganizationID( ctx context.Context, conn pg.Conn, scope Scoper, - encryptionKey cipher.EncryptionKey, + organizationID gid.GID, ) error { q := ` -INSERT INTO - connectors ( - id, - tenant_id, - organization_id, - name, - type, - encrypted_connection, - created_at, - updated_at - ) -VALUES ( - @id, - @tenant_id, - @organization_id, - @name, - @type, - @encrypted_connection, - @created_at, - @updated_at -) -ON CONFLICT (organization_id, name) DO UPDATE SET - tenant_id = @tenant_id, - organization_id = @organization_id, - type = @type, - encrypted_connection = @encrypted_connection, - updated_at = @updated_at -RETURNING +SELECT id, organization_id, - name, - type, + provider, + protocol, + settings, encrypted_connection, created_at, updated_at +FROM + connectors +WHERE + %s + AND organization_id = @organization_id +ORDER BY + created_at ASC ` - if c.Connection == nil { - return fmt.Errorf("connection is nil") - } + q = fmt.Sprintf(q, scope.SQLFragment()) - connection, err := json.Marshal(c.Connection) + args := pgx.StrictNamedArgs{"organization_id": organizationID} + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) if err != nil { - return fmt.Errorf("cannot marshal connection: %w", err) + return fmt.Errorf("cannot query connectors: %w", err) } - encryptedConnection, err := cipher.Encrypt(connection, encryptionKey) - if err != nil { - return fmt.Errorf("cannot encrypt connection: %w", err) - } - - rows, err := conn.Query( - ctx, - q, - pgx.StrictNamedArgs{ - "id": c.ID, - "tenant_id": scope.GetTenantID(), - "organization_id": c.OrganizationID, - "name": c.Name, - "type": c.Type, - "encrypted_connection": encryptedConnection, - "created_at": c.CreatedAt, - "updated_at": c.UpdatedAt, - }, - ) - if err != nil { - return err - } - defer rows.Close() - - cnnctr, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Connector]) + connectors, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Connector]) if err != nil { return fmt.Errorf("cannot collect connectors: %w", err) } - decryptedConnection, err := cipher.Decrypt(cnnctr.EncryptedConnection, encryptionKey) - if err != nil { - return fmt.Errorf("cannot decrypt connection: %w", err) - } - - cnnctr.Connection, err = connector.UnmarshalConnection(cnnctr.Type, decryptedConnection) - if err != nil { - return fmt.Errorf("cannot unmarshal connection: %w", err) - } - - *c = cnnctr + *c = connectors + + return nil +} + +func (c *Connectors) loadAllByOrganizationIDProtocolAndProvider( + ctx context.Context, + conn pg.Conn, + scope Scoper, + organizationID gid.GID, + protocol ConnectorProtocol, + provider ConnectorProvider, +) error { + q := ` +SELECT + id, + organization_id, + provider, + protocol, + settings, + encrypted_connection, + created_at, + updated_at +FROM + connectors +WHERE + %s + AND organization_id = @organization_id + AND protocol = @protocol + AND provider = @provider +ORDER BY + created_at ASC +` + + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "organization_id": organizationID, + "protocol": protocol, + "provider": provider, + } + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query connectors: %w", err) + } + + connectors, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Connector]) + if err != nil { + return fmt.Errorf("cannot collect connectors: %w", err) + } + + *c = connectors + + return nil +} + +func (c *Connectors) decryptConnections(encryptionKey cipher.EncryptionKey) error { + for _, cnnctr := range *c { + if len(cnnctr.EncryptedConnection) == 0 { + continue + } + + decryptedConnection, err := cipher.Decrypt(cnnctr.EncryptedConnection, encryptionKey) + if err != nil { + return fmt.Errorf("cannot decrypt connection for %s: %w", cnnctr.Provider, err) + } + + cnnctr.Connection, err = connector.UnmarshalConnection(cnnctr.Protocol.String(), cnnctr.Provider.String(), decryptedConnection) + if err != nil { + return fmt.Errorf("cannot unmarshal connection for %s: %w", cnnctr.Provider, err) + } + + cnnctr.populateSlackSettings() + } return nil } diff --git a/pkg/coredata/connector_order_field.go b/pkg/coredata/connector_order_field.go index 7425563ea..b94f788da 100644 --- a/pkg/coredata/connector_order_field.go +++ b/pkg/coredata/connector_order_field.go @@ -20,7 +20,7 @@ type ( const ( ConnectorOrderFieldCreatedAt ConnectorOrderField = "CREATED_AT" - ConnectorOrderFieldName ConnectorOrderField = "NAME" + ConnectorOrderFieldProvider ConnectorOrderField = "PROVIDER" ) func (p ConnectorOrderField) Column() string { diff --git a/pkg/coredata/connector_protocol.go b/pkg/coredata/connector_protocol.go new file mode 100644 index 000000000..3929d7e9d --- /dev/null +++ b/pkg/coredata/connector_protocol.go @@ -0,0 +1,54 @@ +// 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 ConnectorProtocol string + +const ( + ConnectorProtocolOAuth2 ConnectorProtocol = "OAUTH2" +) + +func (cp ConnectorProtocol) String() string { + return string(cp) +} + +func (cp *ConnectorProtocol) 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 ConnectorProtocol: %T", value) + } + + switch s { + case "OAUTH2": + *cp = ConnectorProtocolOAuth2 + default: + return fmt.Errorf("invalid ConnectorProtocol value: %q", s) + } + return nil +} + +func (cp ConnectorProtocol) Value() (driver.Value, error) { + return cp.String(), nil +} diff --git a/pkg/coredata/connector_provider.go b/pkg/coredata/connector_provider.go new file mode 100644 index 000000000..1f3703d79 --- /dev/null +++ b/pkg/coredata/connector_provider.go @@ -0,0 +1,54 @@ +// 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 ConnectorProvider string + +const ( + ConnectorProviderSlack ConnectorProvider = "SLACK" +) + +func (cp ConnectorProvider) String() string { + return string(cp) +} + +func (cp *ConnectorProvider) 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 ConnectorProvider: %T", value) + } + + switch s { + case "SLACK": + *cp = ConnectorProviderSlack + default: + return fmt.Errorf("invalid ConnectorProvider value: %q", s) + } + return nil +} + +func (cp ConnectorProvider) Value() (driver.Value, error) { + return cp.String(), nil +} diff --git a/pkg/coredata/connector_provider_filter.go b/pkg/coredata/connector_provider_filter.go new file mode 100644 index 000000000..2d7259956 --- /dev/null +++ b/pkg/coredata/connector_provider_filter.go @@ -0,0 +1,54 @@ +// 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 ( + "github.com/jackc/pgx/v5" +) + +type ( + ConnectorProviderFilter struct { + provider *ConnectorProvider + } +) + +func NewConnectorProviderFilter(provider *ConnectorProvider) *ConnectorProviderFilter { + return &ConnectorProviderFilter{ + provider: provider, + } +} + +func (f *ConnectorProviderFilter) SQLArguments() pgx.NamedArgs { + args := pgx.NamedArgs{} + + if f.provider != nil { + args["provider"] = *f.provider + } + + return args +} + +func (f *ConnectorProviderFilter) SQLFragment() string { + return ` +( + CASE + WHEN @provider::connector_provider IS NULL THEN + TRUE + ELSE + provider = @provider::connector_provider + END +) + ` +} diff --git a/pkg/coredata/entity_type_reg.go b/pkg/coredata/entity_type_reg.go index b543ae000..8c402b035 100644 --- a/pkg/coredata/entity_type_reg.go +++ b/pkg/coredata/entity_type_reg.go @@ -61,4 +61,5 @@ const ( CustomDomainEntityType InvitationEntityType MembershipEntityType + SlackMessageEntityType ) diff --git a/pkg/coredata/migrations/20251016T174950Z.sql b/pkg/coredata/migrations/20251016T174950Z.sql new file mode 100644 index 000000000..8f8f23768 --- /dev/null +++ b/pkg/coredata/migrations/20251016T174950Z.sql @@ -0,0 +1,25 @@ +CREATE TYPE connector_protocol AS ENUM ('OAUTH2'); +CREATE TYPE connector_provider AS ENUM ('SLACK'); + +ALTER TABLE connectors DROP COLUMN type; +ALTER TABLE connectors DROP COLUMN name; + +ALTER TABLE connectors ADD COLUMN protocol connector_protocol NOT NULL; +ALTER TABLE connectors ADD COLUMN provider connector_provider NOT NULL; +ALTER TABLE connectors ADD COLUMN settings JSONB; + +DROP INDEX IF EXISTS idx_connectors_organization_id_name; + +CREATE TABLE slack_messages ( + id TEXT PRIMARY KEY, + tenant_id TEXT NOT NULL, + organization_id TEXT NOT NULL, + body TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL, + sent_at TIMESTAMP WITH TIME ZONE, + error TEXT, + CONSTRAINT fk_slack_messages_organization_id FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE +); + +CREATE INDEX ON slack_messages (sent_at) WHERE sent_at IS NULL AND error IS NULL; diff --git a/pkg/coredata/slack_message.go b/pkg/coredata/slack_message.go new file mode 100644 index 000000000..5b03ee59c --- /dev/null +++ b/pkg/coredata/slack_message.go @@ -0,0 +1,143 @@ +// 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" + "errors" + "fmt" + "time" + + "github.com/getprobo/probo/pkg/gid" + "github.com/jackc/pgx/v5" + "go.gearno.de/kit/pg" +) + +type ( + SlackMessage struct { + ID gid.GID `db:"id"` + OrganizationID gid.GID `db:"organization_id"` + Body string `db:"body"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + SentAt *time.Time `db:"sent_at"` + Error *string `db:"error"` + } + + ErrNoUnsentSlackMessage struct{} +) + +func (e ErrNoUnsentSlackMessage) Error() string { + return "no unsent slack message found" +} + +func NewSlackMessage( + scope Scoper, + organizationID gid.GID, + body string, +) *SlackMessage { + now := time.Now() + return &SlackMessage{ + ID: gid.New(scope.GetTenantID(), SlackMessageEntityType), + OrganizationID: organizationID, + Body: body, + CreatedAt: now, + UpdatedAt: now, + } +} + +func (s *SlackMessage) Insert( + ctx context.Context, + conn pg.Conn, + scope Scoper, +) error { + q := ` +INSERT INTO slack_messages (id, tenant_id, organization_id, body, created_at, updated_at) +VALUES (@id, @tenant_id, @organization_id, @body, @created_at, @updated_at) + ` + + args := pgx.StrictNamedArgs{ + "id": s.ID, + "tenant_id": scope.GetTenantID(), + "organization_id": s.OrganizationID, + "body": s.Body, + "created_at": s.CreatedAt, + "updated_at": s.UpdatedAt, + } + + _, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot insert slack message: %w", err) + } + + return nil +} + +func (s *SlackMessage) LoadNextUnsentForUpdate( + ctx context.Context, + conn pg.Conn, +) error { + q := ` +SELECT id, organization_id, body, created_at, updated_at, sent_at, error +FROM slack_messages +WHERE sent_at IS NULL AND error IS NULL +ORDER BY created_at ASC +LIMIT 1 +FOR UPDATE + ` + + rows, err := conn.Query(ctx, q) + if err != nil { + return fmt.Errorf("cannot query slack messages: %w", err) + } + + message, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[SlackMessage]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrNoUnsentSlackMessage{} + } + + return fmt.Errorf("cannot collect slack message: %w", err) + } + + *s = message + + return nil +} + +func (s *SlackMessage) Update( + ctx context.Context, + conn pg.Conn, +) error { + q := ` +UPDATE slack_messages +SET sent_at = @sent_at, updated_at = @updated_at, error = @error +WHERE id = @id + ` + + args := pgx.StrictNamedArgs{ + "id": s.ID, + "sent_at": s.SentAt, + "updated_at": s.UpdatedAt, + "error": s.Error, + } + + _, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot update slack message: %w", err) + } + + return nil +} diff --git a/pkg/probo/connector_service.go b/pkg/probo/connector_service.go index f8d7a8cd7..9c7013849 100644 --- a/pkg/probo/connector_service.go +++ b/pkg/probo/connector_service.go @@ -15,8 +15,10 @@ package probo import ( + "bytes" "context" "fmt" + "text/template" "time" "github.com/getprobo/probo/pkg/connector" @@ -26,15 +28,19 @@ import ( "go.gearno.de/kit/pg" ) +var ( + welcomeTemplate = template.Must(template.ParseFS(Templates, "templates/welcome.txt.tmpl")) +) + type ( ConnectorService struct { svc *TenantService } - CreateOrUpdateConnectorRequest struct { + CreateConnectorRequest struct { OrganizationID gid.GID - Name string - Type connector.ProtocolType + Provider coredata.ConnectorProvider + Protocol coredata.ConnectorProtocol Connection connector.Connection } ) @@ -43,19 +49,20 @@ func (s *ConnectorService) ListForOrganizationID( ctx context.Context, organizationID gid.GID, cursor *page.Cursor[coredata.ConnectorOrderField], + filter *coredata.ConnectorProviderFilter, ) (*page.Page[*coredata.Connector, coredata.ConnectorOrderField], error) { var connectors coredata.Connectors err := s.svc.pg.WithConn( ctx, func(conn pg.Conn) error { - return connectors.LoadWithoutDecryptedConnectionByOrganizationID( + return connectors.LoadByOrganizationIDWithoutDecryptedConnection( ctx, conn, s.svc.scope, organizationID, cursor, - s.svc.encryptionKey, + filter, ) }, ) @@ -67,34 +74,34 @@ func (s *ConnectorService) ListForOrganizationID( return page.NewPage(connectors, cursor), nil } -func (s *ConnectorService) CreateOrUpdate( +func (s *ConnectorService) Create( ctx context.Context, - req CreateOrUpdateConnectorRequest, + req CreateConnectorRequest, ) (*coredata.Connector, error) { if req.OrganizationID == gid.Nil { return nil, fmt.Errorf("organization ID is required") } - if req.Name == "" { - return nil, fmt.Errorf("connector name is required") + if req.Provider == "" { + return nil, fmt.Errorf("connector provider is required") } - if req.Type == "" { - return nil, fmt.Errorf("connector type is required") + if req.Protocol == "" { + return nil, fmt.Errorf("connector protocol is required") } if req.Connection == nil { return nil, fmt.Errorf("connection configuration is required") } - connectorID := gid.New(s.svc.scope.GetTenantID(), coredata.ConnectorEntityType) + id := gid.New(s.svc.scope.GetTenantID(), coredata.ConnectorEntityType) now := time.Now() - connector := &coredata.Connector{ - ID: connectorID, + newConnector := &coredata.Connector{ + ID: id, OrganizationID: req.OrganizationID, - Name: req.Name, - Type: req.Type, + Provider: req.Provider, + Protocol: req.Protocol, Connection: req.Connection, CreatedAt: now, UpdatedAt: now, @@ -103,8 +110,36 @@ func (s *ConnectorService) CreateOrUpdate( err := s.svc.pg.WithConn( ctx, func(conn pg.Conn) error { - if err := connector.Upsert(ctx, conn, s.svc.scope, s.svc.encryptionKey); err != nil { - return fmt.Errorf("cannot upsert connector: %w", err) + if err := newConnector.Insert(ctx, conn, s.svc.scope, s.svc.encryptionKey); err != nil { + return fmt.Errorf("cannot create connector: %w", err) + } + + if req.Provider == coredata.ConnectorProviderSlack { + slackConn, ok := req.Connection.(*connector.SlackConnection) + if ok && slackConn.Settings.Channel != "" { + var organization coredata.Organization + if err := organization.LoadByID(ctx, conn, s.svc.scope, req.OrganizationID); err != nil { + return fmt.Errorf("cannot load organization: %w", err) + } + + data := struct { + OrganizationName string + ChannelName string + }{ + OrganizationName: organization.Name, + ChannelName: slackConn.Settings.Channel, + } + + var buf bytes.Buffer + if err := welcomeTemplate.Execute(&buf, data); err != nil { + return fmt.Errorf("failed to execute template: %w", err) + } + + slackMessage := coredata.NewSlackMessage(s.svc.scope, req.OrganizationID, buf.String()) + if err := slackMessage.Insert(ctx, conn, s.svc.scope); err != nil { + return fmt.Errorf("cannot insert slack message: %w", err) + } + } } return nil @@ -115,5 +150,5 @@ func (s *ConnectorService) CreateOrUpdate( return nil, err } - return connector, nil + return newConnector, nil } diff --git a/pkg/probo/templates.go b/pkg/probo/templates.go new file mode 100644 index 000000000..e7ef4243a --- /dev/null +++ b/pkg/probo/templates.go @@ -0,0 +1,24 @@ +// 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 ( + "embed" +) + +var ( + //go:embed templates/*.tmpl + Templates embed.FS +) diff --git a/pkg/probo/templates/welcome.txt.tmpl b/pkg/probo/templates/welcome.txt.tmpl new file mode 100644 index 000000000..e2ff0dd96 --- /dev/null +++ b/pkg/probo/templates/welcome.txt.tmpl @@ -0,0 +1,8 @@ +*Welcome to Probo app!* + +This channel is now connected to your Probo platform. You'll receive notifications here for: +• New trust center access requests + +*Organization:* {{.OrganizationName}} +*Channel:* {{.ChannelName}} + diff --git a/pkg/probod/connector_config.go b/pkg/probod/connector_config.go index aaeb57784..661230aab 100644 --- a/pkg/probod/connector_config.go +++ b/pkg/probod/connector_config.go @@ -17,15 +17,16 @@ package probod import ( "encoding/json" "fmt" + "strings" "github.com/getprobo/probo/pkg/connector" ) type ( connectorConfig struct { - Name string `json:"name"` - Type connector.ProtocolType `json:"type"` - Config connector.Connector `json:"-"` + Provider string `json:"provider"` + Protocol connector.ProtocolType `json:"protocol"` + Config connector.Connector `json:"-"` } connectorConfigOAuth2 struct { @@ -40,19 +41,19 @@ type ( func (c *connectorConfig) UnmarshalJSON(data []byte) error { var tmp struct { - Name string `json:"name"` - Type connector.ProtocolType `json:"type"` - RawConfig json.RawMessage `json:"config"` + Provider string `json:"provider"` + Protocol string `json:"protocol"` + RawConfig json.RawMessage `json:"config"` } if err := json.Unmarshal(data, &tmp); err != nil { return fmt.Errorf("cannot unmarshal connector config: %w", err) } - c.Name = tmp.Name - c.Type = tmp.Type + c.Provider = strings.ToUpper(tmp.Provider) + c.Protocol = connector.ProtocolType(strings.ToUpper(tmp.Protocol)) - switch tmp.Type { + switch c.Protocol { case connector.ProtocolOAuth2: var config connectorConfigOAuth2 if err := json.Unmarshal(tmp.RawConfig, &config); err != nil { @@ -70,7 +71,7 @@ func (c *connectorConfig) UnmarshalJSON(data []byte) error { c.Config = &oauth2Connector default: - return fmt.Errorf("unknown connector type: %q", tmp.Type) + return fmt.Errorf("unknown connector protocol: %q", c.Protocol) } return nil diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index 8add8a665..45dcf9d44 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -45,6 +45,7 @@ import ( "github.com/getprobo/probo/pkg/saferedirect" "github.com/getprobo/probo/pkg/server" "github.com/getprobo/probo/pkg/server/api" + "github.com/getprobo/probo/pkg/slack" "github.com/getprobo/probo/pkg/trust" "github.com/prometheus/client_golang/prometheus" "go.gearno.de/kit/httpclient" @@ -72,6 +73,7 @@ type ( TrustCenter trustCenterConfig `json:"trust-center"` AWS awsConfig `json:"aws"` Mailer mailerConfig `json:"mailer"` + Slack slackConfig `json:"slack"` Connectors []connectorConfig `json:"connectors"` OpenAI openaiConfig `json:"openai"` ChromeDPAddr string `json:"chrome-dp-addr"` @@ -143,6 +145,9 @@ func New() *Implm { Addr: "localhost:1025", }, }, + Slack: slackConfig{ + SenderInterval: 60, + }, CustomDomains: customDomainsConfig{ RenewalInterval: 3600, ProvisionInterval: 30, @@ -204,6 +209,7 @@ func (impl *Implm) Run( return fmt.Errorf("cannot get trust auth token secret bytes: %w", err) } + awsConfig := awsconfig.NewConfig( l, httpclient.DefaultPooledClient( @@ -239,7 +245,7 @@ func (impl *Implm) Run( defaultConnectorRegistry := connector.NewConnectorRegistry() for _, connector := range impl.cfg.Connectors { - if err := defaultConnectorRegistry.Register(connector.Name, connector.Config); err != nil { + if err := defaultConnectorRegistry.Register(connector.Provider, connector.Config); err != nil { return fmt.Errorf("cannot register connector: %w", err) } } @@ -338,6 +344,7 @@ func (impl *Implm) Run( pgClient, s3Client, impl.cfg.AWS.Bucket, + impl.cfg.Hostname, impl.cfg.EncryptionKey, impl.cfg.TrustAuth.TokenSecret, authService, @@ -408,6 +415,18 @@ func (impl *Implm) Run( }, ) + slackSenderCtx, stopSlackSender := context.WithCancel(context.Background()) + slackSender := slack.NewSender(pgClient, l.Named("slack-sender"), impl.cfg.EncryptionKey, slack.Config{ + Interval: time.Duration(impl.cfg.Slack.SenderInterval) * time.Second, + }) + wg.Go( + func() { + if err := slackSender.Run(slackSenderCtx); err != nil { + cancel(fmt.Errorf("slack sender crashed: %w", err)) + } + }, + ) + exportJobExporterCtx, stopExportJobExporter := context.WithCancel(context.Background()) wg.Go( func() { @@ -430,6 +449,7 @@ func (impl *Implm) Run( <-ctx.Done() stopMailer() + stopSlackSender() stopExportJobExporter() stopApiServer() stopTrustCenterServer() diff --git a/pkg/probod/slack_config.go b/pkg/probod/slack_config.go new file mode 100644 index 000000000..2739371bd --- /dev/null +++ b/pkg/probod/slack_config.go @@ -0,0 +1,21 @@ +// 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 probod + +type ( + slackConfig struct { + SenderInterval int `json:"sender-interval"` + } +) diff --git a/pkg/server/api/console/v1/resolver.go b/pkg/server/api/console/v1/resolver.go index e16b95424..ec7c15529 100644 --- a/pkg/server/api/console/v1/resolver.go +++ b/pkg/server/api/console/v1/resolver.go @@ -44,6 +44,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/vektah/gqlparser/v2/gqlerror" "go.gearno.de/crypto/uuid" + "go.gearno.de/kit/httpserver" "go.gearno.de/kit/log" ) @@ -218,7 +219,12 @@ func NewMux( r.Post("/auth/reset-password", ResetPasswordHandler(authSvc, authCfg)) r.Get("/connectors/initiate", WithSession(authSvc, authzSvc, authCfg, func(w http.ResponseWriter, r *http.Request) { - connectorID := r.URL.Query().Get("connector_id") + provider := r.URL.Query().Get("provider") + if provider != "SLACK" { + httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("unsupported provider")) + return + } + organizationID, err := gid.ParseGID(r.URL.Query().Get("organization_id")) if err != nil { panic(fmt.Errorf("failed to parse organization id: %w", err)) @@ -226,34 +232,53 @@ func NewMux( _ = GetTenantService(r.Context(), proboSvc, organizationID.TenantID()) - redirectURL, err := connectorRegistry.Initiate(r.Context(), connectorID, organizationID, r) + redirectURL, err := connectorRegistry.Initiate(r.Context(), provider, organizationID, r) if err != nil { panic(fmt.Errorf("cannot initiate connector: %w", err)) } - http.Redirect(w, r, redirectURL, http.StatusSeeOther) + // Allow external redirects for Slack OAuth only for now + slackSafeRedirect := &saferedirect.SafeRedirect{AllowedHost: "slack.com"} + slackSafeRedirect.Redirect(w, r, redirectURL, "/", http.StatusSeeOther) })) - r.Get("/connectors/complete", WithSession(authSvc, authzSvc, authCfg, func(w http.ResponseWriter, r *http.Request) { - connectorID := r.URL.Query().Get("connector_id") - organizationID, err := gid.ParseGID(r.URL.Query().Get("organization_id")) - if err != nil { - panic(fmt.Errorf("failed to parse organization id: %w", err)) + r.Get("/connectors/complete", func(w http.ResponseWriter, r *http.Request) { + provider := r.URL.Query().Get("provider") + if provider == "" { + httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("missing provider parameter")) + return } - connection, err := connectorRegistry.Complete(r.Context(), connectorID, organizationID, r) + var connectorProvider coredata.ConnectorProvider + switch provider { + case "SLACK": + connectorProvider = coredata.ConnectorProviderSlack + default: + httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("unsupported provider")) + return + } + + stateToken := r.URL.Query().Get("state") + if stateToken == "" { + httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("missing state parameter")) + return + } + + connection, organizationID, err := connectorRegistry.Complete(r.Context(), provider, r) if err != nil { panic(fmt.Errorf("failed to complete connector: %w", err)) } - svc := GetTenantService(r.Context(), proboSvc, organizationID.TenantID()) + continueURL := r.URL.Query().Get("continue") - _, err = svc.Connectors.CreateOrUpdate( + svc := proboSvc.WithTenant(organizationID.TenantID()) + + _, err = svc.Connectors.Create( r.Context(), - probo.CreateOrUpdateConnectorRequest{ - OrganizationID: organizationID, - Name: connectorID, - Type: connector.ProtocolType(connection.Type()), + probo.CreateConnectorRequest{ + OrganizationID: *organizationID, + Provider: connectorProvider, + Protocol: coredata.ConnectorProtocol(connection.Type()), Connection: connection, }, ) @@ -261,8 +286,13 @@ func NewMux( panic(fmt.Errorf("failed to create or update connector: %w", err)) } - safeRedirect.RedirectFromQuery(w, r, "continue", "/", http.StatusSeeOther) - })) + if continueURL != "" { + safeRedirect.Redirect(w, r, continueURL, "/", http.StatusSeeOther) + } else { + redirectURL := fmt.Sprintf("/organizations/%s", organizationID.String()) + safeRedirect.Redirect(w, r, redirectURL, "/", http.StatusSeeOther) + } + }) r.Get("/", playground.Handler("GraphQL", "/api/console/v1/query")) r.Post("/query", graphqlHandler(logger, proboSvc, authSvc, authzSvc, authCfg, customDomainCname)) diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index d29caabf1..28bef2a77 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -555,20 +555,6 @@ enum OrganizationOrderField ) } -enum ConnectorOrderField - @goModel( - model: "github.com/getprobo/probo/pkg/coredata.ConnectorOrderField" - ) { - CREATED_AT - @goEnum( - value: "github.com/getprobo/probo/pkg/coredata.ConnectorOrderFieldCreatedAt" - ) - NAME - @goEnum( - value: "github.com/getprobo/probo/pkg/coredata.ConnectorOrderFieldName" - ) -} - enum DataSensitivity @goModel(model: "github.com/getprobo/probo/pkg/coredata.DataSensitivity") { NONE @@ -1473,11 +1459,6 @@ input OrganizationOrder { field: OrganizationOrderField! } -input ConnectorOrder { - field: ConnectorOrderField! - direction: OrderDirection! -} - input DocumentVersionOrder @goModel( model: "github.com/getprobo/probo/pkg/server/api/console/v1/types.DocumentVersionOrderBy" @@ -1621,13 +1602,12 @@ type Organization implements Node { filter: InvitationFilter ): InvitationConnection! @goField(forceResolver: true) - connectors( + slackConnections( first: Int after: CursorKey last: Int before: CursorKey - orderBy: ConnectorOrder - ): ConnectorConnection! @goField(forceResolver: true) + ): SlackConnectionConnection! @goField(forceResolver: true) frameworks( first: Int @@ -1808,14 +1788,24 @@ type Invitation implements Node { organization: Organization! @goField(forceResolver: true) } -type Connector implements Node { +type SlackConnection { id: ID! - name: String! - type: String! + channel: String + channelId: String createdAt: Datetime! updatedAt: Datetime! } +type SlackConnectionConnection { + edges: [SlackConnectionEdge!]! + pageInfo: PageInfo! +} + +type SlackConnectionEdge { + cursor: CursorKey! + node: SlackConnection! +} + type People implements Node { id: ID! fullName: String! @@ -2635,15 +2625,6 @@ type VendorServiceEdge { node: VendorService! } -type ConnectorConnection { - edges: [ConnectorEdge!]! - pageInfo: PageInfo! -} - -type ConnectorEdge { - cursor: CursorKey! - node: Connector! -} type VendorRiskAssessmentConnection { edges: [VendorRiskAssessmentEdge!]! diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index 510ecc0c6..322f82eb7 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -197,24 +197,6 @@ type ComplexityRoot struct { Success func(childComplexity int) int } - Connector struct { - CreatedAt func(childComplexity int) int - ID func(childComplexity int) int - Name func(childComplexity int) int - Type func(childComplexity int) int - UpdatedAt func(childComplexity int) int - } - - ConnectorConnection struct { - Edges func(childComplexity int) int - PageInfo func(childComplexity int) int - } - - ConnectorEdge struct { - Cursor func(childComplexity int) int - Node func(childComplexity int) int - } - ContinualImprovement struct { CreatedAt func(childComplexity int) int Description func(childComplexity int) int @@ -1022,7 +1004,6 @@ type ComplexityRoot struct { Organization struct { Assets func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AssetOrderBy, filter *types.AssetFilter) int Audits func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AuditOrderBy) int - Connectors func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ConnectorOrder) int ContinualImprovements func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ContinualImprovementOrderBy, filter *types.ContinualImprovementFilter) int Controls func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) int CreatedAt func(childComplexity int) int @@ -1045,6 +1026,7 @@ type ComplexityRoot struct { Peoples func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.PeopleOrderBy, filter *types.PeopleFilter) int ProcessingActivities func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ProcessingActivityOrderBy, filter *types.ProcessingActivityFilter) int Risks func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.RiskOrderBy, filter *types.RiskFilter) int + SlackConnections func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int Snapshots func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SnapshotOrderBy) int Tasks func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.TaskOrderBy) int TrustCenter func(childComplexity int) int @@ -1207,6 +1189,24 @@ type ComplexityRoot struct { ID func(childComplexity int) int } + SlackConnection struct { + Channel func(childComplexity int) int + ChannelID func(childComplexity int) int + CreatedAt func(childComplexity int) int + ID func(childComplexity int) int + UpdatedAt func(childComplexity int) int + } + + SlackConnectionConnection struct { + Edges func(childComplexity int) int + PageInfo func(childComplexity int) int + } + + SlackConnectionEdge struct { + Cursor func(childComplexity int) int + Node func(childComplexity int) int + } + Snapshot struct { Controls func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) int CreatedAt func(childComplexity int) int @@ -1892,7 +1892,7 @@ type OrganizationResolver interface { Memberships(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MembershipOrderBy) (*types.MembershipConnection, error) Invitations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.InvitationOrder, filter *types.InvitationFilter) (*types.InvitationConnection, error) - Connectors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ConnectorOrder) (*types.ConnectorConnection, error) + SlackConnections(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.SlackConnectionConnection, error) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.FrameworkOrderBy) (*types.FrameworkConnection, error) Controls(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) (*types.ControlConnection, error) Vendors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy, filter *types.VendorFilter) (*types.VendorConnection, error) @@ -2365,69 +2365,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.ConfirmEmailPayload.Success(childComplexity), true - case "Connector.createdAt": - if e.complexity.Connector.CreatedAt == nil { - break - } - - return e.complexity.Connector.CreatedAt(childComplexity), true - - case "Connector.id": - if e.complexity.Connector.ID == nil { - break - } - - return e.complexity.Connector.ID(childComplexity), true - - case "Connector.name": - if e.complexity.Connector.Name == nil { - break - } - - return e.complexity.Connector.Name(childComplexity), true - - case "Connector.type": - if e.complexity.Connector.Type == nil { - break - } - - return e.complexity.Connector.Type(childComplexity), true - - case "Connector.updatedAt": - if e.complexity.Connector.UpdatedAt == nil { - break - } - - return e.complexity.Connector.UpdatedAt(childComplexity), true - - case "ConnectorConnection.edges": - if e.complexity.ConnectorConnection.Edges == nil { - break - } - - return e.complexity.ConnectorConnection.Edges(childComplexity), true - - case "ConnectorConnection.pageInfo": - if e.complexity.ConnectorConnection.PageInfo == nil { - break - } - - return e.complexity.ConnectorConnection.PageInfo(childComplexity), true - - case "ConnectorEdge.cursor": - if e.complexity.ConnectorEdge.Cursor == nil { - break - } - - return e.complexity.ConnectorEdge.Cursor(childComplexity), true - - case "ConnectorEdge.node": - if e.complexity.ConnectorEdge.Node == nil { - break - } - - return e.complexity.ConnectorEdge.Node(childComplexity), true - case "ContinualImprovement.createdAt": if e.complexity.ContinualImprovement.CreatedAt == nil { break @@ -6204,18 +6141,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Organization.Audits(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.AuditOrderBy)), true - case "Organization.connectors": - if e.complexity.Organization.Connectors == nil { - break - } - - args, err := ec.field_Organization_connectors_args(ctx, rawArgs) - if err != nil { - return 0, false - } - - return e.complexity.Organization.Connectors(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.ConnectorOrder)), true - case "Organization.continualImprovements": if e.complexity.Organization.ContinualImprovements == nil { break @@ -6435,6 +6360,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Organization.Risks(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.RiskOrderBy), args["filter"].(*types.RiskFilter)), true + case "Organization.slackConnections": + if e.complexity.Organization.SlackConnections == nil { + break + } + + args, err := ec.field_Organization_slackConnections_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Organization.SlackConnections(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey)), true + case "Organization.snapshots": if e.complexity.Organization.Snapshots == nil { break @@ -7175,6 +7112,69 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Session.ID(childComplexity), true + case "SlackConnection.channel": + if e.complexity.SlackConnection.Channel == nil { + break + } + + return e.complexity.SlackConnection.Channel(childComplexity), true + + case "SlackConnection.channelId": + if e.complexity.SlackConnection.ChannelID == nil { + break + } + + return e.complexity.SlackConnection.ChannelID(childComplexity), true + + case "SlackConnection.createdAt": + if e.complexity.SlackConnection.CreatedAt == nil { + break + } + + return e.complexity.SlackConnection.CreatedAt(childComplexity), true + + case "SlackConnection.id": + if e.complexity.SlackConnection.ID == nil { + break + } + + return e.complexity.SlackConnection.ID(childComplexity), true + + case "SlackConnection.updatedAt": + if e.complexity.SlackConnection.UpdatedAt == nil { + break + } + + return e.complexity.SlackConnection.UpdatedAt(childComplexity), true + + case "SlackConnectionConnection.edges": + if e.complexity.SlackConnectionConnection.Edges == nil { + break + } + + return e.complexity.SlackConnectionConnection.Edges(childComplexity), true + + case "SlackConnectionConnection.pageInfo": + if e.complexity.SlackConnectionConnection.PageInfo == nil { + break + } + + return e.complexity.SlackConnectionConnection.PageInfo(childComplexity), true + + case "SlackConnectionEdge.cursor": + if e.complexity.SlackConnectionEdge.Cursor == nil { + break + } + + return e.complexity.SlackConnectionEdge.Cursor(childComplexity), true + + case "SlackConnectionEdge.node": + if e.complexity.SlackConnectionEdge.Node == nil { + break + } + + return e.complexity.SlackConnectionEdge.Node(childComplexity), true + case "Snapshot.controls": if e.complexity.Snapshot.Controls == nil { break @@ -8811,7 +8811,6 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputBulkRequestSignaturesInput, ec.unmarshalInputCancelSignatureRequestInput, ec.unmarshalInputConfirmEmailInput, - ec.unmarshalInputConnectorOrder, ec.unmarshalInputContinualImprovementFilter, ec.unmarshalInputContinualImprovementOrder, ec.unmarshalInputControlFilter, @@ -9622,20 +9621,6 @@ enum OrganizationOrderField ) } -enum ConnectorOrderField - @goModel( - model: "github.com/getprobo/probo/pkg/coredata.ConnectorOrderField" - ) { - CREATED_AT - @goEnum( - value: "github.com/getprobo/probo/pkg/coredata.ConnectorOrderFieldCreatedAt" - ) - NAME - @goEnum( - value: "github.com/getprobo/probo/pkg/coredata.ConnectorOrderFieldName" - ) -} - enum DataSensitivity @goModel(model: "github.com/getprobo/probo/pkg/coredata.DataSensitivity") { NONE @@ -10540,11 +10525,6 @@ input OrganizationOrder { field: OrganizationOrderField! } -input ConnectorOrder { - field: ConnectorOrderField! - direction: OrderDirection! -} - input DocumentVersionOrder @goModel( model: "github.com/getprobo/probo/pkg/server/api/console/v1/types.DocumentVersionOrderBy" @@ -10688,13 +10668,12 @@ type Organization implements Node { filter: InvitationFilter ): InvitationConnection! @goField(forceResolver: true) - connectors( + slackConnections( first: Int after: CursorKey last: Int before: CursorKey - orderBy: ConnectorOrder - ): ConnectorConnection! @goField(forceResolver: true) + ): SlackConnectionConnection! @goField(forceResolver: true) frameworks( first: Int @@ -10875,14 +10854,24 @@ type Invitation implements Node { organization: Organization! @goField(forceResolver: true) } -type Connector implements Node { +type SlackConnection { id: ID! - name: String! - type: String! + channel: String + channelId: String createdAt: Datetime! updatedAt: Datetime! } +type SlackConnectionConnection { + edges: [SlackConnectionEdge!]! + pageInfo: PageInfo! +} + +type SlackConnectionEdge { + cursor: CursorKey! + node: SlackConnection! +} + type People implements Node { id: ID! fullName: String! @@ -11702,15 +11691,6 @@ type VendorServiceEdge { node: VendorService! } -type ConnectorConnection { - edges: [ConnectorEdge!]! - pageInfo: PageInfo! -} - -type ConnectorEdge { - cursor: CursorKey! - node: Connector! -} type VendorRiskAssessmentConnection { edges: [VendorRiskAssessmentEdge!]! @@ -18317,101 +18297,6 @@ func (ec *executionContext) field_Organization_audits_argsOrderBy( return zeroVal, nil } -func (ec *executionContext) field_Organization_connectors_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { - var err error - args := map[string]any{} - arg0, err := ec.field_Organization_connectors_argsFirst(ctx, rawArgs) - if err != nil { - return nil, err - } - args["first"] = arg0 - arg1, err := ec.field_Organization_connectors_argsAfter(ctx, rawArgs) - if err != nil { - return nil, err - } - args["after"] = arg1 - arg2, err := ec.field_Organization_connectors_argsLast(ctx, rawArgs) - if err != nil { - return nil, err - } - args["last"] = arg2 - arg3, err := ec.field_Organization_connectors_argsBefore(ctx, rawArgs) - if err != nil { - return nil, err - } - args["before"] = arg3 - arg4, err := ec.field_Organization_connectors_argsOrderBy(ctx, rawArgs) - if err != nil { - return nil, err - } - args["orderBy"] = arg4 - return args, nil -} -func (ec *executionContext) field_Organization_connectors_argsFirst( - ctx context.Context, - rawArgs map[string]any, -) (*int, error) { - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("first")) - if tmp, ok := rawArgs["first"]; ok { - return ec.unmarshalOInt2ᚖint(ctx, tmp) - } - - var zeroVal *int - return zeroVal, nil -} - -func (ec *executionContext) field_Organization_connectors_argsAfter( - ctx context.Context, - rawArgs map[string]any, -) (*page.CursorKey, error) { - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) - if tmp, ok := rawArgs["after"]; ok { - return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) - } - - var zeroVal *page.CursorKey - return zeroVal, nil -} - -func (ec *executionContext) field_Organization_connectors_argsLast( - ctx context.Context, - rawArgs map[string]any, -) (*int, error) { - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("last")) - if tmp, ok := rawArgs["last"]; ok { - return ec.unmarshalOInt2ᚖint(ctx, tmp) - } - - var zeroVal *int - return zeroVal, nil -} - -func (ec *executionContext) field_Organization_connectors_argsBefore( - ctx context.Context, - rawArgs map[string]any, -) (*page.CursorKey, error) { - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) - if tmp, ok := rawArgs["before"]; ok { - return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) - } - - var zeroVal *page.CursorKey - return zeroVal, nil -} - -func (ec *executionContext) field_Organization_connectors_argsOrderBy( - ctx context.Context, - rawArgs map[string]any, -) (*types.ConnectorOrder, error) { - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("orderBy")) - if tmp, ok := rawArgs["orderBy"]; ok { - return ec.unmarshalOConnectorOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐConnectorOrder(ctx, tmp) - } - - var zeroVal *types.ConnectorOrder - return zeroVal, nil -} - func (ec *executionContext) field_Organization_continualImprovements_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -19845,6 +19730,83 @@ func (ec *executionContext) field_Organization_risks_argsFilter( return zeroVal, nil } +func (ec *executionContext) field_Organization_slackConnections_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Organization_slackConnections_argsFirst(ctx, rawArgs) + if err != nil { + return nil, err + } + args["first"] = arg0 + arg1, err := ec.field_Organization_slackConnections_argsAfter(ctx, rawArgs) + if err != nil { + return nil, err + } + args["after"] = arg1 + arg2, err := ec.field_Organization_slackConnections_argsLast(ctx, rawArgs) + if err != nil { + return nil, err + } + args["last"] = arg2 + arg3, err := ec.field_Organization_slackConnections_argsBefore(ctx, rawArgs) + if err != nil { + return nil, err + } + args["before"] = arg3 + return args, nil +} +func (ec *executionContext) field_Organization_slackConnections_argsFirst( + ctx context.Context, + rawArgs map[string]any, +) (*int, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("first")) + if tmp, ok := rawArgs["first"]; ok { + return ec.unmarshalOInt2ᚖint(ctx, tmp) + } + + var zeroVal *int + return zeroVal, nil +} + +func (ec *executionContext) field_Organization_slackConnections_argsAfter( + ctx context.Context, + rawArgs map[string]any, +) (*page.CursorKey, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) + if tmp, ok := rawArgs["after"]; ok { + return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) + } + + var zeroVal *page.CursorKey + return zeroVal, nil +} + +func (ec *executionContext) field_Organization_slackConnections_argsLast( + ctx context.Context, + rawArgs map[string]any, +) (*int, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("last")) + if tmp, ok := rawArgs["last"]; ok { + return ec.unmarshalOInt2ᚖint(ctx, tmp) + } + + var zeroVal *int + return zeroVal, nil +} + +func (ec *executionContext) field_Organization_slackConnections_argsBefore( + ctx context.Context, + rawArgs map[string]any, +) (*page.CursorKey, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) + if tmp, ok := rawArgs["before"]; ok { + return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) + } + + var zeroVal *page.CursorKey + return zeroVal, nil +} + func (ec *executionContext) field_Organization_snapshots_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -22448,8 +22410,8 @@ func (ec *executionContext) fieldContext_Asset_organization(_ context.Context, f return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -23059,8 +23021,8 @@ func (ec *executionContext) fieldContext_Audit_organization(_ context.Context, f return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -24181,430 +24143,6 @@ func (ec *executionContext) fieldContext_ConfirmEmailPayload_success(_ context.C return fc, nil } -func (ec *executionContext) _Connector_id(ctx context.Context, field graphql.CollectedField, obj *types.Connector) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Connector_id(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.ID, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(gid.GID) - fc.Result = res - return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_Connector_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Connector", - 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) _Connector_name(ctx context.Context, field graphql.CollectedField, obj *types.Connector) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Connector_name(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Name, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_Connector_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Connector", - 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) _Connector_type(ctx context.Context, field graphql.CollectedField, obj *types.Connector) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Connector_type(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Type, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_Connector_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Connector", - 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) _Connector_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Connector) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Connector_createdAt(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.CreatedAt, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(time.Time) - fc.Result = res - return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_Connector_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Connector", - 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) _Connector_updatedAt(ctx context.Context, field graphql.CollectedField, obj *types.Connector) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Connector_updatedAt(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.UpdatedAt, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(time.Time) - fc.Result = res - return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_Connector_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Connector", - 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) _ConnectorConnection_edges(ctx context.Context, field graphql.CollectedField, obj *types.ConnectorConnection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_ConnectorConnection_edges(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Edges, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.([]*types.ConnectorEdge) - fc.Result = res - return ec.marshalNConnectorEdge2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐConnectorEdgeᚄ(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_ConnectorConnection_edges(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "ConnectorConnection", - 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_ConnectorEdge_cursor(ctx, field) - case "node": - return ec.fieldContext_ConnectorEdge_node(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type ConnectorEdge", field.Name) - }, - } - return fc, nil -} - -func (ec *executionContext) _ConnectorConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *types.ConnectorConnection) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_ConnectorConnection_pageInfo(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.PageInfo, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*types.PageInfo) - fc.Result = res - return ec.marshalNPageInfo2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐPageInfo(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_ConnectorConnection_pageInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "ConnectorConnection", - 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) _ConnectorEdge_cursor(ctx context.Context, field graphql.CollectedField, obj *types.ConnectorEdge) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_ConnectorEdge_cursor(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Cursor, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(page.CursorKey) - fc.Result = res - return ec.marshalNCursorKey2githubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_ConnectorEdge_cursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "ConnectorEdge", - 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) _ConnectorEdge_node(ctx context.Context, field graphql.CollectedField, obj *types.ConnectorEdge) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_ConnectorEdge_node(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Node, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(*types.Connector) - fc.Result = res - return ec.marshalNConnector2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐConnector(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_ConnectorEdge_node(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "ConnectorEdge", - 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_Connector_id(ctx, field) - case "name": - return ec.fieldContext_Connector_name(ctx, field) - case "type": - return ec.fieldContext_Connector_type(ctx, field) - case "createdAt": - return ec.fieldContext_Connector_createdAt(ctx, field) - case "updatedAt": - return ec.fieldContext_Connector_updatedAt(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type Connector", field.Name) - }, - } - return fc, nil -} - func (ec *executionContext) _ContinualImprovement_id(ctx context.Context, field graphql.CollectedField, obj *types.ContinualImprovement) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ContinualImprovement_id(ctx, field) if err != nil { @@ -24790,8 +24328,8 @@ func (ec *executionContext) fieldContext_ContinualImprovement_organization(_ con return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -28552,8 +28090,8 @@ func (ec *executionContext) fieldContext_CustomDomain_organization(_ context.Con return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -29455,8 +28993,8 @@ func (ec *executionContext) fieldContext_Datum_organization(_ context.Context, f return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -30947,8 +30485,8 @@ func (ec *executionContext) fieldContext_DeleteOrganizationHorizontalLogoPayload return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -32370,8 +31908,8 @@ func (ec *executionContext) fieldContext_Document_organization(_ context.Context return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -35748,8 +35286,8 @@ func (ec *executionContext) fieldContext_Framework_organization(_ context.Contex return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -36846,8 +36384,8 @@ func (ec *executionContext) fieldContext_Invitation_organization(_ context.Conte return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -45885,8 +45423,8 @@ func (ec *executionContext) fieldContext_Nonconformity_organization(_ context.Co return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -46950,8 +46488,8 @@ func (ec *executionContext) fieldContext_Obligation_organization(_ context.Conte return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -48212,8 +47750,8 @@ func (ec *executionContext) fieldContext_Organization_invitations(ctx context.Co return fc, nil } -func (ec *executionContext) _Organization_connectors(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Organization_connectors(ctx, field) +func (ec *executionContext) _Organization_slackConnections(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Organization_slackConnections(ctx, field) if err != nil { return graphql.Null } @@ -48226,7 +47764,7 @@ func (ec *executionContext) _Organization_connectors(ctx context.Context, field }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Organization().Connectors(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.ConnectorOrder)) + return ec.resolvers.Organization().SlackConnections(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey)) }) if err != nil { ec.Error(ctx, err) @@ -48238,12 +47776,12 @@ func (ec *executionContext) _Organization_connectors(ctx context.Context, field } return graphql.Null } - res := resTmp.(*types.ConnectorConnection) + res := resTmp.(*types.SlackConnectionConnection) fc.Result = res - return ec.marshalNConnectorConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐConnectorConnection(ctx, field.Selections, res) + return ec.marshalNSlackConnectionConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSlackConnectionConnection(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Organization_connectors(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Organization_slackConnections(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Organization", Field: field, @@ -48252,11 +47790,11 @@ func (ec *executionContext) fieldContext_Organization_connectors(ctx context.Con Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "edges": - return ec.fieldContext_ConnectorConnection_edges(ctx, field) + return ec.fieldContext_SlackConnectionConnection_edges(ctx, field) case "pageInfo": - return ec.fieldContext_ConnectorConnection_pageInfo(ctx, field) + return ec.fieldContext_SlackConnectionConnection_pageInfo(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type ConnectorConnection", field.Name) + return nil, fmt.Errorf("no field named %q was found under type SlackConnectionConnection", field.Name) }, } defer func() { @@ -48266,7 +47804,7 @@ func (ec *executionContext) fieldContext_Organization_connectors(ctx context.Con } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Organization_connectors_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Organization_slackConnections_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } @@ -49696,8 +49234,8 @@ func (ec *executionContext) fieldContext_OrganizationEdge_node(_ context.Context return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -50789,8 +50327,8 @@ func (ec *executionContext) fieldContext_ProcessingActivity_organization(_ conte return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -53476,8 +53014,8 @@ func (ec *executionContext) fieldContext_Risk_organization(_ context.Context, fi return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -54277,6 +53815,424 @@ func (ec *executionContext) fieldContext_Session_expiresAt(_ context.Context, fi return fc, nil } +func (ec *executionContext) _SlackConnection_id(ctx context.Context, field graphql.CollectedField, obj *types.SlackConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SlackConnection_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(gid.GID) + fc.Result = res + return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SlackConnection_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SlackConnection", + 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) _SlackConnection_channel(ctx context.Context, field graphql.CollectedField, obj *types.SlackConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SlackConnection_channel(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Channel, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SlackConnection_channel(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SlackConnection", + 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) _SlackConnection_channelId(ctx context.Context, field graphql.CollectedField, obj *types.SlackConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SlackConnection_channelId(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.ChannelID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SlackConnection_channelId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SlackConnection", + 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) _SlackConnection_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.SlackConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SlackConnection_createdAt(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.CreatedAt, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(time.Time) + fc.Result = res + return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SlackConnection_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SlackConnection", + 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) _SlackConnection_updatedAt(ctx context.Context, field graphql.CollectedField, obj *types.SlackConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SlackConnection_updatedAt(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.UpdatedAt, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(time.Time) + fc.Result = res + return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SlackConnection_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SlackConnection", + 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) _SlackConnectionConnection_edges(ctx context.Context, field graphql.CollectedField, obj *types.SlackConnectionConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SlackConnectionConnection_edges(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Edges, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*types.SlackConnectionEdge) + fc.Result = res + return ec.marshalNSlackConnectionEdge2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSlackConnectionEdgeᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SlackConnectionConnection_edges(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SlackConnectionConnection", + 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_SlackConnectionEdge_cursor(ctx, field) + case "node": + return ec.fieldContext_SlackConnectionEdge_node(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type SlackConnectionEdge", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _SlackConnectionConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *types.SlackConnectionConnection) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SlackConnectionConnection_pageInfo(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.PageInfo, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*types.PageInfo) + fc.Result = res + return ec.marshalNPageInfo2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐPageInfo(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SlackConnectionConnection_pageInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SlackConnectionConnection", + 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) _SlackConnectionEdge_cursor(ctx context.Context, field graphql.CollectedField, obj *types.SlackConnectionEdge) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SlackConnectionEdge_cursor(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Cursor, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(page.CursorKey) + fc.Result = res + return ec.marshalNCursorKey2githubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SlackConnectionEdge_cursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SlackConnectionEdge", + 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) _SlackConnectionEdge_node(ctx context.Context, field graphql.CollectedField, obj *types.SlackConnectionEdge) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_SlackConnectionEdge_node(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Node, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*types.SlackConnection) + fc.Result = res + return ec.marshalNSlackConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSlackConnection(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_SlackConnectionEdge_node(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SlackConnectionEdge", + 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_SlackConnection_id(ctx, field) + case "channel": + return ec.fieldContext_SlackConnection_channel(ctx, field) + case "channelId": + return ec.fieldContext_SlackConnection_channelId(ctx, field) + case "createdAt": + return ec.fieldContext_SlackConnection_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_SlackConnection_updatedAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type SlackConnection", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _Snapshot_id(ctx context.Context, field graphql.CollectedField, obj *types.Snapshot) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Snapshot_id(ctx, field) if err != nil { @@ -54380,8 +54336,8 @@ func (ec *executionContext) fieldContext_Snapshot_organization(_ context.Context return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -55297,8 +55253,8 @@ func (ec *executionContext) fieldContext_Task_organization(_ context.Context, fi return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -56141,8 +56097,8 @@ func (ec *executionContext) fieldContext_TrustCenter_organization(_ context.Cont return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -59188,8 +59144,8 @@ func (ec *executionContext) fieldContext_UpdateOrganizationPayload_organization( return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -61196,8 +61152,8 @@ func (ec *executionContext) fieldContext_Vendor_organization(_ context.Context, return ec.fieldContext_Organization_memberships(ctx, field) case "invitations": return ec.fieldContext_Organization_invitations(ctx, field) - case "connectors": - return ec.fieldContext_Organization_connectors(ctx, field) + case "slackConnections": + return ec.fieldContext_Organization_slackConnections(ctx, field) case "frameworks": return ec.fieldContext_Organization_frameworks(ctx, field) case "controls": @@ -68623,40 +68579,6 @@ func (ec *executionContext) unmarshalInputConfirmEmailInput(ctx context.Context, return it, nil } -func (ec *executionContext) unmarshalInputConnectorOrder(ctx context.Context, obj any) (types.ConnectorOrder, error) { - var it types.ConnectorOrder - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } - - fieldsInOrder := [...]string{"field", "direction"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "field": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("field")) - data, err := ec.unmarshalNConnectorOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐConnectorOrderField(ctx, v) - if err != nil { - return it, err - } - it.Field = data - case "direction": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("direction")) - data, err := ec.unmarshalNOrderDirection2githubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐOrderDirection(ctx, v) - if err != nil { - return it, err - } - it.Direction = data - } - } - - return it, nil -} - func (ec *executionContext) unmarshalInputContinualImprovementFilter(ctx context.Context, obj any) (types.ContinualImprovementFilter, error) { var it types.ContinualImprovementFilter asMap := map[string]any{} @@ -75656,13 +75578,6 @@ func (ec *executionContext) _Node(ctx context.Context, sel ast.SelectionSet, obj return graphql.Null } return ec._ContinualImprovement(ctx, sel, obj) - case types.Connector: - return ec._Connector(ctx, sel, &obj) - case *types.Connector: - if obj == nil { - return graphql.Null - } - return ec._Connector(ctx, sel, obj) case types.Audit: return ec._Audit(ctx, sel, &obj) case *types.Audit: @@ -76739,153 +76654,6 @@ func (ec *executionContext) _ConfirmEmailPayload(ctx context.Context, sel ast.Se return out } -var connectorImplementors = []string{"Connector", "Node"} - -func (ec *executionContext) _Connector(ctx context.Context, sel ast.SelectionSet, obj *types.Connector) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, connectorImplementors) - - 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("Connector") - case "id": - out.Values[i] = ec._Connector_id(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "name": - out.Values[i] = ec._Connector_name(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "type": - out.Values[i] = ec._Connector_type(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "createdAt": - out.Values[i] = ec._Connector_createdAt(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "updatedAt": - out.Values[i] = ec._Connector_updatedAt(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 connectorConnectionImplementors = []string{"ConnectorConnection"} - -func (ec *executionContext) _ConnectorConnection(ctx context.Context, sel ast.SelectionSet, obj *types.ConnectorConnection) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, connectorConnectionImplementors) - - 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("ConnectorConnection") - case "edges": - out.Values[i] = ec._ConnectorConnection_edges(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "pageInfo": - out.Values[i] = ec._ConnectorConnection_pageInfo(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 connectorEdgeImplementors = []string{"ConnectorEdge"} - -func (ec *executionContext) _ConnectorEdge(ctx context.Context, sel ast.SelectionSet, obj *types.ConnectorEdge) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, connectorEdgeImplementors) - - 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("ConnectorEdge") - case "cursor": - out.Values[i] = ec._ConnectorEdge_cursor(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "node": - out.Values[i] = ec._ConnectorEdge_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 continualImprovementImplementors = []string{"ContinualImprovement", "Node"} func (ec *executionContext) _ContinualImprovement(ctx context.Context, sel ast.SelectionSet, obj *types.ContinualImprovement) graphql.Marshaler { @@ -84956,7 +84724,7 @@ func (ec *executionContext) _Organization(ctx context.Context, sel ast.Selection } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - case "connectors": + case "slackConnections": field := field innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { @@ -84965,7 +84733,7 @@ func (ec *executionContext) _Organization(ctx context.Context, sel ast.Selection ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Organization_connectors(ctx, field, obj) + res = ec._Organization_slackConnections(ctx, field, obj) if res == graphql.Null { atomic.AddUint32(&fs.Invalids, 1) } @@ -87179,6 +86947,147 @@ func (ec *executionContext) _Session(ctx context.Context, sel ast.SelectionSet, return out } +var slackConnectionImplementors = []string{"SlackConnection"} + +func (ec *executionContext) _SlackConnection(ctx context.Context, sel ast.SelectionSet, obj *types.SlackConnection) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, slackConnectionImplementors) + + 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("SlackConnection") + case "id": + out.Values[i] = ec._SlackConnection_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "channel": + out.Values[i] = ec._SlackConnection_channel(ctx, field, obj) + case "channelId": + out.Values[i] = ec._SlackConnection_channelId(ctx, field, obj) + case "createdAt": + out.Values[i] = ec._SlackConnection_createdAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "updatedAt": + out.Values[i] = ec._SlackConnection_updatedAt(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 slackConnectionConnectionImplementors = []string{"SlackConnectionConnection"} + +func (ec *executionContext) _SlackConnectionConnection(ctx context.Context, sel ast.SelectionSet, obj *types.SlackConnectionConnection) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, slackConnectionConnectionImplementors) + + 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("SlackConnectionConnection") + case "edges": + out.Values[i] = ec._SlackConnectionConnection_edges(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "pageInfo": + out.Values[i] = ec._SlackConnectionConnection_pageInfo(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 slackConnectionEdgeImplementors = []string{"SlackConnectionEdge"} + +func (ec *executionContext) _SlackConnectionEdge(ctx context.Context, sel ast.SelectionSet, obj *types.SlackConnectionEdge) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, slackConnectionEdgeImplementors) + + 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("SlackConnectionEdge") + case "cursor": + out.Values[i] = ec._SlackConnectionEdge_cursor(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "node": + out.Values[i] = ec._SlackConnectionEdge_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 snapshotImplementors = []string{"Snapshot", "Node"} func (ec *executionContext) _Snapshot(ctx context.Context, sel ast.SelectionSet, obj *types.Snapshot) graphql.Marshaler { @@ -92680,112 +92589,6 @@ func (ec *executionContext) marshalNConfirmEmailPayload2ᚖgithubᚗcomᚋgetpro return ec._ConfirmEmailPayload(ctx, sel, v) } -func (ec *executionContext) marshalNConnector2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐConnector(ctx context.Context, sel ast.SelectionSet, v *types.Connector) graphql.Marshaler { - if v == nil { - if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") - } - return graphql.Null - } - return ec._Connector(ctx, sel, v) -} - -func (ec *executionContext) marshalNConnectorConnection2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐConnectorConnection(ctx context.Context, sel ast.SelectionSet, v types.ConnectorConnection) graphql.Marshaler { - return ec._ConnectorConnection(ctx, sel, &v) -} - -func (ec *executionContext) marshalNConnectorConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐConnectorConnection(ctx context.Context, sel ast.SelectionSet, v *types.ConnectorConnection) graphql.Marshaler { - if v == nil { - if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") - } - return graphql.Null - } - return ec._ConnectorConnection(ctx, sel, v) -} - -func (ec *executionContext) marshalNConnectorEdge2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐConnectorEdgeᚄ(ctx context.Context, sel ast.SelectionSet, v []*types.ConnectorEdge) 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.marshalNConnectorEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐConnectorEdge(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) marshalNConnectorEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐConnectorEdge(ctx context.Context, sel ast.SelectionSet, v *types.ConnectorEdge) graphql.Marshaler { - if v == nil { - if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") - } - return graphql.Null - } - return ec._ConnectorEdge(ctx, sel, v) -} - -func (ec *executionContext) unmarshalNConnectorOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐConnectorOrderField(ctx context.Context, v any) (coredata.ConnectorOrderField, error) { - tmp, err := graphql.UnmarshalString(v) - res := unmarshalNConnectorOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐConnectorOrderField[tmp] - return res, graphql.ErrorOnPath(ctx, err) -} - -func (ec *executionContext) marshalNConnectorOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐConnectorOrderField(ctx context.Context, sel ast.SelectionSet, v coredata.ConnectorOrderField) graphql.Marshaler { - _ = sel - res := graphql.MarshalString(marshalNConnectorOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐConnectorOrderField[v]) - if res == graphql.Null { - if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") - } - } - return res -} - -var ( - unmarshalNConnectorOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐConnectorOrderField = map[string]coredata.ConnectorOrderField{ - "CREATED_AT": coredata.ConnectorOrderFieldCreatedAt, - "NAME": coredata.ConnectorOrderFieldName, - } - marshalNConnectorOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐConnectorOrderField = map[coredata.ConnectorOrderField]string{ - coredata.ConnectorOrderFieldCreatedAt: "CREATED_AT", - coredata.ConnectorOrderFieldName: "NAME", - } -) - func (ec *executionContext) marshalNContinualImprovement2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐContinualImprovement(ctx context.Context, sel ast.SelectionSet, v *types.ContinualImprovement) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { @@ -98135,6 +97938,84 @@ func (ec *executionContext) marshalNSendSigningNotificationsPayload2ᚖgithubᚗ return ec._SendSigningNotificationsPayload(ctx, sel, v) } +func (ec *executionContext) marshalNSlackConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSlackConnection(ctx context.Context, sel ast.SelectionSet, v *types.SlackConnection) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._SlackConnection(ctx, sel, v) +} + +func (ec *executionContext) marshalNSlackConnectionConnection2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSlackConnectionConnection(ctx context.Context, sel ast.SelectionSet, v types.SlackConnectionConnection) graphql.Marshaler { + return ec._SlackConnectionConnection(ctx, sel, &v) +} + +func (ec *executionContext) marshalNSlackConnectionConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSlackConnectionConnection(ctx context.Context, sel ast.SelectionSet, v *types.SlackConnectionConnection) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._SlackConnectionConnection(ctx, sel, v) +} + +func (ec *executionContext) marshalNSlackConnectionEdge2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSlackConnectionEdgeᚄ(ctx context.Context, sel ast.SelectionSet, v []*types.SlackConnectionEdge) 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.marshalNSlackConnectionEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSlackConnectionEdge(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) marshalNSlackConnectionEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSlackConnectionEdge(ctx context.Context, sel ast.SelectionSet, v *types.SlackConnectionEdge) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._SlackConnectionEdge(ctx, sel, v) +} + func (ec *executionContext) marshalNSnapshot2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSnapshot(ctx context.Context, sel ast.SelectionSet, v *types.Snapshot) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { @@ -100581,14 +100462,6 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast return res } -func (ec *executionContext) unmarshalOConnectorOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐConnectorOrder(ctx context.Context, v any) (*types.ConnectorOrder, error) { - if v == nil { - return nil, nil - } - res, err := ec.unmarshalInputConnectorOrder(ctx, v) - return &res, graphql.ErrorOnPath(ctx, err) -} - func (ec *executionContext) unmarshalOContinualImprovementFilter2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐContinualImprovementFilter(ctx context.Context, v any) (*types.ContinualImprovementFilter, error) { if v == nil { return nil, nil diff --git a/pkg/server/api/console/v1/types/connector.go b/pkg/server/api/console/v1/types/slack_connection.go similarity index 55% rename from pkg/server/api/console/v1/types/connector.go rename to pkg/server/api/console/v1/types/slack_connection.go index f8657ba8b..8c437cc59 100644 --- a/pkg/server/api/console/v1/types/connector.go +++ b/pkg/server/api/console/v1/types/slack_connection.go @@ -19,36 +19,42 @@ import ( "github.com/getprobo/probo/pkg/page" ) -type ( - ConnectorOrderBy OrderBy[coredata.ConnectorOrderField] -) - -func NewConnectorConnection(p *page.Page[*coredata.Connector, coredata.ConnectorOrderField]) *ConnectorConnection { - var edges = make([]*ConnectorEdge, len(p.Data)) +func NewSlackConnectionConnection(p *page.Page[*coredata.Connector, coredata.ConnectorOrderField]) *SlackConnectionConnection { + var edges = make([]*SlackConnectionEdge, len(p.Data)) for i := range edges { - edges[i] = NewConnectorEdge(p.Data[i], p.Cursor.OrderBy.Field) + edges[i] = NewSlackConnectionEdge(p.Data[i], p.Cursor.OrderBy.Field) } - return &ConnectorConnection{ + return &SlackConnectionConnection{ Edges: edges, PageInfo: NewPageInfo(p), } } -func NewConnectorEdge(c *coredata.Connector, orderBy coredata.ConnectorOrderField) *ConnectorEdge { - return &ConnectorEdge{ +func NewSlackConnectionEdge(c *coredata.Connector, orderBy coredata.ConnectorOrderField) *SlackConnectionEdge { + return &SlackConnectionEdge{ Cursor: c.CursorKey(orderBy), - Node: NewConnector(c), + Node: NewSlackConnection(c), } } -func NewConnector(c *coredata.Connector) *Connector { - return &Connector{ +func NewSlackConnection(c *coredata.Connector) *SlackConnection { + conn := &SlackConnection{ ID: c.ID, - Name: c.Name, - Type: string(c.Type), CreatedAt: c.CreatedAt, UpdatedAt: c.UpdatedAt, } + + // Extract channel information from settings + if len(c.Settings) > 0 { + if channel, ok := c.Settings["channel"].(string); ok && channel != "" { + conn.Channel = &channel + } + if channelID, ok := c.Settings["channel_id"].(string); ok && channelID != "" { + conn.ChannelID = &channelID + } + } + + return conn } diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index bc04c6146..18cfdb693 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -146,32 +146,6 @@ type ConfirmEmailPayload struct { Success bool `json:"success"` } -type Connector struct { - ID gid.GID `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` -} - -func (Connector) IsNode() {} -func (this Connector) GetID() gid.GID { return this.ID } - -type ConnectorConnection struct { - Edges []*ConnectorEdge `json:"edges"` - PageInfo *PageInfo `json:"pageInfo"` -} - -type ConnectorEdge struct { - Cursor page.CursorKey `json:"cursor"` - Node *Connector `json:"node"` -} - -type ConnectorOrder struct { - Field coredata.ConnectorOrderField `json:"field"` - Direction page.OrderDirection `json:"direction"` -} - type ContinualImprovement struct { ID gid.GID `json:"id"` SnapshotID *gid.GID `json:"snapshotId,omitempty"` @@ -1374,7 +1348,7 @@ type Organization struct { HeadquarterAddress *string `json:"headquarterAddress,omitempty"` Memberships *MembershipConnection `json:"memberships"` Invitations *InvitationConnection `json:"invitations"` - Connectors *ConnectorConnection `json:"connectors"` + SlackConnections *SlackConnectionConnection `json:"slackConnections"` Frameworks *FrameworkConnection `json:"frameworks"` Controls *ControlConnection `json:"controls"` Vendors *VendorConnection `json:"vendors"` @@ -1590,6 +1564,24 @@ type Session struct { ExpiresAt time.Time `json:"expiresAt"` } +type SlackConnection struct { + ID gid.GID `json:"id"` + Channel *string `json:"channel,omitempty"` + ChannelID *string `json:"channelId,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type SlackConnectionConnection struct { + Edges []*SlackConnectionEdge `json:"edges"` + PageInfo *PageInfo `json:"pageInfo"` +} + +type SlackConnectionEdge struct { + Cursor page.CursorKey `json:"cursor"` + Node *SlackConnection `json:"node"` +} + type Snapshot struct { ID gid.GID `json:"id"` Organization *Organization `json:"organization"` diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 8b6b316e6..3249cfa1f 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -3666,29 +3666,27 @@ func (r *organizationResolver) Invitations(ctx context.Context, obj *types.Organ return types.NewInvitationConnection(page, r, obj.ID, filter), nil } -// Connectors is the resolver for the connectors field. -func (r *organizationResolver) Connectors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ConnectorOrder) (*types.ConnectorConnection, error) { +// SlackConnections is the resolver for the slackConnections field. +func (r *organizationResolver) SlackConnections(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.SlackConnectionConnection, error) { prb := r.ProboService(ctx, obj.ID.TenantID()) + // Filter for Slack connectors only + slackProvider := coredata.ConnectorProviderSlack + filter := coredata.NewConnectorProviderFilter(&slackProvider) + pageOrderBy := page.OrderBy[coredata.ConnectorOrderField]{ Field: coredata.ConnectorOrderFieldCreatedAt, Direction: page.OrderDirectionDesc, } - if orderBy != nil { - pageOrderBy = page.OrderBy[coredata.ConnectorOrderField]{ - Field: orderBy.Field, - Direction: orderBy.Direction, - } - } cursor := types.NewCursor(first, after, last, before, pageOrderBy) - page, err := prb.Connectors.ListForOrganizationID(ctx, obj.ID, cursor) + page, err := prb.Connectors.ListForOrganizationID(ctx, obj.ID, cursor, filter) if err != nil { - panic(fmt.Errorf("cannot list organization connectors: %w", err)) + panic(fmt.Errorf("cannot list organization slack connections: %w", err)) } - return types.NewConnectorConnection(page), nil + return types.NewSlackConnectionConnection(page), nil } // Frameworks is the resolver for the frameworks field. diff --git a/pkg/slack/client.go b/pkg/slack/client.go new file mode 100644 index 000000000..a597078a3 --- /dev/null +++ b/pkg/slack/client.go @@ -0,0 +1,105 @@ +// 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 slack + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" +) + +type ( + Client struct { + webhookURL string + httpClient *http.Client + } + + webhookMessage struct { + Text string `json:"text,omitempty"` + Blocks []block `json:"blocks,omitempty"` + } + + block struct { + Type string `json:"type"` + Text *textItem `json:"text,omitempty"` + } + + textItem struct { + Type string `json:"type"` + Text string `json:"text"` + } +) + +func NewClient(webhookURL string, httpClient *http.Client) *Client { + return &Client{ + webhookURL: webhookURL, + httpClient: httpClient, + } +} + +func (c *Client) PostMessage(ctx context.Context, text string) error { + msg := webhookMessage{ + Text: text, + Blocks: []block{ + { + Type: "section", + Text: &textItem{ + Type: "mrkdwn", + Text: text, + }, + }, + }, + } + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(msg); err != nil { + return fmt.Errorf("cannot marshal message: %w", err) + } + body := buf.Bytes() + + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.webhookURL, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("cannot create request: %w", err) + } + + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return fmt.Errorf("cannot send request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("unexpected status code: %d, failed to read response body: %w", resp.StatusCode, err) + } + + var errorResponse map[string]any + var buf bytes.Buffer + buf.Write(body) + if err := json.NewDecoder(&buf).Decode(&errorResponse); err != nil { + return fmt.Errorf("unexpected status code: %d, response body: %s", resp.StatusCode, string(body)) + } + + return fmt.Errorf("unexpected status code: %d, response: %+v", resp.StatusCode, errorResponse) + } + + return nil +} diff --git a/pkg/slack/sender.go b/pkg/slack/sender.go new file mode 100644 index 000000000..34a40e930 --- /dev/null +++ b/pkg/slack/sender.go @@ -0,0 +1,177 @@ +// 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 slack + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/getprobo/probo/pkg/connector" + "github.com/getprobo/probo/pkg/coredata" + "github.com/getprobo/probo/pkg/crypto/cipher" + "go.gearno.de/kit/httpclient" + "go.gearno.de/kit/log" + "go.gearno.de/kit/pg" +) + +type ( + Sender struct { + pg *pg.Client + logger *log.Logger + encryptionKey cipher.EncryptionKey + interval time.Duration + httpClient *http.Client + } + + Config struct { + Interval time.Duration + } +) + +func NewSender(pg *pg.Client, logger *log.Logger, encryptionKey cipher.EncryptionKey, cfg Config) *Sender { + httpClientOpts := []httpclient.Option{ + httpclient.WithLogger(logger), + } + + httpClient := httpclient.DefaultPooledClient(httpClientOpts...) + + return &Sender{ + pg: pg, + logger: logger, + encryptionKey: encryptionKey, + interval: cfg.Interval, + httpClient: httpClient, + } +} + +func (s *Sender) Run(ctx context.Context) error { +LOOP: + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(s.interval): + ctx := context.Background() + if err := s.batchSendMessages(ctx); err != nil { + s.logger.ErrorCtx(ctx, "cannot send slack message", log.Error(err)) + } + + goto LOOP + } +} + +func (s *Sender) batchSendMessages(ctx context.Context) error { + for { + err := s.pg.WithTx( + ctx, + func(tx pg.Conn) (err error) { + message := &coredata.SlackMessage{} + + defer func() { + if r := recover(); r != nil { + panicErr := fmt.Sprintf("panic recovered: %v", r) + message.Error = &panicErr + message.UpdatedAt = time.Now() + + if updateErr := message.Update(ctx, tx); updateErr != nil { + s.logger.ErrorCtx(ctx, "cannot update slack message after panic", log.Error(updateErr)) + } + + s.logger.ErrorCtx(ctx, "panic while sending slack message", log.String("error", panicErr), log.String("message_id", message.ID.String())) + err = fmt.Errorf("panic recovered: %v", r) + } + }() + + err = message.LoadNextUnsentForUpdate(ctx, tx) + if err != nil { + return err + } + + if sendErr := s.sendMessage(ctx, tx, message); sendErr != nil { + errorMsg := sendErr.Error() + message.Error = &errorMsg + message.UpdatedAt = time.Now() + + if err := message.Update(ctx, tx); err != nil { + return fmt.Errorf("cannot update slack message with error: %w", err) + } + + s.logger.ErrorCtx(ctx, "error sending slack message", log.Error(sendErr), log.String("message_id", message.ID.String())) + return nil + } + + now := time.Now() + message.SentAt = &now + message.UpdatedAt = now + + if err := message.Update(ctx, tx); err != nil { + return fmt.Errorf("cannot update slack message: %w", err) + } + + return nil + }, + ) + + if errors.Is(err, coredata.ErrNoUnsentSlackMessage{}) { + return nil + } + + if err != nil { + return err + } + } +} + +func (s *Sender) sendMessage(ctx context.Context, tx pg.Conn, message *coredata.SlackMessage) error { + tenantID := message.ID.TenantID() + scope := coredata.NewScope(tenantID) + + var connectors coredata.Connectors + if err := connectors.LoadAllByOrganizationIDProtocolAndProvider( + ctx, + tx, + scope, + message.OrganizationID, + coredata.ConnectorProtocolOAuth2, + coredata.ConnectorProviderSlack, + s.encryptionKey, + ); err != nil { + return fmt.Errorf("cannot load slack connectors: %w", err) + } + + if len(connectors) == 0 { + return fmt.Errorf("no slack connectors configured for organization") + } + + for _, c := range connectors { + slackConn, ok := c.Connection.(*connector.SlackConnection) + if !ok { + return fmt.Errorf("slack connector must have SlackConnection type") + } + + if slackConn.Settings.WebhookURL == "" { + return fmt.Errorf("slack connector %s has no webhook URL", c.ID) + } + + client := NewClient(slackConn.Settings.WebhookURL, s.httpClient) + if err := client.PostMessage(ctx, message.Body); err != nil { + return fmt.Errorf("failed to post message to Slack: %w", err) + } + } + + return nil +} diff --git a/pkg/trust/service.go b/pkg/trust/service.go index 85cc55b2d..159496ea6 100644 --- a/pkg/trust/service.go +++ b/pkg/trust/service.go @@ -34,6 +34,7 @@ type ( proboSvc *probo.Service encryptionKey cipher.EncryptionKey tokenSecret string + hostname string auth *auth.Service html2pdfConverter *html2pdf.Converter fileManager *filemanager.Service @@ -47,6 +48,7 @@ type ( proboSvc *probo.Service encryptionKey cipher.EncryptionKey tokenSecret string + hostname string auth *auth.Service html2pdfConverter *html2pdf.Converter fileManager *filemanager.Service @@ -66,6 +68,7 @@ func NewService( pgClient *pg.Client, s3Client *s3.Client, bucket string, + hostname string, encryptionKey cipher.EncryptionKey, tokenSecret string, auth *auth.Service, @@ -78,6 +81,7 @@ func NewService( bucket: bucket, encryptionKey: encryptionKey, tokenSecret: tokenSecret, + hostname: hostname, auth: auth, html2pdfConverter: html2pdfConverter, fileManager: fileManagerService, @@ -93,6 +97,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService { proboSvc: s.proboSvc, encryptionKey: s.encryptionKey, tokenSecret: s.tokenSecret, + hostname: s.hostname, auth: s.auth, html2pdfConverter: s.html2pdfConverter, fileManager: s.fileManager, diff --git a/pkg/trust/templates.go b/pkg/trust/templates.go new file mode 100644 index 000000000..50d69af4d --- /dev/null +++ b/pkg/trust/templates.go @@ -0,0 +1,24 @@ +// 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 trust + +import ( + "embed" +) + +var ( + //go:embed templates/*.tmpl + Templates embed.FS +) diff --git a/pkg/trust/templates/access-request.txt.tmpl b/pkg/trust/templates/access-request.txt.tmpl new file mode 100644 index 000000000..4aad10a8f --- /dev/null +++ b/pkg/trust/templates/access-request.txt.tmpl @@ -0,0 +1,7 @@ +*New Trust Center Access Request* + +*Organization:* {{.OrganizationName}} +*Requested by:* {{.RequesterName}} +*Email:* {{.RequesterEmail}} + +<{{.ConsoleUrl}}|View Access Requests> diff --git a/pkg/trust/trust_center_access_service.go b/pkg/trust/trust_center_access_service.go index a93be9876..f705faf5b 100644 --- a/pkg/trust/trust_center_access_service.go +++ b/pkg/trust/trust_center_access_service.go @@ -15,22 +15,28 @@ package trust import ( + "bytes" "context" "encoding/json" "errors" "fmt" "net/mail" + "text/template" "time" + "github.com/getprobo/probo/pkg/auth" "github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/gid" - "github.com/getprobo/probo/pkg/auth" "go.gearno.de/kit/pg" ) +var ( + accessRequestTemplate = template.Must(template.ParseFS(Templates, "templates/access-request.txt.tmpl")) +) + type ( TrustCenterAccessService struct { - svc *TenantService + svc *TenantService auth *auth.Service } @@ -45,6 +51,7 @@ type ( const ( TokenTypeTrustCenterAccess = "trust_center_access" + TrustCenterAccessURLFormat = "https://%s/organizations/%s/trust-center/access" ) func (s TrustCenterAccessService) ValidateToken( @@ -74,9 +81,9 @@ func (s TrustCenterAccessService) Request( now := time.Now() var access *coredata.TrustCenterAccess + var trustCenter *coredata.TrustCenter err := s.svc.pg.WithTx(ctx, func(tx pg.Conn) error { - var trustCenter *coredata.TrustCenter var organizationID gid.GID trustCenter = &coredata.TrustCenter{} if err := trustCenter.LoadByID(ctx, tx, s.svc.scope, req.TrustCenterID); err != nil { @@ -168,6 +175,10 @@ func (s TrustCenterAccessService) Request( return fmt.Errorf("cannot bulk insert trust center report accesses: %w", err) } + if err := s.queueSlackNotification(ctx, tx, organizationID, access.Name, access.Email); err != nil { + return fmt.Errorf("cannot queue slack notification: %w", err) + } + return nil }) @@ -330,3 +341,42 @@ func filterExistingIDs(allIDs []gid.GID, existingIDs []gid.GID) []gid.GID { return newIDs } + +func (s TrustCenterAccessService) queueSlackNotification( + ctx context.Context, + tx pg.Conn, + organizationID gid.GID, + requesterName string, + requesterEmail string, +) error { + var organization coredata.Organization + if err := organization.LoadByID(ctx, tx, s.svc.scope, organizationID); err != nil { + return fmt.Errorf("cannot load organization: %w", err) + } + + consoleURL := fmt.Sprintf(TrustCenterAccessURLFormat, s.svc.hostname, organizationID) + + data := struct { + OrganizationName string + RequesterName string + RequesterEmail string + ConsoleUrl string + }{ + OrganizationName: organization.Name, + RequesterName: requesterName, + RequesterEmail: requesterEmail, + ConsoleUrl: consoleURL, + } + + var buf bytes.Buffer + if err := accessRequestTemplate.Execute(&buf, data); err != nil { + return fmt.Errorf("failed to execute template: %w", err) + } + + slackMessage := coredata.NewSlackMessage(s.svc.scope, organizationID, buf.String()) + if err := slackMessage.Insert(ctx, tx, s.svc.scope); err != nil { + return fmt.Errorf("cannot insert slack message: %w", err) + } + + return nil +}