From c191d25e9ab7456224e792ade97bb39cd4ba7898 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Mon, 16 Feb 2026 23:12:09 +0100 Subject: [PATCH] Add electronic signature Signed-off-by: Bryan Frimin --- apps/trust/src/components/NDADialog.tsx | 147 -- .../NDADialogSignMutation.graphql.ts | 92 -- apps/trust/src/layouts/MainLayout.tsx | 23 +- apps/trust/src/pages/NDAPage.tsx | 332 ++++ apps/trust/src/pages/NDAPageLoader.tsx | 34 + ...ceptElectronicSignatureMutation.graphql.ts | 115 ++ .../__generated__/NDAPageFragment.graphql.ts | 103 ++ .../__generated__/NDAPageQuery.graphql.ts | 236 +++ ...APageRecordSigningEventMutation.graphql.ts | 94 ++ .../NDAPageRefetchQuery.graphql.ts | 155 ++ apps/trust/src/queries/TrustGraph.ts | 4 +- .../TrustGraphCurrentQuery.graphql.ts | 50 +- apps/trust/src/routes.tsx | 5 + go.mod | 6 +- go.sum | 8 +- packages/emails/emails.go | 23 + packages/emails/scripts/build.ts | 5 + .../src/ElectronicSignatureCertificate.tsx | 31 + .../electronic-signature-certificate.txt | 12 + pkg/coredata/electronic_signature.go | 424 ++++++ .../electronic_signature_document_type.go | 145 ++ pkg/coredata/electronic_signature_event.go | 117 ++ .../electronic_signature_event_source.go | 65 + .../electronic_signature_event_type.go | 86 ++ pkg/coredata/electronic_signature_status.go | 74 + pkg/coredata/email_attachment.go | 105 ++ pkg/coredata/entity_type_reg.go | 9 + pkg/coredata/migrations/20260213T150000Z.sql | 94 ++ pkg/crypto/hash/hash.go | 25 + pkg/esign/certgen.go | 167 ++ pkg/esign/certificate.html.tmpl | 520 +++++++ pkg/esign/completion_certificate_worker.go | 375 +++++ pkg/esign/sealing_worker.go | 344 +++++ pkg/esign/service.go | 352 +++++ pkg/esign/tsa.go | 83 + pkg/filemanager/service.go | 25 + pkg/mailer/mailer.go | 37 +- pkg/probo/service.go | 6 + pkg/probo/trust_center_access_service.go | 18 + pkg/probod/probod.go | 42 +- pkg/server/api/api.go | 4 + pkg/server/api/console/v1/graphql_handler.go | 4 +- pkg/server/api/console/v1/resolver.go | 5 +- pkg/server/api/console/v1/schema.graphql | 147 ++ pkg/server/api/console/v1/schema/schema.go | 1346 +++++++++++++++++ .../console/v1/types/electronic_signature.go | 45 + pkg/server/api/console/v1/types/types.go | 28 + pkg/server/api/console/v1/v1_resolver.go | 76 + pkg/server/api/trust/v1/graphql_handler.go | 4 +- pkg/server/api/trust/v1/resolver.go | 5 +- pkg/server/api/trust/v1/schema.graphql | 146 ++ pkg/server/api/trust/v1/schema/schema.go | 1206 +++++++++++++++ .../trust/v1/types/electronic_signature.go | 32 + pkg/server/api/trust/v1/types/types.go | 33 + pkg/server/api/trust/v1/v1_resolver.go | 92 ++ pkg/server/server.go | 3 + pkg/trust/service.go | 22 + pkg/trust/trust_center_access_service.go | 63 +- 58 files changed, 7557 insertions(+), 292 deletions(-) delete mode 100644 apps/trust/src/components/NDADialog.tsx delete mode 100644 apps/trust/src/components/__generated__/NDADialogSignMutation.graphql.ts create mode 100644 apps/trust/src/pages/NDAPage.tsx create mode 100644 apps/trust/src/pages/NDAPageLoader.tsx create mode 100644 apps/trust/src/pages/__generated__/NDAPageAcceptElectronicSignatureMutation.graphql.ts create mode 100644 apps/trust/src/pages/__generated__/NDAPageFragment.graphql.ts create mode 100644 apps/trust/src/pages/__generated__/NDAPageQuery.graphql.ts create mode 100644 apps/trust/src/pages/__generated__/NDAPageRecordSigningEventMutation.graphql.ts create mode 100644 apps/trust/src/pages/__generated__/NDAPageRefetchQuery.graphql.ts create mode 100644 packages/emails/src/ElectronicSignatureCertificate.tsx create mode 100644 packages/emails/templates/electronic-signature-certificate.txt create mode 100644 pkg/coredata/electronic_signature.go create mode 100644 pkg/coredata/electronic_signature_document_type.go create mode 100644 pkg/coredata/electronic_signature_event.go create mode 100644 pkg/coredata/electronic_signature_event_source.go create mode 100644 pkg/coredata/electronic_signature_event_type.go create mode 100644 pkg/coredata/electronic_signature_status.go create mode 100644 pkg/coredata/email_attachment.go create mode 100644 pkg/coredata/migrations/20260213T150000Z.sql create mode 100644 pkg/crypto/hash/hash.go create mode 100644 pkg/esign/certgen.go create mode 100644 pkg/esign/certificate.html.tmpl create mode 100644 pkg/esign/completion_certificate_worker.go create mode 100644 pkg/esign/sealing_worker.go create mode 100644 pkg/esign/service.go create mode 100644 pkg/esign/tsa.go create mode 100644 pkg/server/api/console/v1/types/electronic_signature.go create mode 100644 pkg/server/api/trust/v1/types/electronic_signature.go diff --git a/apps/trust/src/components/NDADialog.tsx b/apps/trust/src/components/NDADialog.tsx deleted file mode 100644 index 7c7d68106..000000000 --- a/apps/trust/src/components/NDADialog.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { sprintf } from "@probo/helpers"; -import { useTranslate } from "@probo/i18n"; -import { Button, Card, Field, Logo, Spinner } from "@probo/ui"; -import { clsx } from "clsx"; -import { use, useEffect } from "react"; -import { graphql } from "relay-runtime"; -import { useWindowSize } from "usehooks-ts"; -import { z } from "zod"; - -import { useFormWithSchema } from "#/hooks/useFormWithSchema"; -import { useMutationWithToasts } from "#/hooks/useMutationWithToast"; -import { Viewer } from "#/providers/Viewer"; - -import { PDFPreview } from "./PDFPreview"; - -const signMutation = graphql` - mutation NDADialogSignMutation($input: AcceptNonDisclosureAgreementInput!) { - acceptNonDisclosureAgreement(input: $input) { - success - } - } -`; - -const schema = z.object({ - fullName: z.string(), -}); - -export function NDADialog({ - organizationName, - url, - fileName, -}: { - organizationName: string; - url?: string | null; - fileName?: string | null; -}) { - const { __ } = useTranslate(); - useEffect(() => { - document.body.style.setProperty("overflow", "hidden"); - return () => { - document.body.style.removeProperty("overflow"); - }; - }, []); - const { width } = useWindowSize(); - const isMobile = width < 1100; - const isDesktop = !isMobile; - const viewer = use(Viewer); - - const { - handleSubmit: handleSubmitWrapper, - register, - formState, - } = useFormWithSchema(schema, { - defaultValues: { - fullName: viewer?.fullName, - }, - }); - - const [commitSigning, isSigning] = useMutationWithToasts(signMutation, { - onSuccess: () => { - window.location.reload(); - }, - }); - - const handleSubmit = handleSubmitWrapper(async ({ fullName }) => { - await commitSigning({ - variables: { - input: { - fullName, - }, - }, - }); - }); - - return ( -
-
- -
-
-
-

- {__("Review & Sign NDA")} -

-

- {sprintf( - __( - "Access to %s compliance page documents requires signing a Non-Disclosure Agreement (NDA). Please review the agreement below. Once signed, you’ll receive immediate access to the requested documents.", - ), - organizationName, - )} -

- {isMobile && url && ( - - {fileName} - - - )} -
void handleSubmit(e)}> -
- - -
-
-

- {__( - "By clicking Review & Sign, you agree to the terms of this NDA. If you have questions about the NDA, please contact security@probo.com.", - )} -

- - Powered by - {" "} - - -
- {isDesktop && ( -
- {url && } -
- )} -
-
- ); -} diff --git a/apps/trust/src/components/__generated__/NDADialogSignMutation.graphql.ts b/apps/trust/src/components/__generated__/NDADialogSignMutation.graphql.ts deleted file mode 100644 index 547deab5e..000000000 --- a/apps/trust/src/components/__generated__/NDADialogSignMutation.graphql.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @generated SignedSource<<22d97ce5cead5a1cc6c23e89fca72cb1>> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ConcreteRequest } from 'relay-runtime'; -export type AcceptNonDisclosureAgreementInput = { - fullName: string; -}; -export type NDADialogSignMutation$variables = { - input: AcceptNonDisclosureAgreementInput; -}; -export type NDADialogSignMutation$data = { - readonly acceptNonDisclosureAgreement: { - readonly success: boolean; - } | null | undefined; -}; -export type NDADialogSignMutation = { - response: NDADialogSignMutation$data; - variables: NDADialogSignMutation$variables; -}; - -const node: ConcreteRequest = (function(){ -var v0 = [ - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "input" - } -], -v1 = [ - { - "alias": null, - "args": [ - { - "kind": "Variable", - "name": "input", - "variableName": "input" - } - ], - "concreteType": "AcceptNonDisclosureAgreementPayload", - "kind": "LinkedField", - "name": "acceptNonDisclosureAgreement", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "success", - "storageKey": null - } - ], - "storageKey": null - } -]; -return { - "fragment": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Fragment", - "metadata": null, - "name": "NDADialogSignMutation", - "selections": (v1/*: any*/), - "type": "Mutation", - "abstractKey": null - }, - "kind": "Request", - "operation": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Operation", - "name": "NDADialogSignMutation", - "selections": (v1/*: any*/) - }, - "params": { - "cacheID": "130cfc307dca0525194e0103a0548bd0", - "id": null, - "metadata": {}, - "name": "NDADialogSignMutation", - "operationKind": "mutation", - "text": "mutation NDADialogSignMutation(\n $input: AcceptNonDisclosureAgreementInput!\n) {\n acceptNonDisclosureAgreement(input: $input) {\n success\n }\n}\n" - } -}; -})(); - -(node as any).hash = "1b9447e5cbb2ec7dce4f3f9c68493555"; - -export default node; diff --git a/apps/trust/src/layouts/MainLayout.tsx b/apps/trust/src/layouts/MainLayout.tsx index e0d89d1d9..e1aec50b8 100644 --- a/apps/trust/src/layouts/MainLayout.tsx +++ b/apps/trust/src/layouts/MainLayout.tsx @@ -2,9 +2,8 @@ import { useFavicon, useSystemTheme } from "@probo/hooks"; import { useTranslate } from "@probo/i18n"; import { Logo, TabLink, Tabs } from "@probo/ui"; import { type PreloadedQuery, usePreloadedQuery } from "react-relay"; -import { Outlet } from "react-router"; +import { Navigate, Outlet } from "react-router"; -import { NDADialog } from "#/components/NDADialog"; import { OrganizationSidebar } from "#/components/OrganizationSidebar"; import { TrustCenterProvider } from "#/providers/TrustCenterProvider"; import { Viewer } from "#/providers/Viewer"; @@ -28,20 +27,20 @@ export function MainLayout(props: Props) { return null; } - const showNDADialog + const ndaSig = trustCenter.ndaSignature; + const hasPendingNDA = trustCenter.isViewerMember - && !trustCenter.hasAcceptedNonDisclosureAgreement - && trustCenter.ndaFileUrl; + && trustCenter.ndaFileUrl + && ndaSig + && ndaSig.status !== "COMPLETED"; + + if (hasPendingNDA) { + return ; + } + return ( - {showNDADialog && ( - - )}
diff --git a/apps/trust/src/pages/NDAPage.tsx b/apps/trust/src/pages/NDAPage.tsx new file mode 100644 index 000000000..c5a5bc507 --- /dev/null +++ b/apps/trust/src/pages/NDAPage.tsx @@ -0,0 +1,332 @@ +import { sprintf } from "@probo/helpers"; +import { useTranslate } from "@probo/i18n"; +import { Button, Card, Field, IconCircleX, Logo, Spinner } from "@probo/ui"; +import { startTransition, useEffect, useRef } from "react"; +import { + type PreloadedQuery, + useMutation, + usePreloadedQuery, + useRefetchableFragment, +} from "react-relay"; +import { Navigate, useNavigate } from "react-router"; +import { graphql } from "relay-runtime"; +import { useWindowSize } from "usehooks-ts"; +import { z } from "zod"; + +import { PDFPreview } from "#/components/PDFPreview"; +import { useFormWithSchema } from "#/hooks/useFormWithSchema"; + +import type { NDAPageAcceptElectronicSignatureMutation } from "./__generated__/NDAPageAcceptElectronicSignatureMutation.graphql"; +import type { NDAPageFragment$key } from "./__generated__/NDAPageFragment.graphql"; +import type { NDAPageQuery as NDAPageQueryType } from "./__generated__/NDAPageQuery.graphql"; +import type { NDAPageRecordSigningEventMutation } from "./__generated__/NDAPageRecordSigningEventMutation.graphql"; +import type { NDAPageRefetchQuery } from "./__generated__/NDAPageRefetchQuery.graphql"; + +export const ndaPageQuery = graphql` + query NDAPageQuery { + viewer { + fullName + } + currentTrustCenter @required(action: THROW) { + organization { + name + } + ndaFileUrl + ndaFileName + ndaSignature { + status + } + ...NDAPageFragment + } + } +`; + +const ndaPageFragment = graphql` + fragment NDAPageFragment on TrustCenter + @refetchable(queryName: "NDAPageRefetchQuery") { + ndaSignature @required(action: THROW) { + id + status + consentText + lastError + } + } +`; + +const acceptElectronicSignatureMutation = graphql` + mutation NDAPageAcceptElectronicSignatureMutation( + $input: AcceptElectronicSignatureInput! + ) { + acceptElectronicSignature(input: $input) { + signature { + id + status + } + } + } +`; + +const recordSigningEventMutation = graphql` + mutation NDAPageRecordSigningEventMutation( + $input: RecordSigningEventInput! + ) { + recordSigningEvent(input: $input) { + success + } + } +`; + +const schema = z.object({ + fullName: z.string().min(1), +}); + +export function NDAPage(props: { + queryRef: PreloadedQuery; +}) { + const { __ } = useTranslate(); + const navigate = useNavigate(); + const documentViewedRef = useRef(false); + + const queryData = usePreloadedQuery(ndaPageQuery, props.queryRef); + const trustCenter = queryData.currentTrustCenter; + const viewer = queryData.viewer; + + const [data, refetch] = useRefetchableFragment( + ndaPageFragment, + trustCenter, + ); + const ndaSignature = data.ndaSignature; + + const { width } = useWindowSize(); + const isMobile = width < 1100; + const isDesktop = !isMobile; + + const { + handleSubmit: handleSubmitWrapper, + register, + formState, + } = useFormWithSchema(schema, { + defaultValues: { + fullName: viewer?.fullName, + }, + }); + + const [acceptSignature, isAccepting] = useMutation( + acceptElectronicSignatureMutation, + ); + + const [recordSigningEvent] = useMutation( + recordSigningEventMutation, + ); + + const isProcessing + = ndaSignature.status === "ACCEPTED" + || ndaSignature.status === "PROCESSING"; + + const isFailed = ndaSignature.status === "FAILED"; + const isCompleted = ndaSignature.status === "COMPLETED"; + + useEffect(() => { + if (isCompleted) { + navigate("/overview", { replace: true }); + } + }, [isCompleted, navigate]); + + useEffect(() => { + if (!isProcessing) return; + + const poll = () => startTransition(() => { + refetch({}, { fetchPolicy: "network-only" }); + }); + const interval = setInterval(poll, 1500); + + return () => clearInterval(interval); + }, [isProcessing, refetch]); + + useEffect(() => { + if ( + ndaSignature + && ndaSignature.status === "PENDING" + && !documentViewedRef.current + ) { + documentViewedRef.current = true; + recordSigningEvent({ + variables: { + input: { + signatureId: ndaSignature.id, + eventType: "DOCUMENT_VIEWED", + }, + }, + }); + } + }, [ndaSignature, recordSigningEvent]); + + const handleSubmit = handleSubmitWrapper(({ fullName }) => { + if (!ndaSignature) return; + + if (ndaSignature.status === "PENDING") { + recordSigningEvent({ + variables: { + input: { + signatureId: ndaSignature.id, + eventType: "FULL_NAME_TYPED", + }, + }, + }); + } + + recordSigningEvent({ + variables: { + input: { + signatureId: ndaSignature.id, + eventType: "CONSENT_GIVEN", + }, + }, + onCompleted: () => { + acceptSignature({ + variables: { + input: { + signatureId: ndaSignature.id, + fullName, + }, + }, + }); + }, + }); + }); + + const handleTryAgain = () => { + if (ndaSignature && viewer?.fullName) { + acceptSignature({ + variables: { + input: { + signatureId: ndaSignature.id, + fullName: viewer.fullName, + }, + }, + }); + } + }; + + if ( + !trustCenter.ndaFileUrl + || !trustCenter.ndaSignature + || trustCenter.ndaSignature.status === "COMPLETED" + ) { + return ; + } + + const consentText = ndaSignature?.consentText + ? ndaSignature.consentText + : __( + "By clicking Review & Sign, you agree to the terms of this NDA. If you have questions about the NDA, please contact security@probo.com.", + ); + + return ( +
+
+ +
+
+
+
+

+ {__("Non-Disclosure Agreement")} +

+

+ {sprintf( + __( + "%s requires you to sign an NDA before accessing compliance documents.", + ), + trustCenter.organization.name, + )} +

+ {isMobile && trustCenter.ndaFileUrl && ( + + {trustCenter.ndaFileName} + + + )} +
{ + e.preventDefault(); + handleTryAgain(); + } + : e => void handleSubmit(e) + } + className="mt-8" + > + +

+ {consentText} +

+ {isFailed && ( +
+ +
+

+ {__("Signature processing failed")} +

+

+ {ndaSignature?.lastError + ?? __("We encountered an issue processing your signature. Please try again.")} +

+
+
+ )} + {isProcessing + ? ( + + ) + : ( + + )} + +
+ + Powered by + {" "} + + +
+ {isDesktop && ( +
+ {trustCenter.ndaFileUrl && } +
+ )} +
+
+ ); +} diff --git a/apps/trust/src/pages/NDAPageLoader.tsx b/apps/trust/src/pages/NDAPageLoader.tsx new file mode 100644 index 000000000..2c114f1b5 --- /dev/null +++ b/apps/trust/src/pages/NDAPageLoader.tsx @@ -0,0 +1,34 @@ +import { Suspense, useEffect } from "react"; +import { useQueryLoader } from "react-relay"; + +import { RelayProvider } from "#/providers/RelayProviders"; + +import type { NDAPageQuery } from "./__generated__/NDAPageQuery.graphql"; +import { NDAPage, ndaPageQuery } from "./NDAPage"; + +function NDAPageQueryLoader() { + const [queryRef, loadQuery] + = useQueryLoader(ndaPageQuery); + + useEffect(() => { + if (!queryRef) { + loadQuery({}); + } + }); + + if (!queryRef) return null; + + return ( + + + + ); +} + +export default function NDAPageLoader() { + return ( + + + + ); +} diff --git a/apps/trust/src/pages/__generated__/NDAPageAcceptElectronicSignatureMutation.graphql.ts b/apps/trust/src/pages/__generated__/NDAPageAcceptElectronicSignatureMutation.graphql.ts new file mode 100644 index 000000000..8a49f24c3 --- /dev/null +++ b/apps/trust/src/pages/__generated__/NDAPageAcceptElectronicSignatureMutation.graphql.ts @@ -0,0 +1,115 @@ +/** + * @generated SignedSource<<41e415a03c63efbb590b1c2c782d5eac>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type ElectronicSignatureStatus = "ACCEPTED" | "COMPLETED" | "FAILED" | "PENDING" | "PROCESSING"; +export type AcceptElectronicSignatureInput = { + fullName: string; + signatureId: string; +}; +export type NDAPageAcceptElectronicSignatureMutation$variables = { + input: AcceptElectronicSignatureInput; +}; +export type NDAPageAcceptElectronicSignatureMutation$data = { + readonly acceptElectronicSignature: { + readonly signature: { + readonly id: string; + readonly status: ElectronicSignatureStatus; + }; + } | null | undefined; +}; +export type NDAPageAcceptElectronicSignatureMutation = { + response: NDAPageAcceptElectronicSignatureMutation$data; + variables: NDAPageAcceptElectronicSignatureMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "AcceptElectronicSignaturePayload", + "kind": "LinkedField", + "name": "acceptElectronicSignature", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ElectronicSignature", + "kind": "LinkedField", + "name": "signature", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "NDAPageAcceptElectronicSignatureMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "NDAPageAcceptElectronicSignatureMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "db0c219a7133025b56252836d7f1a85d", + "id": null, + "metadata": {}, + "name": "NDAPageAcceptElectronicSignatureMutation", + "operationKind": "mutation", + "text": "mutation NDAPageAcceptElectronicSignatureMutation(\n $input: AcceptElectronicSignatureInput!\n) {\n acceptElectronicSignature(input: $input) {\n signature {\n id\n status\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "b4ab543bd58878bb4968de6ea8b7c448"; + +export default node; diff --git a/apps/trust/src/pages/__generated__/NDAPageFragment.graphql.ts b/apps/trust/src/pages/__generated__/NDAPageFragment.graphql.ts new file mode 100644 index 000000000..672159d34 --- /dev/null +++ b/apps/trust/src/pages/__generated__/NDAPageFragment.graphql.ts @@ -0,0 +1,103 @@ +/** + * @generated SignedSource<<93f8676826dfa28c1681da68a6ac1cf2>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +export type ElectronicSignatureStatus = "ACCEPTED" | "COMPLETED" | "FAILED" | "PENDING" | "PROCESSING"; +import { FragmentRefs } from "relay-runtime"; +export type NDAPageFragment$data = { + readonly id: string; + readonly ndaSignature: { + readonly consentText: string; + readonly id: string; + readonly lastError: string | null | undefined; + readonly status: ElectronicSignatureStatus; + }; + readonly " $fragmentType": "NDAPageFragment"; +}; +export type NDAPageFragment$key = { + readonly " $data"?: NDAPageFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"NDAPageFragment">; +}; + +import NDAPageRefetchQuery_graphql from './NDAPageRefetchQuery.graphql'; + +const node: ReaderFragment = (function(){ +var v0 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "refetch": { + "connection": null, + "fragmentPathInResult": [ + "node" + ], + "operation": NDAPageRefetchQuery_graphql, + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + } + } + }, + "name": "NDAPageFragment", + "selections": [ + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "ElectronicSignature", + "kind": "LinkedField", + "name": "ndaSignature", + "plural": false, + "selections": [ + (v0/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "consentText", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "lastError", + "storageKey": null + } + ], + "storageKey": null + }, + "action": "THROW" + }, + (v0/*: any*/) + ], + "type": "TrustCenter", + "abstractKey": null +}; +})(); + +(node as any).hash = "b53a5f455508ed45124b7d09cfb573da"; + +export default node; diff --git a/apps/trust/src/pages/__generated__/NDAPageQuery.graphql.ts b/apps/trust/src/pages/__generated__/NDAPageQuery.graphql.ts new file mode 100644 index 000000000..dd2832ed8 --- /dev/null +++ b/apps/trust/src/pages/__generated__/NDAPageQuery.graphql.ts @@ -0,0 +1,236 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type ElectronicSignatureStatus = "ACCEPTED" | "COMPLETED" | "FAILED" | "PENDING" | "PROCESSING"; +export type NDAPageQuery$variables = Record; +export type NDAPageQuery$data = { + readonly currentTrustCenter: { + readonly ndaFileName: string | null | undefined; + readonly ndaFileUrl: string | null | undefined; + readonly ndaSignature: { + readonly status: ElectronicSignatureStatus; + } | null | undefined; + readonly organization: { + readonly name: string; + }; + readonly " $fragmentSpreads": FragmentRefs<"NDAPageFragment">; + }; + readonly viewer: { + readonly fullName: string; + } | null | undefined; +}; +export type NDAPageQuery = { + response: NDAPageQuery$data; + variables: NDAPageQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null +}, +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}, +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "ndaFileUrl", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "ndaFileName", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null +}, +v5 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "NDAPageQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Identity", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + (v0/*: any*/) + ], + "storageKey": null + }, + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "TrustCenter", + "kind": "LinkedField", + "name": "currentTrustCenter", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + (v1/*: any*/) + ], + "storageKey": null + }, + (v2/*: any*/), + (v3/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "ElectronicSignature", + "kind": "LinkedField", + "name": "ndaSignature", + "plural": false, + "selections": [ + (v4/*: any*/) + ], + "storageKey": null + }, + { + "args": null, + "kind": "FragmentSpread", + "name": "NDAPageFragment" + } + ], + "storageKey": null + }, + "action": "THROW" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "NDAPageQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Identity", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + (v0/*: any*/), + (v5/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "TrustCenter", + "kind": "LinkedField", + "name": "currentTrustCenter", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + (v1/*: any*/), + (v5/*: any*/) + ], + "storageKey": null + }, + (v2/*: any*/), + (v3/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "ElectronicSignature", + "kind": "LinkedField", + "name": "ndaSignature", + "plural": false, + "selections": [ + (v4/*: any*/), + (v5/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "consentText", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "lastError", + "storageKey": null + } + ], + "storageKey": null + }, + (v5/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "c0b175c435c5730e390fcfde596d8bc3", + "id": null, + "metadata": {}, + "name": "NDAPageQuery", + "operationKind": "query", + "text": "query NDAPageQuery {\n viewer {\n fullName\n id\n }\n currentTrustCenter {\n organization {\n name\n id\n }\n ndaFileUrl\n ndaFileName\n ndaSignature {\n status\n id\n }\n ...NDAPageFragment\n id\n }\n}\n\nfragment NDAPageFragment on TrustCenter {\n ndaSignature {\n id\n status\n consentText\n lastError\n }\n id\n}\n" + } +}; +})(); + +(node as any).hash = "a78bf8b65feba4627ecb265ca882f645"; + +export default node; diff --git a/apps/trust/src/pages/__generated__/NDAPageRecordSigningEventMutation.graphql.ts b/apps/trust/src/pages/__generated__/NDAPageRecordSigningEventMutation.graphql.ts new file mode 100644 index 000000000..a25042db5 --- /dev/null +++ b/apps/trust/src/pages/__generated__/NDAPageRecordSigningEventMutation.graphql.ts @@ -0,0 +1,94 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type ElectronicSignatureEventType = "CERTIFICATE_GENERATED" | "CONSENT_GIVEN" | "DOCUMENT_VIEWED" | "FULL_NAME_TYPED" | "SEAL_COMPUTED" | "SIGNATURE_ACCEPTED" | "SIGNATURE_COMPLETED" | "TIMESTAMP_REQUESTED"; +export type RecordSigningEventInput = { + eventType: ElectronicSignatureEventType; + signatureId: string; +}; +export type NDAPageRecordSigningEventMutation$variables = { + input: RecordSigningEventInput; +}; +export type NDAPageRecordSigningEventMutation$data = { + readonly recordSigningEvent: { + readonly success: boolean; + } | null | undefined; +}; +export type NDAPageRecordSigningEventMutation = { + response: NDAPageRecordSigningEventMutation$data; + variables: NDAPageRecordSigningEventMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "RecordSigningEventPayload", + "kind": "LinkedField", + "name": "recordSigningEvent", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "success", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "NDAPageRecordSigningEventMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "NDAPageRecordSigningEventMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "72aa3c3ec642d5bbb317a6e2a65703de", + "id": null, + "metadata": {}, + "name": "NDAPageRecordSigningEventMutation", + "operationKind": "mutation", + "text": "mutation NDAPageRecordSigningEventMutation(\n $input: RecordSigningEventInput!\n) {\n recordSigningEvent(input: $input) {\n success\n }\n}\n" + } +}; +})(); + +(node as any).hash = "ec40eb438c6d6b20f31974e099535887"; + +export default node; diff --git a/apps/trust/src/pages/__generated__/NDAPageRefetchQuery.graphql.ts b/apps/trust/src/pages/__generated__/NDAPageRefetchQuery.graphql.ts new file mode 100644 index 000000000..c5fac0509 --- /dev/null +++ b/apps/trust/src/pages/__generated__/NDAPageRefetchQuery.graphql.ts @@ -0,0 +1,155 @@ +/** + * @generated SignedSource<<47d15b007fc81de3850393c9b158726c>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type NDAPageRefetchQuery$variables = { + id: string; +}; +export type NDAPageRefetchQuery$data = { + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"NDAPageFragment">; + }; +}; +export type NDAPageRefetchQuery = { + response: NDAPageRefetchQuery$data; + variables: NDAPageRefetchQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "NDAPageRefetchQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "NDAPageFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "NDAPageRefetchQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ElectronicSignature", + "kind": "LinkedField", + "name": "ndaSignature", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "consentText", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "lastError", + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "TrustCenter", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "1c61baa555ad2b24794ce0bf5ecc14ac", + "id": null, + "metadata": {}, + "name": "NDAPageRefetchQuery", + "operationKind": "query", + "text": "query NDAPageRefetchQuery(\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...NDAPageFragment\n id\n }\n}\n\nfragment NDAPageFragment on TrustCenter {\n ndaSignature {\n id\n status\n consentText\n lastError\n }\n id\n}\n" + } +}; +})(); + +(node as any).hash = "b53a5f455508ed45124b7d09cfb573da"; + +export default node; diff --git a/apps/trust/src/queries/TrustGraph.ts b/apps/trust/src/queries/TrustGraph.ts index 63ad05efa..5333f104f 100644 --- a/apps/trust/src/queries/TrustGraph.ts +++ b/apps/trust/src/queries/TrustGraph.ts @@ -11,11 +11,13 @@ export const currentTrustGraphQuery = graphql` id slug isViewerMember - hasAcceptedNonDisclosureAgreement logoFileUrl darkLogoFileUrl ndaFileName ndaFileUrl + ndaSignature { + status + } organization { name description diff --git a/apps/trust/src/queries/__generated__/TrustGraphCurrentQuery.graphql.ts b/apps/trust/src/queries/__generated__/TrustGraphCurrentQuery.graphql.ts index 8a0b14806..bacc0b9b2 100644 --- a/apps/trust/src/queries/__generated__/TrustGraphCurrentQuery.graphql.ts +++ b/apps/trust/src/queries/__generated__/TrustGraphCurrentQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<006540288cfca33589264e1f596e1028>> + * @generated SignedSource<<4bf04c5599ef88c6104919907fb182dd>> * @lightSyntaxTransform * @nogrep */ @@ -10,6 +10,7 @@ import { ConcreteRequest } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; +export type ElectronicSignatureStatus = "ACCEPTED" | "COMPLETED" | "FAILED" | "PENDING" | "PROCESSING"; export type TrustGraphCurrentQuery$variables = Record; export type TrustGraphCurrentQuery$data = { readonly currentTrustCenter: { @@ -22,12 +23,14 @@ export type TrustGraphCurrentQuery$data = { }>; }; readonly darkLogoFileUrl: string | null | undefined; - readonly hasAcceptedNonDisclosureAgreement: boolean; readonly id: string; readonly isViewerMember: boolean; readonly logoFileUrl: string | null | undefined; readonly ndaFileName: string | null | undefined; readonly ndaFileUrl: string | null | undefined; + readonly ndaSignature: { + readonly status: ElectronicSignatureStatus; + } | null | undefined; readonly organization: { readonly description: string | null | undefined; readonly email: string | null | undefined; @@ -91,35 +94,35 @@ v5 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "hasAcceptedNonDisclosureAgreement", + "name": "logoFileUrl", "storageKey": null }, v6 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "logoFileUrl", + "name": "darkLogoFileUrl", "storageKey": null }, v7 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "darkLogoFileUrl", + "name": "ndaFileName", "storageKey": null }, v8 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "ndaFileName", + "name": "ndaFileUrl", "storageKey": null }, v9 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "ndaFileUrl", + "name": "status", "storageKey": null }, v10 = { @@ -237,7 +240,18 @@ return { (v6/*: any*/), (v7/*: any*/), (v8/*: any*/), - (v9/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "ElectronicSignature", + "kind": "LinkedField", + "name": "ndaSignature", + "plural": false, + "selections": [ + (v9/*: any*/) + ], + "storageKey": null + }, { "alias": null, "args": null, @@ -341,7 +355,19 @@ return { (v6/*: any*/), (v7/*: any*/), (v8/*: any*/), - (v9/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "ElectronicSignature", + "kind": "LinkedField", + "name": "ndaSignature", + "plural": false, + "selections": [ + (v9/*: any*/), + (v2/*: any*/) + ], + "storageKey": null + }, { "alias": null, "args": null, @@ -641,16 +667,16 @@ return { ] }, "params": { - "cacheID": "af3118fae0bbc255713aae845ab83032", + "cacheID": "5756e245b9ad1dc00d25c505e4764a62", "id": null, "metadata": {}, "name": "TrustGraphCurrentQuery", "operationKind": "query", - "text": "query TrustGraphCurrentQuery {\n viewer {\n email\n fullName\n id\n }\n currentTrustCenter {\n id\n slug\n isViewerMember\n hasAcceptedNonDisclosureAgreement\n logoFileUrl\n darkLogoFileUrl\n ndaFileName\n ndaFileUrl\n organization {\n name\n description\n websiteUrl\n email\n headquarterAddress\n id\n }\n ...OverviewPageFragment\n vendorInfo: vendors(first: 0) {\n totalCount\n }\n audits(first: 50) {\n edges {\n node {\n id\n ...AuditRowFragment\n }\n }\n }\n }\n}\n\nfragment AuditRowFragment on Audit {\n report {\n id\n filename\n isUserAuthorized\n hasUserRequestedAccess\n }\n framework {\n id\n name\n lightLogoURL\n darkLogoURL\n }\n}\n\nfragment DocumentRowFragment on Document {\n id\n title\n isUserAuthorized\n hasUserRequestedAccess\n}\n\nfragment OverviewPageFragment on TrustCenter {\n references(first: 14) {\n edges {\n node {\n id\n name\n logoUrl\n websiteUrl\n }\n }\n }\n vendors(first: 3) {\n edges {\n node {\n id\n countries\n ...VendorRowFragment\n }\n }\n }\n documents(first: 5) {\n edges {\n node {\n id\n ...DocumentRowFragment\n documentType\n }\n }\n }\n trustCenterFiles(first: 5) {\n edges {\n node {\n id\n category\n ...TrustCenterFileRowFragment\n }\n }\n }\n}\n\nfragment TrustCenterFileRowFragment on TrustCenterFile {\n id\n name\n isUserAuthorized\n hasUserRequestedAccess\n}\n\nfragment VendorRowFragment on Vendor {\n name\n description\n websiteUrl\n countries\n}\n" + "text": "query TrustGraphCurrentQuery {\n viewer {\n email\n fullName\n id\n }\n currentTrustCenter {\n id\n slug\n isViewerMember\n logoFileUrl\n darkLogoFileUrl\n ndaFileName\n ndaFileUrl\n ndaSignature {\n status\n id\n }\n organization {\n name\n description\n websiteUrl\n email\n headquarterAddress\n id\n }\n ...OverviewPageFragment\n vendorInfo: vendors(first: 0) {\n totalCount\n }\n audits(first: 50) {\n edges {\n node {\n id\n ...AuditRowFragment\n }\n }\n }\n }\n}\n\nfragment AuditRowFragment on Audit {\n report {\n id\n filename\n isUserAuthorized\n hasUserRequestedAccess\n }\n framework {\n id\n name\n lightLogoURL\n darkLogoURL\n }\n}\n\nfragment DocumentRowFragment on Document {\n id\n title\n isUserAuthorized\n hasUserRequestedAccess\n}\n\nfragment OverviewPageFragment on TrustCenter {\n references(first: 14) {\n edges {\n node {\n id\n name\n logoUrl\n websiteUrl\n }\n }\n }\n vendors(first: 3) {\n edges {\n node {\n id\n countries\n ...VendorRowFragment\n }\n }\n }\n documents(first: 5) {\n edges {\n node {\n id\n ...DocumentRowFragment\n documentType\n }\n }\n }\n trustCenterFiles(first: 5) {\n edges {\n node {\n id\n category\n ...TrustCenterFileRowFragment\n }\n }\n }\n}\n\nfragment TrustCenterFileRowFragment on TrustCenterFile {\n id\n name\n isUserAuthorized\n hasUserRequestedAccess\n}\n\nfragment VendorRowFragment on Vendor {\n name\n description\n websiteUrl\n countries\n}\n" } }; })(); -(node as any).hash = "566fdb9af999e51fdf50e2eccca60941"; +(node as any).hash = "3b8d99e97c59c613f0ef2f01951a5221"; export default node; diff --git a/apps/trust/src/routes.tsx b/apps/trust/src/routes.tsx index 841c1b756..4595adf58 100644 --- a/apps/trust/src/routes.tsx +++ b/apps/trust/src/routes.tsx @@ -56,6 +56,11 @@ const routes = [ Component: Fragment, ErrorBoundary: ErrorBoundary, }, + { + path: "/nda", + Component: lazy(() => import("#/pages/NDAPageLoader")), + ErrorBoundary: ErrorBoundary, + }, // Custom domain routes (subdomain-based) { path: "/overview", diff --git a/go.mod b/go.mod index 352c15148..be37cf7dd 100644 --- a/go.mod +++ b/go.mod @@ -13,9 +13,11 @@ require ( github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d github.com/chromedp/chromedp v0.14.2 github.com/crewjam/saml v0.5.1 + github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8 github.com/go-chi/chi/v5 v5.2.4 github.com/go-chi/cors v1.2.2 + github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.8.0 github.com/jhillyerd/enmime v1.3.0 github.com/modelcontextprotocol/go-sdk v1.2.0 @@ -29,7 +31,7 @@ require ( github.com/yuin/goldmark v1.7.16 go.gearno.de/crypto/uuid v0.1.1-0.20251208105319-3f587312a712 go.gearno.de/kit v0.1.1 - go.gearno.de/x/ref v0.0.0-20240502200927-d74926fcb14c + go.gearno.de/x/ref v0.0.0-20260216110753-a700c951377c go.opentelemetry.io/otel v1.39.0 go.opentelemetry.io/otel/trace v1.39.0 go.probo.inc/mcpgen v0.0.0-20251124210642-41a5174eb92f @@ -66,6 +68,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/di-wu/parser v0.3.0 // indirect github.com/di-wu/xsd-datetime v1.0.0 // indirect + github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49 // indirect github.com/dnephin/pflag v1.0.7 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -82,7 +85,6 @@ require ( github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.9 // indirect github.com/googleapis/gax-go/v2 v2.16.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect diff --git a/go.sum b/go.sum index b9adf6a25..09b2b37ee 100644 --- a/go.sum +++ b/go.sum @@ -77,6 +77,10 @@ github.com/di-wu/parser v0.3.0 h1:NMOvy5ifswgt4gsdhySVcKOQtvjC43cHZIfViWctqQY= github.com/di-wu/parser v0.3.0/go.mod h1:SLp58pW6WamdmznrVRrw2NTyn4wAvT9rrEFynKX7nYo= github.com/di-wu/xsd-datetime v1.0.0 h1:vZoGNkbzpBNoc+JyfVLEbutNDNydYV8XwHeV7eUJoxI= github.com/di-wu/xsd-datetime v1.0.0/go.mod h1:i3iEhrP3WchwseOBeIdW/zxeoleXTOzx1WyDXgdmOww= +github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49 h1:h+XMRXf+WLY0h/3itqE8OT3TgjCMHK4nq2FNGi0au2c= +github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea h1:ALRwvjsSP53QmnN3Bcj0NpR8SsFLnskny/EIMebAk1c= +github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk= github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE= github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8 h1:0+BTyxIYgiVAry/P5s8R4dYuLkhB9Nhso8ogFWNr4IQ= @@ -255,8 +259,8 @@ go.gearno.de/kit v0.1.1 h1:QuBZCZ/h2Eyh6DjjR6CGjkdsab/ztHz6xUiIk0FeREE= go.gearno.de/kit v0.1.1/go.mod h1:WI/gQ14O9M6wsKa/HFL4ZH+Q/U0930hYWZRdHHo9Agk= go.gearno.de/x/panicf v0.1.1 h1:E3Cr9NB8Ry2EsvEG/1eHr7kplP3tEjTf5d56dTX64VQ= go.gearno.de/x/panicf v0.1.1/go.mod h1:VnB8oF0UefMZcYeD4v+Wk4U5Z1uza7PHLlhT2CbNEbU= -go.gearno.de/x/ref v0.0.0-20240502200927-d74926fcb14c h1:b8Wwr2owaB6g38tptSk5tekXFSE7RpXsUpaoCO8tQDM= -go.gearno.de/x/ref v0.0.0-20240502200927-d74926fcb14c/go.mod h1:k3GtgnI5X9dl8FlqaNYkCkil7/iACQdFOromU/H4u6I= +go.gearno.de/x/ref v0.0.0-20260216110753-a700c951377c h1:rIVWwnNxHYu9aZhHkptXlNYTBJbY4ccaIAYjztVeaDc= +go.gearno.de/x/ref v0.0.0-20260216110753-a700c951377c/go.mod h1:k3GtgnI5X9dl8FlqaNYkCkil7/iACQdFOromU/H4u6I= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= diff --git a/packages/emails/emails.go b/packages/emails/emails.go index d2db5ad01..a15f1cd8a 100644 --- a/packages/emails/emails.go +++ b/packages/emails/emails.go @@ -217,6 +217,7 @@ const ( subjectTrustCenterAccess = "Compliance Page Access Invitation - %s" subjectTrustCenterDocumentAccessRejected = "Compliance Page Document Access Rejected - %s" subjectMagicLink = "Connect to %s" + subjectElectronicSignatureCertificate = "Your signed %s - Certificate of Completion" ) var ( @@ -238,6 +239,8 @@ var ( trustCenterDocumentAccessRejectedTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/trust-center-document-access-rejected.txt.tmpl")) magicLinkHTMLTemplate = htmltemplate.Must(htmltemplate.ParseFS(Templates, "dist/magic-link.html.tmpl")) magicLinkTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/magic-link.txt.tmpl")) + electronicSignatureCertificateHTMLTemplate = htmltemplate.Must(htmltemplate.ParseFS(Templates, "dist/electronic-signature-certificate.html.tmpl")) + electronicSignatureCertificateTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/electronic-signature-certificate.txt.tmpl")) ) func (p *Presenter) getCommonVariables(ctx context.Context) (*CommonVariables, error) { @@ -460,6 +463,26 @@ func (p *Presenter) RenderMagicLink(ctx context.Context, magicLinkUrlPath string return fmt.Sprintf(subjectMagicLink, organizationName), textBody, htmlBody, err } +func (p *Presenter) RenderElectronicSignatureCertificate(ctx context.Context, signerName string, documentType string) (subject string, textBody string, htmlBody *string, err error) { + vars, err := p.getCommonVariables(ctx) + if err != nil { + return "", "", nil, fmt.Errorf("cannot get common variables: %w", err) + } + + data := struct { + *CommonVariables + SignerName string + DocumentType string + }{ + CommonVariables: vars, + SignerName: signerName, + DocumentType: documentType, + } + + textBody, htmlBody, err = renderEmail(electronicSignatureCertificateTextTemplate, electronicSignatureCertificateHTMLTemplate, data) + return fmt.Sprintf(subjectElectronicSignatureCertificate, documentType), textBody, htmlBody, err +} + func renderEmail(textTemplate *texttemplate.Template, htmlTemplate *htmltemplate.Template, data any) (textBody string, htmlBody *string, err error) { var textBuf bytes.Buffer if err := textTemplate.Execute(&textBuf, data); err != nil { diff --git a/packages/emails/scripts/build.ts b/packages/emails/scripts/build.ts index 6eccaf9b4..4c992b312 100644 --- a/packages/emails/scripts/build.ts +++ b/packages/emails/scripts/build.ts @@ -12,6 +12,7 @@ import Invitation from "../src/Invitation"; import PasswordReset from "../src/PasswordReset"; import TrustCenterAccess from "../src/TrustCenterAccess"; import TrustCenterDocumentAccessRejected from "../src/TrustCenterDocumentAccessRejected"; +import ElectronicSignatureCertificate from "../src/ElectronicSignatureCertificate"; import MagicLink from "../src/MagicLink"; const __filename = fileURLToPath(import.meta.url); @@ -59,6 +60,10 @@ const templates: TemplateConfig[] = [ name: "magic-link", render: () => MagicLink(), }, + { + name: "electronic-signature-certificate", + render: () => ElectronicSignatureCertificate(), + }, ]; async function build() { diff --git a/packages/emails/src/ElectronicSignatureCertificate.tsx b/packages/emails/src/ElectronicSignatureCertificate.tsx new file mode 100644 index 000000000..659c7a2e0 --- /dev/null +++ b/packages/emails/src/ElectronicSignatureCertificate.tsx @@ -0,0 +1,31 @@ +import { Text } from "@react-email/components"; +import * as React from "react"; +import EmailLayout, { + bodyText, + footerText, +} from "./components/EmailLayout"; + +export const ElectronicSignatureCertificate = () => { + return ( + + + Your {"{{.DocumentType}}"} has been signed + electronically. A Certificate of Completion is attached to this email + as a PDF document. + + + + The certificate contains a complete record of the signing event, + including the integrity seal, timestamp, and full audit trail. + + + + If you have any questions, please contact support. + + + ); +}; + +export default ElectronicSignatureCertificate; diff --git a/packages/emails/templates/electronic-signature-certificate.txt b/packages/emails/templates/electronic-signature-certificate.txt new file mode 100644 index 000000000..7b2b10599 --- /dev/null +++ b/packages/emails/templates/electronic-signature-certificate.txt @@ -0,0 +1,12 @@ +{{.SenderCompanyName}} + +Hi {{.RecipientFullName}}, + +Your {{.DocumentType}} has been signed electronically. A Certificate of Completion is attached to this email as a PDF document. + +The certificate contains a complete record of the signing event, including the integrity seal, timestamp, and audit trail. + +If you have any questions, please contact support. + +{{.SenderCompanyHeadquarterAddress}} +Powered By Probo diff --git a/pkg/coredata/electronic_signature.go b/pkg/coredata/electronic_signature.go new file mode 100644 index 000000000..29e10228f --- /dev/null +++ b/pkg/coredata/electronic_signature.go @@ -0,0 +1,424 @@ +// 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" + "maps" + "strings" + "time" + + "github.com/jackc/pgx/v5" + "go.gearno.de/kit/pg" + "go.gearno.de/x/ref" + "go.probo.inc/probo/pkg/crypto/hash" + "go.probo.inc/probo/pkg/gid" +) + +type ElectronicSignature struct { + ID gid.GID `db:"id"` + TenantID gid.TenantID `db:"tenant_id"` + OrganizationID gid.GID `db:"organization_id"` + Status ElectronicSignatureStatus `db:"status"` + DocumentType ElectronicSignatureDocumentType `db:"document_type"` + FileID gid.GID `db:"file_id"` + SignerEmail string `db:"signer_email"` + ConsentText string `db:"consent_text"` + SignerFullName *string `db:"signer_full_name"` + SignerIPAddress *string `db:"signer_ip_address"` + SignerUserAgent *string `db:"signer_user_agent"` + FileHash *string `db:"file_hash"` + Seal *string `db:"seal"` + SealVersion int `db:"seal_version"` + TSAToken []byte `db:"tsa_token"` + SignedAt *time.Time `db:"signed_at"` + CertificateFileID *gid.GID `db:"certificate_file_id"` + CertificateProcessingStartedAt *time.Time `db:"certificate_processing_started_at"` + AttemptCount int `db:"attempt_count"` + MaxAttempts int `db:"max_attempts"` + LastAttemptedAt *time.Time `db:"last_attempted_at"` + LastError *string `db:"last_error"` + ProcessingStartedAt *time.Time `db:"processing_started_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +func (es *ElectronicSignature) NewEvent( + eventType ElectronicSignatureEventType, + eventSource ElectronicSignatureEventSource, +) ElectronicSignatureEvent { + now := time.Now() + + return ElectronicSignatureEvent{ + ID: gid.New(es.ID.TenantID(), ElectronicSignatureEventEntityType), + ElectronicSignatureID: es.ID, + EventType: eventType, + EventSource: eventSource, + ActorEmail: es.SignerEmail, + ActorIPAddress: ref.UnrefOrZero(es.SignerIPAddress), + ActorUserAgent: ref.UnrefOrZero(es.SignerUserAgent), + OccurredAt: now, + CreatedAt: now, + } +} + +func (es *ElectronicSignature) Insert( + ctx context.Context, + conn pg.Conn, + scope Scoper, +) error { + q := ` +INSERT INTO electronic_signatures ( + id, tenant_id, organization_id, status, document_type, file_id, + signer_email, consent_text, seal_version, attempt_count, max_attempts, + created_at, updated_at +) VALUES ( + @id, @tenant_id, @organization_id, @status, @document_type, @file_id, + @signer_email, @consent_text, @seal_version, @attempt_count, @max_attempts, + @created_at, @updated_at +) +` + args := pgx.StrictNamedArgs{ + "id": es.ID, + "tenant_id": scope.GetTenantID(), + "organization_id": es.OrganizationID, + "status": es.Status, + "document_type": es.DocumentType, + "file_id": es.FileID, + "signer_email": es.SignerEmail, + "consent_text": es.ConsentText, + "seal_version": es.SealVersion, + "attempt_count": es.AttemptCount, + "max_attempts": es.MaxAttempts, + "created_at": es.CreatedAt, + "updated_at": es.UpdatedAt, + } + + _, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot insert electronic signature: %w", err) + } + + return nil +} + +func (es *ElectronicSignature) Update( + ctx context.Context, + conn pg.Conn, + scope Scoper, +) error { + q := ` +UPDATE electronic_signatures SET + status = @status, + signer_full_name = @signer_full_name, + signer_ip_address = @signer_ip_address, + signer_user_agent = @signer_user_agent, + file_hash = @file_hash, + seal = @seal, + seal_version = @seal_version, + tsa_token = @tsa_token, + signed_at = @signed_at, + certificate_file_id = @certificate_file_id, + certificate_processing_started_at = @certificate_processing_started_at, + attempt_count = @attempt_count, + max_attempts = @max_attempts, + last_attempted_at = @last_attempted_at, + last_error = @last_error, + processing_started_at = @processing_started_at, + updated_at = @updated_at +WHERE + %s + AND id = @id +` + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "id": es.ID, + "status": es.Status, + "signer_full_name": es.SignerFullName, + "signer_ip_address": es.SignerIPAddress, + "signer_user_agent": es.SignerUserAgent, + "file_hash": es.FileHash, + "seal": es.Seal, + "seal_version": es.SealVersion, + "tsa_token": es.TSAToken, + "signed_at": es.SignedAt, + "certificate_file_id": es.CertificateFileID, + "certificate_processing_started_at": es.CertificateProcessingStartedAt, + "attempt_count": es.AttemptCount, + "max_attempts": es.MaxAttempts, + "last_attempted_at": es.LastAttemptedAt, + "last_error": es.LastError, + "processing_started_at": es.ProcessingStartedAt, + "updated_at": es.UpdatedAt, + } + maps.Copy(args, scope.SQLArguments()) + + _, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot update electronic signature: %w", err) + } + + return nil +} + +func (es *ElectronicSignature) LoadByID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + id gid.GID, +) error { + q := ` +SELECT + id, tenant_id, organization_id, status, document_type, file_id, + signer_email, consent_text, signer_full_name, signer_ip_address, + signer_user_agent, file_hash, seal, seal_version, tsa_token, signed_at, + certificate_file_id, certificate_processing_started_at, + attempt_count, max_attempts, last_attempted_at, last_error, + processing_started_at, created_at, updated_at +FROM electronic_signatures +WHERE %s AND id = @id +LIMIT 1 +` + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{"id": id} + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query electronic signature: %w", err) + } + + sig, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ElectronicSignature]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrResourceNotFound + } + return fmt.Errorf("cannot collect electronic signature: %w", err) + } + + *es = sig + return nil +} + +func (es *ElectronicSignature) LoadByOrgEmailAndDocType( + ctx context.Context, + conn pg.Conn, + scope Scoper, + orgID gid.GID, + email string, + docType ElectronicSignatureDocumentType, + fileID gid.GID, +) error { + q := ` +SELECT + id, tenant_id, organization_id, status, document_type, file_id, + signer_email, consent_text, signer_full_name, signer_ip_address, + signer_user_agent, file_hash, seal, seal_version, tsa_token, signed_at, + certificate_file_id, certificate_processing_started_at, + attempt_count, max_attempts, last_attempted_at, last_error, + processing_started_at, created_at, updated_at +FROM electronic_signatures +WHERE %s + AND organization_id = @organization_id + AND signer_email = @signer_email + AND document_type = @document_type + AND file_id = @file_id +LIMIT 1 +` + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "organization_id": orgID, + "signer_email": email, + "document_type": docType, + "file_id": fileID, + } + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query electronic signature: %w", err) + } + + sig, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ElectronicSignature]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrResourceNotFound + } + return fmt.Errorf("cannot collect electronic signature: %w", err) + } + + *es = sig + return nil +} + +func (es *ElectronicSignature) LoadNextAcceptedForUpdateSkipLocked( + ctx context.Context, + conn pg.Conn, +) error { + q := ` +SELECT + id, tenant_id, organization_id, status, document_type, file_id, + signer_email, consent_text, signer_full_name, signer_ip_address, + signer_user_agent, file_hash, seal, seal_version, tsa_token, signed_at, + certificate_file_id, certificate_processing_started_at, + attempt_count, max_attempts, last_attempted_at, last_error, + processing_started_at, created_at, updated_at +FROM electronic_signatures +WHERE status = 'ACCEPTED' AND attempt_count < max_attempts +ORDER BY updated_at ASC +LIMIT 1 +FOR UPDATE SKIP LOCKED +` + + rows, err := conn.Query(ctx, q) + if err != nil { + return fmt.Errorf("cannot query accepted signatures: %w", err) + } + + sig, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ElectronicSignature]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrResourceNotFound + } + return fmt.Errorf("cannot collect electronic signature: %w", err) + } + + *es = sig + return nil +} + +func (es *ElectronicSignature) LoadNextCompletedWithoutCertificateForUpdate( + ctx context.Context, + conn pg.Conn, +) error { + q := ` +SELECT + id, tenant_id, organization_id, status, document_type, file_id, + signer_email, consent_text, signer_full_name, signer_ip_address, + signer_user_agent, file_hash, seal, seal_version, tsa_token, signed_at, + certificate_file_id, certificate_processing_started_at, + attempt_count, max_attempts, last_attempted_at, last_error, + processing_started_at, created_at, updated_at +FROM electronic_signatures +WHERE status = 'COMPLETED' + AND certificate_file_id IS NULL + AND certificate_processing_started_at IS NULL +ORDER BY signed_at ASC +LIMIT 1 +FOR UPDATE SKIP LOCKED +` + + rows, err := conn.Query(ctx, q) + if err != nil { + return fmt.Errorf("cannot query completed signatures: %w", err) + } + + sig, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ElectronicSignature]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrResourceNotFound + } + return fmt.Errorf("cannot collect electronic signature: %w", err) + } + + *es = sig + return nil +} + +func ResetStaleProcessingSignatures( + ctx context.Context, + conn pg.Conn, + staleAfter time.Duration, +) error { + q := ` +UPDATE electronic_signatures +SET status = 'ACCEPTED', processing_started_at = NULL, updated_at = NOW() +WHERE status = 'PROCESSING' + AND processing_started_at < NOW() - $1::interval +` + _, err := conn.Exec(ctx, q, staleAfter.String()) + if err != nil { + return fmt.Errorf("cannot reset stale processing signatures: %w", err) + } + + return nil +} + +func (es *ElectronicSignature) ComputeSeal(version int) (string, error) { + switch version { + case 1: + return es.computeSealV1() + default: + return "", fmt.Errorf("unsupported seal version %d", version) + } +} + +func (es *ElectronicSignature) computeSealV1() (string, error) { + if es.SignedAt == nil { + return "", fmt.Errorf("signed_at must not be nil") + } + + fields := []string{ + es.ID.String(), + es.OrganizationID.String(), + es.DocumentType.String(), + es.FileID.String(), + ref.UnrefOrZero(es.FileHash), + ref.UnrefOrZero(es.SignerFullName), + strings.ToLower(es.SignerEmail), + ref.UnrefOrZero(es.SignerIPAddress), + ref.UnrefOrZero(es.SignerUserAgent), + es.ConsentText, + es.SignedAt.UTC().Truncate(time.Microsecond).Format(time.RFC3339Nano), + } + + for i, f := range fields { + if f == "" { + return "", fmt.Errorf("seal field %d must not be empty", i) + } + if strings.Contains(f, "\n") { + return "", fmt.Errorf("seal field %d must not contain newline", i) + } + } + + input := strings.Join(fields, "\n") + return hash.SHA256Hex([]byte(input)), nil +} + +func ResetStaleCertificateProcessing( + ctx context.Context, + conn pg.Conn, + staleAfter time.Duration, +) error { + q := ` +UPDATE electronic_signatures +SET certificate_processing_started_at = NULL, updated_at = NOW() +WHERE status = 'COMPLETED' + AND certificate_file_id IS NULL + AND certificate_processing_started_at IS NOT NULL + AND certificate_processing_started_at < NOW() - $1::interval +` + _, err := conn.Exec(ctx, q, staleAfter.String()) + if err != nil { + return fmt.Errorf("cannot reset stale certificate processing: %w", err) + } + + return nil +} diff --git a/pkg/coredata/electronic_signature_document_type.go b/pkg/coredata/electronic_signature_document_type.go new file mode 100644 index 000000000..1e9191211 --- /dev/null +++ b/pkg/coredata/electronic_signature_document_type.go @@ -0,0 +1,145 @@ +// 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 ( + ElectronicSignatureDocumentType string +) + +const ( + ElectronicSignatureDocumentTypeNDA ElectronicSignatureDocumentType = "NDA" + ElectronicSignatureDocumentTypeDPA ElectronicSignatureDocumentType = "DPA" + ElectronicSignatureDocumentTypeMSA ElectronicSignatureDocumentType = "MSA" + ElectronicSignatureDocumentTypeSOW ElectronicSignatureDocumentType = "SOW" + ElectronicSignatureDocumentTypeSLA ElectronicSignatureDocumentType = "SLA" + ElectronicSignatureDocumentTypeTOS ElectronicSignatureDocumentType = "TOS" + ElectronicSignatureDocumentTypePrivacyPolicy ElectronicSignatureDocumentType = "PRIVACY_POLICY" + ElectronicSignatureDocumentTypeOther ElectronicSignatureDocumentType = "OTHER" + + ESignProcessConsentText = "By typing my full name and clicking Accept, I consent to sign this document electronically and agree that my electronic signature has the same legal validity as a handwritten signature." +) + +func ElectronicSignatureDocumentTypes() []ElectronicSignatureDocumentType { + return []ElectronicSignatureDocumentType{ + ElectronicSignatureDocumentTypeNDA, + ElectronicSignatureDocumentTypeDPA, + ElectronicSignatureDocumentTypeMSA, + ElectronicSignatureDocumentTypeSOW, + ElectronicSignatureDocumentTypeSLA, + ElectronicSignatureDocumentTypeTOS, + ElectronicSignatureDocumentTypePrivacyPolicy, + ElectronicSignatureDocumentTypeOther, + } +} + +func (dt ElectronicSignatureDocumentType) MarshalText() ([]byte, error) { + return []byte(dt.String()), nil +} + +func (dt *ElectronicSignatureDocumentType) UnmarshalText(data []byte) error { + val := string(data) + + switch val { + case ElectronicSignatureDocumentTypeNDA.String(): + *dt = ElectronicSignatureDocumentTypeNDA + case ElectronicSignatureDocumentTypeDPA.String(): + *dt = ElectronicSignatureDocumentTypeDPA + case ElectronicSignatureDocumentTypeMSA.String(): + *dt = ElectronicSignatureDocumentTypeMSA + case ElectronicSignatureDocumentTypeSOW.String(): + *dt = ElectronicSignatureDocumentTypeSOW + case ElectronicSignatureDocumentTypeSLA.String(): + *dt = ElectronicSignatureDocumentTypeSLA + case ElectronicSignatureDocumentTypeTOS.String(): + *dt = ElectronicSignatureDocumentTypeTOS + case ElectronicSignatureDocumentTypePrivacyPolicy.String(): + *dt = ElectronicSignatureDocumentTypePrivacyPolicy + case ElectronicSignatureDocumentTypeOther.String(): + *dt = ElectronicSignatureDocumentTypeOther + default: + return fmt.Errorf("invalid ElectronicSignatureDocumentType value: %q", val) + } + + return nil +} + +func (dt ElectronicSignatureDocumentType) String() string { + return string(dt) +} + +func (dt *ElectronicSignatureDocumentType) Scan(value any) error { + val, ok := value.(string) + if !ok { + return fmt.Errorf("invalid scan source for ElectronicSignatureDocumentType, expected string got %T", value) + } + + return dt.UnmarshalText([]byte(val)) +} + +func (dt ElectronicSignatureDocumentType) Value() (driver.Value, error) { + return dt.String(), nil +} + +func (dt ElectronicSignatureDocumentType) DisplayName() string { + switch dt { + case ElectronicSignatureDocumentTypeNDA: + return "Non-Disclosure Agreement" + case ElectronicSignatureDocumentTypeDPA: + return "Data Processing Agreement" + case ElectronicSignatureDocumentTypeMSA: + return "Master Service Agreement" + case ElectronicSignatureDocumentTypeSOW: + return "Statement of Work" + case ElectronicSignatureDocumentTypeSLA: + return "Service Level Agreement" + case ElectronicSignatureDocumentTypeTOS: + return "Terms of Service" + case ElectronicSignatureDocumentTypePrivacyPolicy: + return "Privacy Policy" + default: + return string(dt) + } +} + +func (dt ElectronicSignatureDocumentType) ConsentText() (string, error) { + var docAgreement string + switch dt { + case ElectronicSignatureDocumentTypeNDA: + docAgreement = "I agree to the terms of this Non-Disclosure Agreement." + case ElectronicSignatureDocumentTypeDPA: + docAgreement = "I agree to the terms of this Data Processing Agreement." + case ElectronicSignatureDocumentTypeMSA: + docAgreement = "I agree to the terms of this Master Service Agreement." + case ElectronicSignatureDocumentTypeSOW: + docAgreement = "I agree to the terms of this Statement of Work." + case ElectronicSignatureDocumentTypeSLA: + docAgreement = "I agree to the terms of this Service Level Agreement." + case ElectronicSignatureDocumentTypeTOS: + docAgreement = "I agree to these Terms of Service." + case ElectronicSignatureDocumentTypePrivacyPolicy: + docAgreement = "I agree to this Privacy Policy." + case ElectronicSignatureDocumentTypeOther: + return "", fmt.Errorf("document type OTHER requires explicit consent text") + default: + return "", fmt.Errorf("unknown document type %q", dt) + } + + return docAgreement + " " + ESignProcessConsentText, nil +} diff --git a/pkg/coredata/electronic_signature_event.go b/pkg/coredata/electronic_signature_event.go new file mode 100644 index 000000000..b8b089570 --- /dev/null +++ b/pkg/coredata/electronic_signature_event.go @@ -0,0 +1,117 @@ +// 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" + "fmt" + "maps" + "time" + + "github.com/jackc/pgx/v5" + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/gid" +) + +type ( + ElectronicSignatureEvent struct { + ID gid.GID `db:"id"` + TenantID gid.TenantID `db:"tenant_id"` + ElectronicSignatureID gid.GID `db:"electronic_signature_id"` + EventType ElectronicSignatureEventType `db:"event_type"` + EventSource ElectronicSignatureEventSource `db:"event_source"` + ActorEmail string `db:"actor_email"` + ActorIPAddress string `db:"actor_ip_address"` + ActorUserAgent string `db:"actor_user_agent"` + OccurredAt time.Time `db:"occurred_at"` + CreatedAt time.Time `db:"created_at"` + } + + ElectronicSignatureEvents []*ElectronicSignatureEvent +) + +func (e *ElectronicSignatureEvent) Insert( + ctx context.Context, + conn pg.Conn, + scope Scoper, +) error { + q := ` +INSERT INTO electronic_signature_events ( + id, tenant_id, electronic_signature_id, event_type, event_source, + actor_email, actor_ip_address, actor_user_agent, + occurred_at, created_at +) VALUES ( + @id, @tenant_id, @electronic_signature_id, @event_type, @event_source, + @actor_email, @actor_ip_address, @actor_user_agent, + @occurred_at, @created_at +) +` + args := pgx.StrictNamedArgs{ + "id": e.ID, + "tenant_id": scope.GetTenantID(), + "electronic_signature_id": e.ElectronicSignatureID, + "event_type": e.EventType, + "event_source": e.EventSource, + "actor_email": e.ActorEmail, + "actor_ip_address": e.ActorIPAddress, + "actor_user_agent": e.ActorUserAgent, + "occurred_at": e.OccurredAt, + "created_at": e.CreatedAt, + } + + _, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot insert electronic signature event: %w", err) + } + + return nil +} + +func (es *ElectronicSignatureEvents) LoadBySignatureID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + sigID gid.GID, +) error { + q := ` +SELECT + id, tenant_id, electronic_signature_id, event_type, event_source, + actor_email, actor_ip_address, actor_user_agent, + occurred_at, created_at +FROM electronic_signature_events +WHERE %s AND electronic_signature_id = @electronic_signature_id +ORDER BY occurred_at ASC +` + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "electronic_signature_id": sigID, + } + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query electronic signature events: %w", err) + } + + events, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ElectronicSignatureEvent]) + if err != nil { + return fmt.Errorf("cannot collect electronic signature events: %w", err) + } + + *es = events + + return nil +} diff --git a/pkg/coredata/electronic_signature_event_source.go b/pkg/coredata/electronic_signature_event_source.go new file mode 100644 index 000000000..d23b5f82d --- /dev/null +++ b/pkg/coredata/electronic_signature_event_source.go @@ -0,0 +1,65 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "database/sql/driver" + "fmt" +) + +type ( + ElectronicSignatureEventSource string +) + +const ( + ElectronicSignatureEventSourceClient ElectronicSignatureEventSource = "CLIENT" + ElectronicSignatureEventSourceServer ElectronicSignatureEventSource = "SERVER" +) + +func (s ElectronicSignatureEventSource) MarshalText() ([]byte, error) { + return []byte(s.String()), nil +} + +func (s *ElectronicSignatureEventSource) UnmarshalText(data []byte) error { + val := string(data) + + switch val { + case ElectronicSignatureEventSourceClient.String(): + *s = ElectronicSignatureEventSourceClient + case ElectronicSignatureEventSourceServer.String(): + *s = ElectronicSignatureEventSourceServer + default: + return fmt.Errorf("invalid ElectronicSignatureEventSource value: %q", val) + } + + return nil +} + +func (s ElectronicSignatureEventSource) String() string { + return string(s) +} + +func (s *ElectronicSignatureEventSource) Scan(value any) error { + val, ok := value.(string) + if !ok { + return fmt.Errorf("invalid scan source for ElectronicSignatureEventSource, expected string got %T", value) + } + + return s.UnmarshalText([]byte(val)) +} + +func (s ElectronicSignatureEventSource) Value() (driver.Value, error) { + return s.String(), nil +} diff --git a/pkg/coredata/electronic_signature_event_type.go b/pkg/coredata/electronic_signature_event_type.go new file mode 100644 index 000000000..bf3b95f31 --- /dev/null +++ b/pkg/coredata/electronic_signature_event_type.go @@ -0,0 +1,86 @@ +// 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 ( + ElectronicSignatureEventType string +) + +const ( + ElectronicSignatureEventTypeDocumentViewed ElectronicSignatureEventType = "DOCUMENT_VIEWED" + ElectronicSignatureEventTypeConsentGiven ElectronicSignatureEventType = "CONSENT_GIVEN" + ElectronicSignatureEventTypeFullNameTyped ElectronicSignatureEventType = "FULL_NAME_TYPED" + ElectronicSignatureEventTypeSignatureAccepted ElectronicSignatureEventType = "SIGNATURE_ACCEPTED" + ElectronicSignatureEventTypeSignatureCompleted ElectronicSignatureEventType = "SIGNATURE_COMPLETED" + ElectronicSignatureEventTypeSealComputed ElectronicSignatureEventType = "SEAL_COMPUTED" + ElectronicSignatureEventTypeTimestampRequested ElectronicSignatureEventType = "TIMESTAMP_REQUESTED" + ElectronicSignatureEventTypeCertificateGenerated ElectronicSignatureEventType = "CERTIFICATE_GENERATED" + ElectronicSignatureEventTypeProcessingError ElectronicSignatureEventType = "PROCESSING_ERROR" +) + +func (t ElectronicSignatureEventType) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t *ElectronicSignatureEventType) UnmarshalText(data []byte) error { + val := string(data) + + switch val { + case ElectronicSignatureEventTypeDocumentViewed.String(): + *t = ElectronicSignatureEventTypeDocumentViewed + case ElectronicSignatureEventTypeConsentGiven.String(): + *t = ElectronicSignatureEventTypeConsentGiven + case ElectronicSignatureEventTypeFullNameTyped.String(): + *t = ElectronicSignatureEventTypeFullNameTyped + case ElectronicSignatureEventTypeSignatureAccepted.String(): + *t = ElectronicSignatureEventTypeSignatureAccepted + case ElectronicSignatureEventTypeSignatureCompleted.String(): + *t = ElectronicSignatureEventTypeSignatureCompleted + case ElectronicSignatureEventTypeSealComputed.String(): + *t = ElectronicSignatureEventTypeSealComputed + case ElectronicSignatureEventTypeTimestampRequested.String(): + *t = ElectronicSignatureEventTypeTimestampRequested + case ElectronicSignatureEventTypeCertificateGenerated.String(): + *t = ElectronicSignatureEventTypeCertificateGenerated + case ElectronicSignatureEventTypeProcessingError.String(): + *t = ElectronicSignatureEventTypeProcessingError + default: + return fmt.Errorf("invalid ElectronicSignatureEventType value: %q", val) + } + + return nil +} + +func (t ElectronicSignatureEventType) String() string { + return string(t) +} + +func (t *ElectronicSignatureEventType) Scan(value any) error { + val, ok := value.(string) + if !ok { + return fmt.Errorf("invalid scan source for ElectronicSignatureEventType, expected string got %T", value) + } + + return t.UnmarshalText([]byte(val)) +} + +func (t ElectronicSignatureEventType) Value() (driver.Value, error) { + return t.String(), nil +} diff --git a/pkg/coredata/electronic_signature_status.go b/pkg/coredata/electronic_signature_status.go new file mode 100644 index 000000000..271335e28 --- /dev/null +++ b/pkg/coredata/electronic_signature_status.go @@ -0,0 +1,74 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "database/sql/driver" + "fmt" +) + +type ( + ElectronicSignatureStatus string +) + +const ( + ElectronicSignatureStatusPending ElectronicSignatureStatus = "PENDING" + ElectronicSignatureStatusAccepted ElectronicSignatureStatus = "ACCEPTED" + ElectronicSignatureStatusProcessing ElectronicSignatureStatus = "PROCESSING" + ElectronicSignatureStatusCompleted ElectronicSignatureStatus = "COMPLETED" + ElectronicSignatureStatusFailed ElectronicSignatureStatus = "FAILED" +) + +func (s ElectronicSignatureStatus) MarshalText() ([]byte, error) { + return []byte(s.String()), nil +} + +func (s *ElectronicSignatureStatus) UnmarshalText(data []byte) error { + val := string(data) + + switch val { + case ElectronicSignatureStatusPending.String(): + *s = ElectronicSignatureStatusPending + case ElectronicSignatureStatusAccepted.String(): + *s = ElectronicSignatureStatusAccepted + case ElectronicSignatureStatusProcessing.String(): + *s = ElectronicSignatureStatusProcessing + case ElectronicSignatureStatusCompleted.String(): + *s = ElectronicSignatureStatusCompleted + case ElectronicSignatureStatusFailed.String(): + *s = ElectronicSignatureStatusFailed + default: + return fmt.Errorf("invalid ElectronicSignatureStatus value: %q", val) + } + + return nil +} + +func (s ElectronicSignatureStatus) String() string { + return string(s) +} + +func (s *ElectronicSignatureStatus) Scan(value any) error { + val, ok := value.(string) + if !ok { + return fmt.Errorf("invalid scan source for ElectronicSignatureStatus, expected string got %T", value) + } + + return s.UnmarshalText([]byte(val)) +} + +func (s ElectronicSignatureStatus) Value() (driver.Value, error) { + return s.String(), nil +} diff --git a/pkg/coredata/email_attachment.go b/pkg/coredata/email_attachment.go new file mode 100644 index 000000000..60585bbb7 --- /dev/null +++ b/pkg/coredata/email_attachment.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 coredata + +import ( + "context" + "fmt" + "time" + + "github.com/jackc/pgx/v5" + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/gid" +) + +type ( + EmailAttachment struct { + ID gid.GID `db:"id"` + EmailID gid.GID `db:"email_id"` + FileID gid.GID `db:"file_id"` + Filename string `db:"filename"` + // TODO (to drop) + ContentType string `db:"content_type"` + CreatedAt time.Time `db:"created_at"` + } + + EmailAttachments []*EmailAttachment +) + +func NewEmailAttachment(emailID, fileID gid.GID, filename, contentType string) *EmailAttachment { + return &EmailAttachment{ + ID: gid.New(gid.NilTenant, EmailAttachmentEntityType), + EmailID: emailID, + FileID: fileID, + Filename: filename, + ContentType: contentType, + CreatedAt: time.Now(), + } +} + +func (a *EmailAttachment) Insert( + ctx context.Context, + conn pg.Conn, +) error { + q := ` +INSERT INTO email_attachments (id, email_id, file_id, filename, content_type, created_at) +VALUES (@id, @email_id, @file_id, @filename, @content_type, @created_at) +` + args := pgx.StrictNamedArgs{ + "id": a.ID, + "email_id": a.EmailID, + "file_id": a.FileID, + "filename": a.Filename, + "content_type": a.ContentType, + "created_at": a.CreatedAt, + } + + _, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot insert email attachment: %w", err) + } + + return nil +} + +func (a *EmailAttachments) LoadByEmailID( + ctx context.Context, + conn pg.Conn, + emailID gid.GID, +) error { + q := ` +SELECT id, email_id, file_id, filename, content_type, created_at +FROM email_attachments +WHERE email_id = @email_id +ORDER BY created_at ASC +` + args := pgx.StrictNamedArgs{ + "email_id": emailID, + } + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query email attachments: %w", err) + } + + attachments, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[EmailAttachment]) + if err != nil { + return fmt.Errorf("cannot collect email attachments: %w", err) + } + + *a = attachments + + return nil +} diff --git a/pkg/coredata/entity_type_reg.go b/pkg/coredata/entity_type_reg.go index 2125eb907..33183bcd1 100644 --- a/pkg/coredata/entity_type_reg.go +++ b/pkg/coredata/entity_type_reg.go @@ -82,6 +82,9 @@ const ( WebhookSubscriptionEntityType uint16 = 56 WebhookDataEntityType uint16 = 57 WebhookEventEntityType uint16 = 58 + ElectronicSignatureEntityType uint16 = 59 + ElectronicSignatureEventEntityType uint16 = 60 + EmailAttachmentEntityType uint16 = 61 ) func NewEntityFromID(id gid.GID) (any, bool) { @@ -200,6 +203,12 @@ func NewEntityFromID(id gid.GID) (any, bool) { return &WebhookData{ID: id}, true case WebhookEventEntityType: return &WebhookEvent{ID: id}, true + case ElectronicSignatureEntityType: + return &ElectronicSignature{ID: id}, true + case ElectronicSignatureEventEntityType: + return &ElectronicSignatureEvent{ID: id}, true + case EmailAttachmentEntityType: + return &EmailAttachment{ID: id}, true default: return nil, false } diff --git a/pkg/coredata/migrations/20260213T150000Z.sql b/pkg/coredata/migrations/20260213T150000Z.sql new file mode 100644 index 000000000..040dad709 --- /dev/null +++ b/pkg/coredata/migrations/20260213T150000Z.sql @@ -0,0 +1,94 @@ +-- Electronic signature enums +CREATE TYPE electronic_signature_document_type AS ENUM ( + 'NDA', 'DPA', 'MSA', 'SOW', 'SLA', 'TOS', 'PRIVACY_POLICY', 'OTHER' +); + +CREATE TYPE electronic_signature_status AS ENUM ( + 'PENDING', + 'ACCEPTED', + 'PROCESSING', + 'COMPLETED', + 'FAILED' +); + +CREATE TYPE electronic_signature_event_type AS ENUM ( + 'DOCUMENT_VIEWED', 'CONSENT_GIVEN', 'FULL_NAME_TYPED', + 'SIGNATURE_ACCEPTED', + 'SIGNATURE_COMPLETED', 'SEAL_COMPUTED', 'TIMESTAMP_REQUESTED', + 'CERTIFICATE_GENERATED', + 'PROCESSING_ERROR' +); + +CREATE TYPE electronic_signature_event_source AS ENUM ('CLIENT', 'SERVER'); + +-- Electronic signatures table +CREATE TABLE electronic_signatures ( + id TEXT PRIMARY KEY, + tenant_id TEXT NOT NULL, + organization_id TEXT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, + status electronic_signature_status NOT NULL DEFAULT 'PENDING', + document_type electronic_signature_document_type NOT NULL, + file_id TEXT NOT NULL REFERENCES files(id), + signer_email CITEXT NOT NULL, + + -- Set at creation time (PENDING): + consent_text TEXT NOT NULL, + + -- Set when status transitions to ACCEPTED (signer submits): + signer_full_name TEXT, + signer_ip_address TEXT, + signer_user_agent TEXT, + + -- Set when status transitions to COMPLETED (worker finishes): + file_hash TEXT, + seal TEXT, + seal_version INT NOT NULL DEFAULT 1, + tsa_token BYTEA, + signed_at TIMESTAMPTZ, + + -- Set by certificate worker after COMPLETED: + certificate_file_id TEXT REFERENCES files(id), + certificate_processing_started_at TIMESTAMPTZ, + + -- Async processing state: + attempt_count INT NOT NULL DEFAULT 0, + max_attempts INT NOT NULL DEFAULT 10, + last_attempted_at TIMESTAMPTZ, + last_error TEXT, + processing_started_at TIMESTAMPTZ, + + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + + -- file_id in the constraint allows re-signing when the org replaces the NDA file. + UNIQUE(organization_id, signer_email, document_type, file_id) +); + +-- Electronic signature events table +CREATE TABLE electronic_signature_events ( + id TEXT PRIMARY KEY, + tenant_id TEXT NOT NULL, + electronic_signature_id TEXT NOT NULL REFERENCES electronic_signatures(id) ON DELETE CASCADE, + event_type electronic_signature_event_type NOT NULL, + event_source electronic_signature_event_source NOT NULL, + actor_email CITEXT NOT NULL, + actor_ip_address TEXT NOT NULL, + actor_user_agent TEXT NOT NULL, + occurred_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL +); + +CREATE INDEX idx_esig_events_signature + ON electronic_signature_events (electronic_signature_id, occurred_at ASC); + +-- Email attachments table (generic, not esign-specific) +CREATE TABLE email_attachments ( + id TEXT PRIMARY KEY, + email_id TEXT NOT NULL REFERENCES emails(id) ON DELETE CASCADE, + file_id TEXT NOT NULL REFERENCES files(id), + filename TEXT NOT NULL, + content_type TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL +); + +CREATE INDEX idx_email_attachments_email_id ON email_attachments (email_id); diff --git a/pkg/crypto/hash/hash.go b/pkg/crypto/hash/hash.go new file mode 100644 index 000000000..7dfe7f6b1 --- /dev/null +++ b/pkg/crypto/hash/hash.go @@ -0,0 +1,25 @@ +// 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 hash + +import ( + "crypto/sha256" + "encoding/hex" +) + +func SHA256Hex(data []byte) string { + h := sha256.Sum256(data) + return hex.EncodeToString(h[:]) +} diff --git a/pkg/esign/certgen.go b/pkg/esign/certgen.go new file mode 100644 index 000000000..37a90d562 --- /dev/null +++ b/pkg/esign/certgen.go @@ -0,0 +1,167 @@ +// 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 esign + +import ( + "bytes" + "context" + _ "embed" + "fmt" + "io" + "strings" + "text/template" + "time" + + "github.com/digitorus/timestamp" + "go.gearno.de/x/ref" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/html2pdf" +) + +type ( + CertificateGenerator struct { + HTML2PDFConverter *html2pdf.Converter + } + + certificateData struct { + SignatureID string + OrganizationID string + SignerFullName string + SignerEmail string + SignerIPAddress string + SignerUserAgent string + DocumentType string + DocumentTypeName string + FileID string + FileHash string + Seal string + SealVersion int + ConsentText string + SignedAt string + TSAAuthority string + TSATime string + TSASerial string + Events []certificateEvent + } + + certificateEvent struct { + EventType string + Source string + Actor string + IPAddress string + OccurredAt string + } +) + +var ( + //go:embed certificate.html.tmpl + certificateTemplateHTML string + + certificateTemplate = template.Must(template.New("certificate").Parse(certificateTemplateHTML)) +) + +func (g *CertificateGenerator) Generate( + ctx context.Context, + signature *coredata.ElectronicSignature, + events coredata.ElectronicSignatureEvents, +) (io.Reader, error) { + data := certificateData{ + SignatureID: signature.ID.String(), + OrganizationID: signature.OrganizationID.String(), + SignerFullName: ref.UnrefOrZero(signature.SignerFullName), + SignerEmail: signature.SignerEmail, + SignerIPAddress: ref.UnrefOrZero(signature.SignerIPAddress), + SignerUserAgent: ref.UnrefOrZero(signature.SignerUserAgent), + DocumentType: signature.DocumentType.String(), + DocumentTypeName: signature.DocumentType.DisplayName(), + FileID: signature.FileID.String(), + FileHash: ref.UnrefOrZero(signature.FileHash), + Seal: ref.UnrefOrZero(signature.Seal), + SealVersion: signature.SealVersion, + ConsentText: signature.ConsentText, + } + + if signature.SignedAt == nil { + return nil, fmt.Errorf("cannot generate certificate: signature %s has no signed_at timestamp", signature.ID) + } + data.SignedAt = signature.SignedAt.UTC().Format(time.RFC3339) + + if len(signature.TSAToken) == 0 { + return nil, fmt.Errorf("cannot generate certificate: signature %s has no TSA token", signature.ID) + } + + tsResp, err := timestamp.ParseResponse(signature.TSAToken) + if err != nil { + return nil, fmt.Errorf("cannot parse TSA token for signature %s: %w", signature.ID, err) + } + + data.TSATime = tsResp.Time.UTC().Format(time.RFC3339) + data.TSASerial = tsResp.SerialNumber.String() + data.TSAAuthority = tsaAuthorityName(tsResp) + + for _, evt := range events { + data.Events = append( + data.Events, + certificateEvent{ + EventType: evt.EventType.String(), + Source: evt.EventSource.String(), + Actor: evt.ActorEmail, + IPAddress: evt.ActorIPAddress, + OccurredAt: evt.OccurredAt.UTC().Format(time.RFC3339), + }, + ) + } + + var htmlBuf bytes.Buffer + if err := certificateTemplate.Execute(&htmlBuf, data); err != nil { + return nil, fmt.Errorf("cannot render certificate template: %w", err) + } + + pdfReader, err := g.HTML2PDFConverter.GeneratePDF( + ctx, + htmlBuf.Bytes(), + html2pdf.RenderConfig{ + PageFormat: html2pdf.PageFormatA4, + Orientation: html2pdf.OrientationPortrait, + MarginTop: html2pdf.NewMarginMillimeters(20), + MarginBottom: html2pdf.NewMarginMillimeters(20), + MarginLeft: html2pdf.NewMarginMillimeters(20), + MarginRight: html2pdf.NewMarginMillimeters(20), + }, + ) + if err != nil { + return nil, fmt.Errorf("cannot generate certificate PDF: %w", err) + } + + return pdfReader, nil +} + +func tsaAuthorityName(ts *timestamp.Timestamp) string { + if len(ts.Certificates) == 0 { + return "" + } + + cert := ts.Certificates[0] + + if len(cert.Subject.Organization) > 0 { + org := strings.Join(cert.Subject.Organization, ", ") + if cert.Subject.CommonName != "" { + return org + " (" + cert.Subject.CommonName + ")" + } + return org + } + + return cert.Subject.CommonName +} diff --git a/pkg/esign/certificate.html.tmpl b/pkg/esign/certificate.html.tmpl new file mode 100644 index 000000000..76c07fedc --- /dev/null +++ b/pkg/esign/certificate.html.tmpl @@ -0,0 +1,520 @@ + + + + + + + + +
+
+
Certificate Of Completion
+
+
+
probo
+
+
+ + +
+
+ Signature Id: {{.SignatureID}} +
+
+ Status: Completed +
+
+ + +
+ Subject: {{.DocumentTypeName}} +
+ + +
+
+
Document Type: {{.DocumentType}}
+
File Hash (SHA-256):
+
{{.FileHash}}
+
+
+   +
+
+
Organization Id:
+
{{.OrganizationID}}
+
File Id:
+
{{.FileID}}
+
+
+ + + + + + + +
Signer EventsTimestamp
+ + + + + +
+
{{.SignerFullName}}
+
{{.SignerEmail}}
+
Security Level: Email, Account Authentication
+
Using IP Address: {{.SignerIPAddress}}
+
+ Electronic Record and Signature Disclosure:
+ Accepted +
+
+
Signed: {{.SignedAt}}
+
+ User Agent: {{.SignerUserAgent}} +
+
+ + + + + + +
Electronic Consent
+ + + + + + + + + + + + + {{range .Events}} + + + + + + {{end}} + +
Signing Summary EventsStatusTimestamps
{{.EventType}}{{.Source}}{{.OccurredAt}}
+ + + + + + +
Integrity Verification
+ + + + + + {{if .TSAAuthority}} + + + + + {{end}} + {{if .TSATime}} + + + + + {{end}} + {{if .TSASerial}} + + + + + {{end}} +
Seal (SHA-256, v{{.SealVersion}}){{.Seal}}
TSA Authority{{.TSAAuthority}}
TSA Timestamp{{.TSATime}}
TSA Serial Number{{.TSASerial}}
+ + + + + + +
Seal Verification (v{{.SealVersion}})
+
+ To independently verify the integrity seal, compute the SHA-256 hash of + the following fields joined by newline characters (\n), + in this exact order: +
    +
  1. Signature ID
  2. +
  3. Organization ID
  4. +
  5. Document Type (the raw value, e.g. NDA)
  6. +
  7. File ID
  8. +
  9. File Hash (SHA-256, lowercase hex)
  10. +
  11. Signer Full Name
  12. +
  13. Signer Email (lowercased)
  14. +
  15. Signer IP Address
  16. +
  17. Signer User Agent
  18. +
  19. Consent Text (the full text shown above)
  20. +
  21. Signed At (UTC, RFC 3339 with nanoseconds, truncated to microsecond precision)
  22. +
+
+ The resulting lowercase hex-encoded SHA-256 digest must match the seal + shown above. +
+
+ + +
+
Electronic Record and Signature Disclosure
+
+

+ From time to time, the organization identified in this certificate (we, us, or + the Company) may be required by law to provide to you certain written notices or + disclosures. Described below are the terms and conditions for providing to you + such notices and disclosures electronically through the Probo platform. Please + read the information below carefully and thoroughly. +

+ +

Withdrawing your consent

+

+ If you decide to receive notices and disclosures from us electronically, you may + at any time change your mind and tell us that you wish to withdraw your consent to + receive notices and disclosures electronically. How you must inform us of your + decision is described below. +

+ +

All notices and disclosures will be sent to you electronically

+

+ Unless you tell us otherwise in accordance with the procedures described herein, we + will provide electronically to you through the Probo platform all required notices, + disclosures, authorizations, acknowledgements, and other documents that are required + to be provided or made available to you during the course of our relationship with + you. To reduce the chance of you inadvertently not receiving any notice or + disclosure, we prefer to provide all of the required notices and disclosures to you + by the same method and to the same address that you have given us. +

+ +

How to contact us

+

+ You may contact us to let us know of your changes as to how we may contact you + electronically, and to withdraw your prior consent to receive notices and + disclosures electronically. To reach us, please send an email to + legal@getprobo.com. +

+ +

To advise us of your new email address

+

+ To let us know of a change in your email address where we should send notices and + disclosures electronically to you, you must send an email message to us at + legal@getprobo.com and in the body of such request you must state your previous + email address and your new email address. We do not require any other information + from you to change your email address. +

+ +

To withdraw your consent

+

+ To inform us that you no longer wish to receive future notices and disclosures in + electronic format you may: +

+
    +
  • + decline to sign a document from within your signing session, and on the + subsequent page, indicate you wish to withdraw your consent; or +
  • +
  • + send us an email to legal@getprobo.com and in the body of such request you + must state your email, full name, mailing address, and telephone number. We do + not need any other information from you to withdraw consent. The consequences of + your withdrawing consent for online documents will be that transactions may take + a longer time to process. +
  • +
+ +

Acknowledging your access and consent to receive and sign documents electronically

+

+ By signing the document electronically through the Probo platform, you confirm + that: +

+
    +
  • + You can access and read this Electronic Record and Signature Disclosure; and +
  • +
  • + You can print on paper this Electronic Record and Signature Disclosure, or save + or send this Electronic Record and Signature Disclosure to a location where you + can print it, for future reference and access; and +
  • +
  • + Until or unless you notify us as described above, you consent to receive + exclusively through electronic means all notices, disclosures, authorizations, + acknowledgements, and other documents that are required to be provided or made + available to you during the course of your relationship with us. +
  • +
+
+
+ + + + + diff --git a/pkg/esign/completion_certificate_worker.go b/pkg/esign/completion_certificate_worker.go new file mode 100644 index 000000000..ce30fbe0d --- /dev/null +++ b/pkg/esign/completion_certificate_worker.go @@ -0,0 +1,375 @@ +// 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 esign + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "go.gearno.de/crypto/uuid" + "go.gearno.de/kit/log" + "go.gearno.de/kit/pg" + "go.gearno.de/x/ref" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/filemanager" + "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/mail" + + emails "go.probo.inc/probo/packages/emails" +) + +// EmailPresenterConfigFunc resolves the emails.PresenterConfig for the +// organization that owns the given trust center. +type EmailPresenterConfigFunc func(ctx context.Context, organizationID gid.GID) (emails.PresenterConfig, error) + +type ( + CompletionCertificateWorker struct { + pg *pg.Client + fileManager *filemanager.Service + certificateGen *CertificateGenerator + presenterConfigFunc EmailPresenterConfigFunc + bucket string + logger *log.Logger + interval time.Duration + staleAfter time.Duration + maxConcurrency int + } + + CompletionCertificateWorkerOption func(*CompletionCertificateWorker) +) + +const ( + certificateFilename = "certificate-of-completion.pdf" +) + +func WithCompletionCertificateWorkerInterval(d time.Duration) CompletionCertificateWorkerOption { + return func(w *CompletionCertificateWorker) { w.interval = d } +} + +func WithCompletionCertificateWorkerStaleAfter(d time.Duration) CompletionCertificateWorkerOption { + return func(w *CompletionCertificateWorker) { w.staleAfter = d } +} + +func WithCompletionCertificateWorkerMaxConcurrency(n int) CompletionCertificateWorkerOption { + return func(w *CompletionCertificateWorker) { + if n > 0 { + w.maxConcurrency = n + } + } +} + +func NewCompletionCertificateWorker( + pgClient *pg.Client, + fileManager *filemanager.Service, + certificateGen *CertificateGenerator, + presenterConfigFunc EmailPresenterConfigFunc, + bucket string, + logger *log.Logger, + opts ...CompletionCertificateWorkerOption, +) *CompletionCertificateWorker { + w := &CompletionCertificateWorker{ + pg: pgClient, + fileManager: fileManager, + certificateGen: certificateGen, + presenterConfigFunc: presenterConfigFunc, + bucket: bucket, + logger: logger, + interval: 10 * time.Second, + staleAfter: 10 * time.Minute, + maxConcurrency: 5, + } + + for _, opt := range opts { + opt(w) + } + + return w +} + +func (w *CompletionCertificateWorker) Run(ctx context.Context) error { + var ( + wg sync.WaitGroup + sem = make(chan struct{}, w.maxConcurrency) + ) + defer wg.Wait() + +LOOP: + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(w.interval): + // From there we should not accept cancelations anymore. + nonCancelableCtx := context.WithoutCancel(ctx) + + w.recoverStaleCertificateRows(nonCancelableCtx) + for { + if err := w.processNext(nonCancelableCtx, sem, &wg); err != nil { + if !errors.Is(err, coredata.ErrResourceNotFound) { + w.logger.ErrorCtx(nonCancelableCtx, "cannot process certificate", log.Error(err)) + } + break + } + } + goto LOOP + } +} + +func (w *CompletionCertificateWorker) processNext(ctx context.Context, sem chan struct{}, wg *sync.WaitGroup) error { + select { + case sem <- struct{}{}: + case <-ctx.Done(): + return ctx.Err() + } + + var ( + signature coredata.ElectronicSignature + now = time.Now() + ) + + if err := w.pg.WithTx( + ctx, + func(tx pg.Conn) error { + if err := signature.LoadNextCompletedWithoutCertificateForUpdate(ctx, tx); err != nil { + return err + } + scope := coredata.NewScopeFromObjectID(signature.ID) + signature.CertificateProcessingStartedAt = &now + signature.UpdatedAt = now + + if err := signature.Update(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot update signature: %w", err) + } + + return nil + }, + ); err != nil { + <-sem + return err + } + + wg.Add(1) + go func(signature coredata.ElectronicSignature) { + defer wg.Done() + defer func() { <-sem }() + + scope := coredata.NewScopeFromObjectID(signature.ID) + + if err := w.generateAndCommit(ctx, &signature); err != nil { + if err := w.handleCertFailure(ctx, &signature, scope, err); err != nil { + w.logger.ErrorCtx(ctx, "cannot handle certificate failure", log.Error(err)) + } + } + }(signature) + + return nil +} + +func (w *CompletionCertificateWorker) generateAndCommit( + ctx context.Context, + signature *coredata.ElectronicSignature, +) error { + var ( + scope = coredata.NewScopeFromObjectID(signature.ID) + ) + + email, attachments, err := w.generateCertificate(ctx, signature, scope) + if err != nil { + return err + } + + if err := w.pg.WithTx( + ctx, + func(tx pg.Conn) error { + signature.CertificateFileID = &attachments[1].FileID + signature.UpdatedAt = time.Now() + if err := signature.Update(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot update signature: %w", err) + } + + event := signature.NewEvent( + coredata.ElectronicSignatureEventTypeCertificateGenerated, + coredata.ElectronicSignatureEventSourceServer, + ) + if err := event.Insert(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot insert certificate event: %w", err) + } + + if err := email.Insert(ctx, tx); err != nil { + return fmt.Errorf("cannot insert certificate email: %w", err) + } + + for _, attachment := range attachments { + if err := attachment.Insert(ctx, tx); err != nil { + return fmt.Errorf("cannot insert email attachment: %w", err) + } + } + + return nil + }, + ); err != nil { + return err + } + + return nil +} + +func (w *CompletionCertificateWorker) generateCertificate( + ctx context.Context, + signature *coredata.ElectronicSignature, + scope coredata.Scoper, +) (*coredata.Email, coredata.EmailAttachments, error) { + var ( + events = coredata.ElectronicSignatureEvents{} + signedFile = coredata.File{} + ) + + if err := w.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := events.LoadBySignatureID(ctx, conn, scope, signature.ID); err != nil { + return fmt.Errorf("cannot load events: %w", err) + } + + if err := signedFile.LoadByID(ctx, conn, scope, signature.FileID); err != nil { + return fmt.Errorf("cannot load signed file: %w", err) + } + + return nil + }, + ); err != nil { + return nil, nil, err + } + + certificatePDFReader, err := w.certificateGen.Generate(ctx, signature, events) + if err != nil { + return nil, nil, fmt.Errorf("cannot generate certificate: %w", err) + } + + certificateOfCompletionFile := coredata.File{ + ID: gid.New(scope.GetTenantID(), coredata.FileEntityType), + OrganizationID: signature.OrganizationID, + BucketName: w.bucket, + MimeType: "application/pdf", + FileName: certificateFilename, + FileKey: uuid.MustNewV4().String(), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + certificateOfCompletionFileSize, err := w.fileManager.PutFile( + ctx, + &certificateOfCompletionFile, + certificatePDFReader, + map[string]string{ + "type": "certificate-of-completion", + "signature-id": signature.ID.String(), + }, + ) + if err != nil { + return nil, nil, fmt.Errorf("cannot upload cert to S3: %w", err) + } + + certificateOfCompletionFile.FileSize = certificateOfCompletionFileSize + + if err := w.pg.WithTx( + ctx, + func(tx pg.Conn) error { + if err := certificateOfCompletionFile.Insert(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot insert certificate of completion file: %w", err) + } + + return nil + }, + ); err != nil { + return nil, nil, err + } + + presenterCfg, err := w.presenterConfigFunc(ctx, signature.OrganizationID) + if err != nil { + return nil, nil, fmt.Errorf("cannot resolve presenter config: %w", err) + } + emailPresenter := emails.NewPresenterFromConfig(w.fileManager, presenterCfg, ref.UnrefOrZero(signature.SignerFullName)) + + docTypeName := signature.DocumentType.DisplayName() + subject, textBody, htmlBody, err := emailPresenter.RenderElectronicSignatureCertificate(ctx, ref.UnrefOrZero(signature.SignerFullName), docTypeName) + if err != nil { + return nil, nil, fmt.Errorf("cannot render email: %w", err) + } + + email := coredata.NewEmail( + ref.UnrefOrZero(signature.SignerFullName), + mail.Addr(signature.SignerEmail), + subject, + textBody, + htmlBody, + ) + + attachments := coredata.EmailAttachments{ + coredata.NewEmailAttachment( + email.ID, + signedFile.ID, + signedFile.FileName, + signedFile.MimeType, + ), + coredata.NewEmailAttachment( + email.ID, + certificateOfCompletionFile.ID, + certificateFilename, + "application/pdf", + ), + } + + return email, attachments, nil +} + +func (w *CompletionCertificateWorker) handleCertFailure( + ctx context.Context, + signature *coredata.ElectronicSignature, + scope coredata.Scoper, + processingError error, +) error { + w.logger.ErrorCtx(ctx, "certificate worker failure", + log.Error(processingError), + log.String("signature_id", signature.ID.String()), + ) + + return w.pg.WithTx( + ctx, + func(tx pg.Conn) error { + signature.CertificateProcessingStartedAt = nil + signature.UpdatedAt = time.Now() + + if err := signature.Update(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot update signature: %w", err) + } + + return nil + }, + ) +} + +func (w *CompletionCertificateWorker) recoverStaleCertificateRows(ctx context.Context) { + if err := w.pg.WithConn( + ctx, + func(conn pg.Conn) error { + return coredata.ResetStaleCertificateProcessing(ctx, conn, w.staleAfter) + }, + ); err != nil { + w.logger.ErrorCtx(ctx, "cannot recover stale certificates", log.Error(err)) + } +} diff --git a/pkg/esign/sealing_worker.go b/pkg/esign/sealing_worker.go new file mode 100644 index 000000000..fb8f29e29 --- /dev/null +++ b/pkg/esign/sealing_worker.go @@ -0,0 +1,344 @@ +// 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 esign + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "go.gearno.de/kit/log" + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/crypto/hash" + "go.probo.inc/probo/pkg/filemanager" +) + +var ( + ErrLoadFile = errors.New("esign: cannot load file") + ErrDownloadPDF = errors.New("esign: cannot download PDF") + ErrComputeSeal = errors.New("esign: cannot compute seal") + ErrTSATimestamp = errors.New("esign: cannot get TSA timestamp") +) + +const ( + currentSealVersion = 1 +) + +type ( + SealingWorker struct { + pg *pg.Client + fileManager *filemanager.Service + tsaClient *TSAClient + logger *log.Logger + interval time.Duration + tsaTimeout time.Duration + staleAfter time.Duration + maxConcurrency int + } + + SealingWorkerOption func(*SealingWorker) +) + +func WithSealingWorkerInterval(d time.Duration) SealingWorkerOption { + return func(w *SealingWorker) { w.interval = d } +} + +func WithSealingWorkerTSATimeout(d time.Duration) SealingWorkerOption { + return func(w *SealingWorker) { w.tsaTimeout = d } +} + +func WithSealingWorkerStaleAfter(d time.Duration) SealingWorkerOption { + return func(w *SealingWorker) { w.staleAfter = d } +} + +func WithSealingWorkerMaxConcurrency(n int) SealingWorkerOption { + return func(w *SealingWorker) { + if n > 0 { + w.maxConcurrency = n + } + } +} + +func NewSealingWorker( + pgClient *pg.Client, + fileManager *filemanager.Service, + tsaClient *TSAClient, + logger *log.Logger, + opts ...SealingWorkerOption, +) *SealingWorker { + w := &SealingWorker{ + pg: pgClient, + fileManager: fileManager, + tsaClient: tsaClient, + logger: logger, + interval: 10 * time.Second, + tsaTimeout: 10 * time.Second, + staleAfter: 5 * time.Minute, + maxConcurrency: 5, + } + + for _, opt := range opts { + opt(w) + } + + return w +} + +func (w *SealingWorker) Run(ctx context.Context) error { + var ( + wg sync.WaitGroup + sem = make(chan struct{}, w.maxConcurrency) + ) + + defer wg.Wait() + +LOOP: + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(w.interval): + // From there we should not accept cancelations anymore. + nonCancelableCtx := context.WithoutCancel(ctx) + + w.recoverStaleRows(nonCancelableCtx) + for { + if err := w.processNext(nonCancelableCtx, sem, &wg); err != nil { + if !errors.Is(err, coredata.ErrResourceNotFound) { + w.logger.ErrorCtx(nonCancelableCtx, "cannot claim signature", log.Error(err)) + } + break + } + } + + goto LOOP + } +} + +func (w *SealingWorker) processNext(ctx context.Context, sem chan struct{}, wg *sync.WaitGroup) error { + select { + case sem <- struct{}{}: + case <-ctx.Done(): + return ctx.Err() + } + + var ( + signature = coredata.ElectronicSignature{} + now = time.Now() + ) + + if err := w.pg.WithTx( + ctx, + func(tx pg.Conn) error { + if err := signature.LoadNextAcceptedForUpdateSkipLocked(ctx, tx); err != nil { + return err + } + + signature.Status = coredata.ElectronicSignatureStatusProcessing + signature.ProcessingStartedAt = &now + signature.AttemptCount++ + signature.LastAttemptedAt = &now + signature.UpdatedAt = now + if err := signature.Update(ctx, tx, coredata.NewNoScope()); err != nil { + return fmt.Errorf("cannot update signature: %w", err) + } + + return nil + }, + ); err != nil { + <-sem + return err + } + + wg.Add(1) + go func(signature coredata.ElectronicSignature) { + defer wg.Done() + defer func() { <-sem }() + + if err := w.sealAndCommit(ctx, &signature); err != nil { + if err := w.failSignature(ctx, &signature, err); err != nil { + w.logger.ErrorCtx(ctx, "cannot fail signature", log.Error(err)) + } + } + }(signature) + + return nil +} + +func (w *SealingWorker) sealAndCommit( + ctx context.Context, + signature *coredata.ElectronicSignature, +) error { + var ( + scope = coredata.NewScopeFromObjectID(signature.ID) + file coredata.File + events []coredata.ElectronicSignatureEvent + ) + + if err := w.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := file.LoadByID(ctx, conn, scope, signature.FileID); err != nil { + return fmt.Errorf("cannot load file: %w", err) + } + + return nil + }, + ); err != nil { + return fmt.Errorf("%w: %w", ErrLoadFile, err) + } + + pdfBytes, err := w.fileManager.GetFileBytes(ctx, &file) + if err != nil { + return fmt.Errorf("%w: %w", ErrDownloadPDF, err) + } + + fileHash := hash.SHA256Hex(pdfBytes) + signature.FileHash = &fileHash + + seal, err := signature.ComputeSeal(currentSealVersion) + if err != nil { + return fmt.Errorf("%w: %w", ErrComputeSeal, err) + } + signature.Seal = &seal + signature.SealVersion = currentSealVersion + events = append( + events, + signature.NewEvent( + coredata.ElectronicSignatureEventTypeSealComputed, + coredata.ElectronicSignatureEventSourceServer, + ), + ) + + tsaCtx, cancel := context.WithTimeout(ctx, w.tsaTimeout) + defer cancel() + tsaToken, err := w.tsaClient.Timestamp(tsaCtx, []byte(seal)) + if err != nil { + return fmt.Errorf("%w: %w", ErrTSATimestamp, err) + } + signature.TSAToken = tsaToken + events = append( + events, + signature.NewEvent( + coredata.ElectronicSignatureEventTypeTimestampRequested, + coredata.ElectronicSignatureEventSourceServer, + ), + ) + + if err := w.pg.WithTx( + ctx, + func(tx pg.Conn) error { + var current coredata.ElectronicSignature + if err := current.LoadByID(ctx, tx, scope, signature.ID); err != nil { + return fmt.Errorf("cannot load signature: %w", err) + } + + if current.Status != coredata.ElectronicSignatureStatusProcessing { + return fmt.Errorf("esign: unexpected status %s, expected PROCESSING", current.Status) + } + + signature.Status = coredata.ElectronicSignatureStatusCompleted + signature.UpdatedAt = time.Now() + if err := signature.Update(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot update signature: %w", err) + } + events = append( + events, + signature.NewEvent( + coredata.ElectronicSignatureEventTypeSignatureCompleted, + coredata.ElectronicSignatureEventSourceServer, + ), + ) + + for i := range events { + if err := events[i].Insert(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot insert %s event: %w", events[i].EventType, err) + } + } + + return nil + }, + ); err != nil { + return fmt.Errorf("cannot commit signing results: %w", err) + } + + return nil +} + +func (w *SealingWorker) failSignature( + ctx context.Context, + signature *coredata.ElectronicSignature, + processingError error, +) error { + scope := coredata.NewScopeFromObjectID(signature.ID) + + w.logger.ErrorCtx(ctx, "sealing worker failure", + log.Error(processingError), + log.String("signature_id", signature.ID.String()), + ) + + return w.pg.WithTx( + ctx, + func(tx pg.Conn) error { + errStr := userFacingError(processingError) + signature.LastError = &errStr + signature.ProcessingStartedAt = nil + signature.UpdatedAt = time.Now() + if signature.AttemptCount >= signature.MaxAttempts { + signature.Status = coredata.ElectronicSignatureStatusFailed + } else { + signature.Status = coredata.ElectronicSignatureStatusAccepted + } + if err := signature.Update(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot update signature: %w", err) + } + + event := signature.NewEvent(coredata.ElectronicSignatureEventTypeProcessingError, coredata.ElectronicSignatureEventSourceServer) + if err := event.Insert(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot insert PROCESSING_ERROR event: %w", err) + } + + return nil + }, + ) +} + +func userFacingError(err error) string { + switch { + case errors.Is(err, ErrTSATimestamp): + return "The timestamp authority is temporarily unavailable." + case errors.Is(err, ErrLoadFile): + return "Unable to load the document for signing." + case errors.Is(err, ErrDownloadPDF): + return "Unable to retrieve the document." + case errors.Is(err, ErrComputeSeal): + return "Unable to generate the cryptographic seal." + default: + return "An unexpected error occurred while processing your signature." + } +} + +func (w *SealingWorker) recoverStaleRows(ctx context.Context) { + if err := w.pg.WithConn( + ctx, + func(conn pg.Conn) error { + return coredata.ResetStaleProcessingSignatures(ctx, conn, w.staleAfter) + }, + ); err != nil { + w.logger.ErrorCtx(ctx, "cannot recover stale signatures", log.Error(err)) + } +} diff --git a/pkg/esign/service.go b/pkg/esign/service.go new file mode 100644 index 000000000..dc8294dda --- /dev/null +++ b/pkg/esign/service.go @@ -0,0 +1,352 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package esign + +import ( + "context" + "fmt" + "strings" + "time" + + "go.gearno.de/kit/httpclient" + "go.gearno.de/kit/log" + "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/filemanager" + "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/html2pdf" + "go.probo.inc/probo/pkg/mail" + "golang.org/x/sync/errgroup" +) + +// Service manages the electronic signature lifecycle. +type ( + Service struct { + pg *pg.Client + fileManager *filemanager.Service + tsaClient *TSAClient + certificateGen *CertificateGenerator + bucket string + logger *log.Logger + } + + CreateSignatureRequest struct { + OrganizationID gid.GID + DocumentType coredata.ElectronicSignatureDocumentType + FileID gid.GID + SignerEmail mail.Addr + ConsentText string // optional; required when DocumentType == OTHER + } + + AcceptSignatureRequest struct { + SignatureID gid.GID + SignerFullName string + SignerEmail mail.Addr + SignerIPAddr string + SignerUA string + } + + RecordEventRequest struct { + SignatureID gid.GID + EventType coredata.ElectronicSignatureEventType + EventSource coredata.ElectronicSignatureEventSource + ActorEmail mail.Addr + ActorIPAddr string + ActorUA string + } +) + +func NewService( + pgClient *pg.Client, + fileManager *filemanager.Service, + html2pdfConverter *html2pdf.Converter, + tsaURL string, + bucket string, + logger *log.Logger, +) *Service { + httpClient := httpclient.DefaultPooledClient( + httpclient.WithLogger(logger), + ) + + return &Service{ + pg: pgClient, + fileManager: fileManager, + tsaClient: &TSAClient{URL: tsaURL, HTTPClient: httpClient}, + certificateGen: &CertificateGenerator{ + HTML2PDFConverter: html2pdfConverter, + }, + bucket: bucket, + logger: logger, + } +} + +func (s *Service) Run(ctx context.Context, presenterConfigFunc EmailPresenterConfigFunc) error { + g := errgroup.Group{} + + sealingWorkerCtx, stopSealingWorker := context.WithCancel(context.Background()) + sealingWorker := NewSealingWorker( + s.pg, + s.fileManager, + s.tsaClient, + s.logger.Named("sealing-worker"), + ) + g.Go(func() error { return sealingWorker.Run(sealingWorkerCtx) }) + + certWorkerCtx, stopCertWorker := context.WithCancel(context.Background()) + certWorker := NewCompletionCertificateWorker( + s.pg, + s.fileManager, + s.certificateGen, + presenterConfigFunc, + s.bucket, + s.logger.Named("completion-certificate-worker"), + ) + g.Go(func() error { return certWorker.Run(certWorkerCtx) }) + + <-ctx.Done() + + stopSealingWorker() + stopCertWorker() + + return g.Wait() +} + +// CreateSignatureRequest contains the parameters for creating a PENDING +// electronic signature. + +// CreateSignature creates a PENDING electronic signature row. The conn +// parameter allows the caller to include this insert inside its own +// transaction. +func (s *Service) CreateSignature( + ctx context.Context, + conn pg.Conn, + req *CreateSignatureRequest, +) (*coredata.ElectronicSignature, error) { + consentText := req.ConsentText + if consentText == "" { + var err error + consentText, err = req.DocumentType.ConsentText() + if err != nil { + return nil, fmt.Errorf("cannot derive consent text: %w", err) + } + } else { + // Caller provided explicit text; append e-sign process consent + // suffix if not already present. + if !strings.HasSuffix(consentText, coredata.ESignProcessConsentText) { + consentText = consentText + " " + coredata.ESignProcessConsentText + } + } + + now := time.Now() + scope := coredata.NewScopeFromObjectID(req.OrganizationID) + + sig := &coredata.ElectronicSignature{ + ID: gid.New(scope.GetTenantID(), coredata.ElectronicSignatureEntityType), + OrganizationID: req.OrganizationID, + Status: coredata.ElectronicSignatureStatusPending, + DocumentType: req.DocumentType, + FileID: req.FileID, + SignerEmail: req.SignerEmail.String(), + ConsentText: consentText, + SealVersion: 1, + AttemptCount: 0, + MaxAttempts: 10, + CreatedAt: now, + UpdatedAt: now, + } + + if err := sig.Insert(ctx, conn, scope); err != nil { + return nil, fmt.Errorf("cannot insert electronic signature: %w", err) + } + + return sig, nil +} + +func (s *Service) AcceptSignature(ctx context.Context, req *AcceptSignatureRequest) error { + var ( + scope = coredata.NewScopeFromObjectID(req.SignatureID) + now = time.Now() + ) + + return s.pg.WithTx( + ctx, + func(tx pg.Conn) error { + var signature coredata.ElectronicSignature + if err := signature.LoadByID(ctx, tx, scope, req.SignatureID); err != nil { + return fmt.Errorf("cannot load electronic signature: %w", err) + } + + if signature.Status != coredata.ElectronicSignatureStatusPending && + signature.Status != coredata.ElectronicSignatureStatusFailed { + return fmt.Errorf("cannot accept electronic signature in status %s", signature.Status) + } + + // If retrying from FAILED, reset attempt tracking. + if signature.Status == coredata.ElectronicSignatureStatusFailed { + signature.AttemptCount = 0 + signature.LastError = nil + } + + signature.SignerFullName = &req.SignerFullName + signature.SignerIPAddress = &req.SignerIPAddr + signature.SignerUserAgent = &req.SignerUA + signature.SignedAt = &now + signature.Status = coredata.ElectronicSignatureStatusAccepted + signature.UpdatedAt = now + + if err := signature.Update(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot update signature: %w", err) + } + + s.recordEvent( + ctx, + tx, + &RecordEventRequest{ + SignatureID: signature.ID, + EventType: coredata.ElectronicSignatureEventTypeSignatureAccepted, + EventSource: coredata.ElectronicSignatureEventSourceServer, + ActorEmail: req.SignerEmail, + ActorIPAddr: req.SignerIPAddr, + ActorUA: req.SignerUA, + }, + ) + + return nil + }, + ) +} + +func (s *Service) RecordEvent(ctx context.Context, req *RecordEventRequest) error { + return s.pg.WithTx( + ctx, + func(tx pg.Conn) error { + return s.recordEvent(ctx, tx, req) + }, + ) +} + +func (s *Service) recordEvent(ctx context.Context, tx pg.Conn, req *RecordEventRequest) error { + var ( + now = time.Now() + scope = coredata.NewScopeFromObjectID(req.SignatureID) + ) + + event := coredata.ElectronicSignatureEvent{ + ID: gid.New(scope.GetTenantID(), coredata.ElectronicSignatureEventEntityType), + ElectronicSignatureID: req.SignatureID, + EventType: req.EventType, + EventSource: req.EventSource, + ActorEmail: req.ActorEmail.String(), + ActorIPAddress: req.ActorIPAddr, + ActorUserAgent: req.ActorUA, + OccurredAt: now, + CreatedAt: now, + } + + if err := event.Insert(ctx, tx, scope); err != nil { + return fmt.Errorf("cannot insert signing event: %w", err) + } + + return nil +} + +func (s *Service) LoadSignatureByID(ctx context.Context, id gid.GID) (*coredata.ElectronicSignature, error) { + var ( + scope = coredata.NewScopeFromObjectID(id) + signature = coredata.ElectronicSignature{} + ) + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := signature.LoadByID(ctx, conn, scope, id); err != nil { + return fmt.Errorf("cannot load electronic signature: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + return &signature, nil +} + +func (s *Service) LoadSignatureByOrgEmailAndDocType( + ctx context.Context, + orgID gid.GID, + email string, + docType coredata.ElectronicSignatureDocumentType, + fileID gid.GID, +) (*coredata.ElectronicSignature, error) { + scope := coredata.NewScopeFromObjectID(orgID) + var sig coredata.ElectronicSignature + err := s.pg.WithConn(ctx, func(conn pg.Conn) error { + return sig.LoadByOrgEmailAndDocType(ctx, conn, scope, orgID, email, docType, fileID) + }) + if err != nil { + return nil, err + } + return &sig, nil +} + +func (s *Service) GenerateCertificateFileURL( + ctx context.Context, + certificateFileID gid.GID, + expiresIn time.Duration, +) (string, error) { + scope := coredata.NewScopeFromObjectID(certificateFileID) + var file coredata.File + err := s.pg.WithConn(ctx, func(conn pg.Conn) error { + return file.LoadByID(ctx, conn, scope, certificateFileID) + }) + if err != nil { + return "", fmt.Errorf("cannot load certificate file: %w", err) + } + + url, err := s.fileManager.GenerateFileUrl(ctx, &file, expiresIn) + if err != nil { + return "", fmt.Errorf("cannot generate certificate file URL: %w", err) + } + + return url, nil +} + +func (s *Service) LoadEventsBySignatureID( + ctx context.Context, + signatureID gid.GID, +) (coredata.ElectronicSignatureEvents, error) { + var ( + scope = coredata.NewScopeFromObjectID(signatureID) + events = coredata.ElectronicSignatureEvents{} + ) + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := events.LoadBySignatureID(ctx, conn, scope, signatureID); err != nil { + return fmt.Errorf("cannot load events: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return events, nil +} diff --git a/pkg/esign/tsa.go b/pkg/esign/tsa.go new file mode 100644 index 000000000..0cce04e0d --- /dev/null +++ b/pkg/esign/tsa.go @@ -0,0 +1,83 @@ +// 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 esign + +import ( + "bytes" + "context" + "crypto" + "fmt" + "io" + "net/http" + + "github.com/digitorus/timestamp" + "go.gearno.de/kit/httpclient" +) + +// TSAClient sends RFC 3161 timestamp requests to a Trusted Timestamp Authority. +type TSAClient struct { + URL string + HTTPClient *http.Client +} + +// Timestamp sends an RFC 3161 TimeStampReq via HTTP POST to the TSA. +// The data parameter is the raw bytes to timestamp (typically the seal hex +// string as UTF-8 bytes). CreateRequest internally computes SHA-256(data) +// to build the MessageImprint. Returns the raw DER-encoded TimeStampResp bytes. +func (c *TSAClient) Timestamp(ctx context.Context, data []byte) ([]byte, error) { + tsReq, err := timestamp.CreateRequest( + bytes.NewReader(data), + ×tamp.RequestOptions{ + Hash: crypto.SHA256, + Certificates: true, + }, + ) + if err != nil { + return nil, fmt.Errorf("esign: cannot create timestamp request: %w", err) + } + + httpClient := c.HTTPClient + if httpClient == nil { + httpClient = httpclient.DefaultPooledClient() + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.URL, bytes.NewReader(tsReq)) + if err != nil { + return nil, fmt.Errorf("esign: cannot build TSA HTTP request: %w", err) + } + req.Header.Set("Content-Type", "application/timestamp-query") + + resp, err := httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("esign: TSA request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("esign: TSA returned HTTP %d", resp.StatusCode) + } + + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("esign: cannot read TSA response: %w", err) + } + + // Validate the response: checks PKIStatus and parses the signed TSTInfo. + if _, err := timestamp.ParseResponse(respBytes); err != nil { + return nil, fmt.Errorf("esign: invalid TSA response: %w", err) + } + + return respBytes, nil +} diff --git a/pkg/filemanager/service.go b/pkg/filemanager/service.go index 5befcedd1..4837f41fc 100644 --- a/pkg/filemanager/service.go +++ b/pkg/filemanager/service.go @@ -76,6 +76,31 @@ func (s *Service) GetFileBase64( return base64Data, mimeType, nil } +// GetFileBytes downloads a file from S3 and returns the raw bytes. +func (s *Service) GetFileBytes( + ctx context.Context, + file File, +) ([]byte, error) { + result, err := s.s3Client.GetObject( + ctx, + &s3.GetObjectInput{ + Bucket: aws.String(file.GetBucketName()), + Key: aws.String(file.GetObjectKey()), + }, + ) + if err != nil { + return nil, fmt.Errorf("cannot get file from S3: %w", err) + } + defer func() { _ = result.Body.Close() }() + + data, err := io.ReadAll(result.Body) + if err != nil { + return nil, fmt.Errorf("cannot read file data: %w", err) + } + + return data, nil +} + func (s *Service) GetFileSize(content io.Reader) (int64, error) { seeker, ok := content.(io.Seeker) if !ok { diff --git a/pkg/mailer/mailer.go b/pkg/mailer/mailer.go index 55a712b08..8fc937419 100644 --- a/pkg/mailer/mailer.go +++ b/pkg/mailer/mailer.go @@ -24,18 +24,20 @@ import ( "net/smtp" "time" - "go.probo.inc/probo/pkg/coredata" "github.com/jhillyerd/enmime" "go.gearno.de/kit/log" "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/filemanager" ) type ( Mailer struct { - pg *pg.Client - l *log.Logger - cfg Config - interval time.Duration + pg *pg.Client + fileManager *filemanager.Service + l *log.Logger + cfg Config + interval time.Duration } Config struct { @@ -50,7 +52,7 @@ type ( } ) -func NewMailer(pg *pg.Client, l *log.Logger, cfg Config) *Mailer { +func NewMailer(pg *pg.Client, fileManager *filemanager.Service, l *log.Logger, cfg Config) *Mailer { // Set a default timeout if not provided if cfg.Timeout == 0 { cfg.Timeout = 10 * time.Second @@ -61,7 +63,7 @@ func NewMailer(pg *pg.Client, l *log.Logger, cfg Config) *Mailer { cfg.Interval = 60 * time.Second } - return &Mailer{pg: pg, l: l, cfg: cfg, interval: cfg.Interval} + return &Mailer{pg: pg, fileManager: fileManager, l: l, cfg: cfg, interval: cfg.Interval} } func (m *Mailer) Run(ctx context.Context) error { @@ -151,6 +153,11 @@ func (m *Mailer) batchSendEmails(ctx context.Context) error { return err } + var attachments coredata.EmailAttachments + if err := attachments.LoadByEmailID(ctx, tx, email.ID); err != nil { + return fmt.Errorf("cannot load email attachments: %w", err) + } + mail := enmime.Builder(). Subject(email.Subject). From(m.cfg.SenderName, m.cfg.SenderEmail). @@ -161,6 +168,22 @@ func (m *Mailer) batchSendEmails(ctx context.Context) error { mail = mail.HTML([]byte(*email.HtmlBody)) } + for _, att := range attachments { + var file coredata.File + if err := file.LoadByID(ctx, tx, coredata.NewNoScope(), att.FileID); err != nil { + return fmt.Errorf("cannot load file record for attachment %s: %w", att.Filename, err) + } + + // TODO use reader + data, err := m.fileManager.GetFileBytes(ctx, &file) + if err != nil { + return fmt.Errorf("cannot download attachment %s: %w", att.Filename, err) + } + + // TODO [esign] maybe use AddAttachmentWithReader instead? + mail = mail.AddAttachment(data, att.ContentType, att.Filename) + } + envelope, err := mail.Build() if err != nil { return fmt.Errorf("cannot build email: %w", err) diff --git a/pkg/probo/service.go b/pkg/probo/service.go index ae117f5bf..14e611bd6 100644 --- a/pkg/probo/service.go +++ b/pkg/probo/service.go @@ -27,6 +27,7 @@ import ( "go.probo.inc/probo/pkg/certmanager" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/crypto/cipher" + "go.probo.inc/probo/pkg/esign" "go.probo.inc/probo/pkg/filemanager" "go.probo.inc/probo/pkg/filevalidation" "go.probo.inc/probo/pkg/gid" @@ -61,6 +62,7 @@ type ( fileManager *filemanager.Service logger *log.Logger slack *slack.Service + esign *esign.Service } TenantService struct { @@ -73,6 +75,7 @@ type ( tokenSecret string agent *agents.Agent fileManager *filemanager.Service + esign *esign.Service Frameworks *FrameworkService Measures *MeasureService Tasks *TaskService @@ -128,6 +131,7 @@ func NewService( logger *log.Logger, slackService *slack.Service, iamService *iam.Service, + esignService *esign.Service, ) (*Service, error) { if bucket == "" { return nil, fmt.Errorf("bucket is required") @@ -148,6 +152,7 @@ func NewService( fileManager: fileManagerService, logger: logger, slack: slackService, + esign: esignService, } return svc, nil @@ -164,6 +169,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService { tokenSecret: s.tokenSecret, agent: agents.NewAgent(nil, s.agentConfig), fileManager: s.fileManager, + esign: s.esign, } tenantService.Frameworks = &FrameworkService{ diff --git a/pkg/probo/trust_center_access_service.go b/pkg/probo/trust_center_access_service.go index 68eeaafcc..288f1d050 100644 --- a/pkg/probo/trust_center_access_service.go +++ b/pkg/probo/trust_center_access_service.go @@ -23,6 +23,7 @@ import ( "go.gearno.de/kit/pg" "go.probo.inc/probo/packages/emails" "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/esign" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/mail" "go.probo.inc/probo/pkg/page" @@ -253,6 +254,23 @@ func (s TrustCenterAccessService) Create( return fmt.Errorf("cannot insert trust center access: %w", err) } + if trustCenter.NonDisclosureAgreementFileID != nil && s.svc.esign != nil { + _, err := s.svc.esign.CreateSignature( + ctx, + tx, + &esign.CreateSignatureRequest{ + OrganizationID: access.OrganizationID, + DocumentType: coredata.ElectronicSignatureDocumentTypeNDA, + FileID: *trustCenter.NonDisclosureAgreementFileID, + SignerEmail: access.Email, + }, + ) + + if err != nil { + return fmt.Errorf("cannot create pending signature: %w", err) + } + } + return nil }, ) diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index 13a5a5b2b..0eb563407 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -51,6 +51,7 @@ import ( "go.probo.inc/probo/pkg/crypto/cipher" "go.probo.inc/probo/pkg/crypto/keys" "go.probo.inc/probo/pkg/crypto/passwdhash" + "go.probo.inc/probo/pkg/esign" "go.probo.inc/probo/pkg/filemanager" "go.probo.inc/probo/pkg/html2pdf" "go.probo.inc/probo/pkg/iam" @@ -69,6 +70,10 @@ type ( cfg config } + esignConfig struct { + TSAURL string `json:"tsa-url"` + } + config struct { BaseURL *baseurl.BaseURL `json:"base-url"` EncryptionKey cipher.EncryptionKey `json:"encryption-key"` @@ -83,6 +88,7 @@ type ( ChromeDPAddr string `json:"chrome-dp-addr"` CustomDomains customDomainsConfig `json:"custom-domains"` SCIMBridge scimBridgeConfig `json:"scim-bridge"` + ESign esignConfig `json:"esign"` } trustCenterConfig struct { @@ -174,6 +180,9 @@ func New() *Implm { SyncInterval: 60, // 15 minutes PollInterval: 30, // 30 seconds }, + ESign: esignConfig{ + TSAURL: "http://timestamp.digicert.com", + }, }, } } @@ -375,6 +384,15 @@ func (impl *Implm) Run( l.Named("slack"), ) + esignService := esign.NewService( + pgClient, + fileManagerService, + html2pdfConverter, + impl.cfg.ESign.TSAURL, + impl.cfg.AWS.Bucket, + l.Named("esign"), + ) + proboService, err := probo.NewService( ctx, impl.cfg.EncryptionKey, @@ -390,6 +408,7 @@ func (impl *Implm) Run( l.Named("probo"), slackService, iamService, + esignService, ) if err != nil { return fmt.Errorf("cannot create probo service: %w", err) @@ -403,6 +422,7 @@ func (impl *Implm) Run( impl.cfg.EncryptionKey, impl.cfg.GetSlackSigningSecret(), iamService, + esignService, html2pdfConverter, fileManagerService, l, @@ -416,6 +436,7 @@ func (impl *Implm) Run( Probo: proboService, IAM: iamService, Trust: trustService, + ESign: esignService, Slack: slackService, ConnectorRegistry: defaultConnectorRegistry, BaseURL: impl.cfg.BaseURL, @@ -452,6 +473,7 @@ func (impl *Implm) Run( mailerCtx, stopMailer := context.WithCancel(context.Background()) mailer := mailer.NewMailer( pgClient, + fileManagerService, l, mailer.Config{ SenderEmail: impl.cfg.Notifications.Mailer.SenderEmail, @@ -516,6 +538,15 @@ func (impl *Implm) Run( }, ) + esignServiceCtx, stopESignService := context.WithCancel(context.Background()) + wg.Go( + func() { + if err := esignService.Run(esignServiceCtx, trustService.EmailPresenterConfigByOrganizationID); err != nil { + cancel(fmt.Errorf("esign service crashed: %w", err)) + } + }, + ) + trustCenterServerCtx, stopTrustCenterServer := context.WithCancel(context.Background()) defer stopTrustCenterServer() wg.Go( @@ -528,13 +559,14 @@ func (impl *Implm) Run( <-ctx.Done() - stopMailer() - stopSlackSender() - stopWebhookSender() - stopExportJobExporter() - stopIAMService() stopApiServer() stopTrustCenterServer() + stopWebhookSender() + stopESignService() + stopExportJobExporter() + stopIAMService() + stopMailer() + stopSlackSender() wg.Wait() diff --git a/pkg/server/api/api.go b/pkg/server/api/api.go index eb627dde0..b5bfe2a7f 100644 --- a/pkg/server/api/api.go +++ b/pkg/server/api/api.go @@ -25,6 +25,7 @@ import ( "go.gearno.de/kit/log" "go.probo.inc/probo/pkg/baseurl" "go.probo.inc/probo/pkg/connector" + "go.probo.inc/probo/pkg/esign" "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/probo" "go.probo.inc/probo/pkg/securecookie" @@ -44,6 +45,7 @@ type ( Probo *probo.Service IAM *iam.Service Trust *trust.Service + ESign *esign.Service Slack *slack.Service Cookie securecookie.Config TokenSecret string @@ -117,6 +119,7 @@ func NewServer(cfg Config) (*Server, error) { cfg.Logger.Named("trust.v1"), cfg.IAM, cfg.Trust, + cfg.ESign, cfg.Cookie, cfg.BaseURL, ), @@ -124,6 +127,7 @@ func NewServer(cfg Config) (*Server, error) { cfg.Logger.Named("console.v1"), cfg.Probo, cfg.IAM, + cfg.ESign, cfg.Cookie, cfg.TokenSecret, cfg.ConnectorRegistry, diff --git a/pkg/server/api/console/v1/graphql_handler.go b/pkg/server/api/console/v1/graphql_handler.go index f5d15c29e..0bb0f8682 100644 --- a/pkg/server/api/console/v1/graphql_handler.go +++ b/pkg/server/api/console/v1/graphql_handler.go @@ -18,6 +18,7 @@ import ( "net/http" "go.gearno.de/kit/log" + "go.probo.inc/probo/pkg/esign" "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/probo" "go.probo.inc/probo/pkg/server/api/authz" @@ -25,12 +26,13 @@ import ( "go.probo.inc/probo/pkg/server/gqlutils" ) -func NewGraphQLHandler(iamSvc *iam.Service, proboSvc *probo.Service, customDomainCname string, logger *log.Logger) http.Handler { +func NewGraphQLHandler(iamSvc *iam.Service, proboSvc *probo.Service, esignSvc *esign.Service, customDomainCname string, logger *log.Logger) http.Handler { config := schema.Config{ Resolvers: &Resolver{ authorize: authz.NewAuthorizeFunc(iamSvc, logger), probo: proboSvc, iam: iamSvc, + esign: esignSvc, customDomainCname: customDomainCname, logger: logger, }, diff --git a/pkg/server/api/console/v1/resolver.go b/pkg/server/api/console/v1/resolver.go index 3a3dc2eea..3702f358d 100644 --- a/pkg/server/api/console/v1/resolver.go +++ b/pkg/server/api/console/v1/resolver.go @@ -31,6 +31,7 @@ import ( "go.probo.inc/probo/pkg/baseurl" "go.probo.inc/probo/pkg/connector" "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/esign" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/probo" @@ -47,6 +48,7 @@ type ( authorize authz.AuthorizeFunc probo *probo.Service iam *iam.Service + esign *esign.Service logger *log.Logger customDomainCname string } @@ -56,6 +58,7 @@ func NewMux( logger *log.Logger, proboSvc *probo.Service, iamSvc *iam.Service, + esignSvc *esign.Service, cookieConfig securecookie.Config, tokenSecret string, connectorRegistry *connector.ConnectorRegistry, @@ -66,7 +69,7 @@ func NewMux( safeRedirect := &saferedirect.SafeRedirect{AllowedHost: baseURL.Host()} - graphqlHandler := NewGraphQLHandler(iamSvc, proboSvc, customDomainCname, logger) + graphqlHandler := NewGraphQLHandler(iamSvc, proboSvc, esignSvc, customDomainCname, logger) r.Group(func(r chi.Router) { r.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig)) diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 5fb434b4b..82bc029d4 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -2714,6 +2714,7 @@ type TrustCenterAccess implements Node { name: String! state: TrustCenterAccessState! hasAcceptedNonDisclosureAgreement: Boolean! + ndaSignature: ElectronicSignature @goField(forceResolver: true) createdAt: Datetime! updatedAt: Datetime! lastTokenExpiresAt: Datetime @@ -5387,3 +5388,149 @@ type CreateCustomDomainPayload { type DeleteCustomDomainPayload { deletedCustomDomainId: ID! } + +# Electronic Signature + +enum ElectronicSignatureStatus + @goModel( + model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatus" + ) { + PENDING + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusPending" + ) + ACCEPTED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusAccepted" + ) + PROCESSING + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusProcessing" + ) + COMPLETED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusCompleted" + ) + FAILED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusFailed" + ) +} + +enum ElectronicSignatureDocumentType + @goModel( + model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentType" + ) { + NDA + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeNDA" + ) + DPA + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeDPA" + ) + MSA + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeMSA" + ) + SOW + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeSOW" + ) + SLA + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeSLA" + ) + TOS + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeTOS" + ) + PRIVACY_POLICY + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypePrivacyPolicy" + ) + OTHER + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeOther" + ) +} + +enum ElectronicSignatureEventType + @goModel( + model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventType" + ) { + DOCUMENT_VIEWED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeDocumentViewed" + ) + CONSENT_GIVEN + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeConsentGiven" + ) + FULL_NAME_TYPED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeFullNameTyped" + ) + SIGNATURE_ACCEPTED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeSignatureAccepted" + ) + SIGNATURE_COMPLETED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeSignatureCompleted" + ) + SEAL_COMPUTED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeSealComputed" + ) + TIMESTAMP_REQUESTED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeTimestampRequested" + ) + CERTIFICATE_GENERATED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeCertificateGenerated" + ) + PROCESSING_ERROR + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeProcessingError" + ) +} + +enum ElectronicSignatureEventSource + @goModel( + model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventSource" + ) { + CLIENT + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventSourceClient" + ) + SERVER + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventSourceServer" + ) +} + +type ElectronicSignature implements Node { + id: ID! + status: ElectronicSignatureStatus! + documentType: ElectronicSignatureDocumentType! + consentText: String! + lastError: String + signedAt: Datetime + certificateFileUrl: String @goField(forceResolver: true) + events: [ElectronicSignatureEvent!]! @goField(forceResolver: true) + createdAt: Datetime! + updatedAt: Datetime! +} + +type ElectronicSignatureEvent { + id: ID! + eventType: ElectronicSignatureEventType! + eventSource: ElectronicSignatureEventSource! + actorEmail: String! + actorIpAddress: String! + actorUserAgent: String! + occurredAt: Datetime! + createdAt: Datetime! +} diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index cac7f0f20..2b52e4ac9 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -68,6 +68,7 @@ type ResolverRoot interface { DocumentVersionConnection() DocumentVersionConnectionResolver DocumentVersionSignature() DocumentVersionSignatureResolver DocumentVersionSignatureConnection() DocumentVersionSignatureConnectionResolver + ElectronicSignature() ElectronicSignatureResolver Evidence() EvidenceResolver EvidenceConnection() EvidenceConnectionResolver File() FileResolver @@ -779,6 +780,30 @@ type ComplexityRoot struct { Node func(childComplexity int) int } + ElectronicSignature struct { + CertificateFileURL func(childComplexity int) int + ConsentText func(childComplexity int) int + CreatedAt func(childComplexity int) int + DocumentType func(childComplexity int) int + Events func(childComplexity int) int + ID func(childComplexity int) int + LastError func(childComplexity int) int + SignedAt func(childComplexity int) int + Status func(childComplexity int) int + UpdatedAt func(childComplexity int) int + } + + ElectronicSignatureEvent struct { + ActorEmail func(childComplexity int) int + ActorIPAddress func(childComplexity int) int + ActorUserAgent func(childComplexity int) int + CreatedAt func(childComplexity int) int + EventSource func(childComplexity int) int + EventType func(childComplexity int) int + ID func(childComplexity int) int + OccurredAt func(childComplexity int) int + } + Evidence struct { CreatedAt func(childComplexity int) int Description func(childComplexity int) int @@ -1516,6 +1541,7 @@ type ComplexityRoot struct { ID func(childComplexity int) int LastTokenExpiresAt func(childComplexity int) int Name func(childComplexity int) int + NdaSignature func(childComplexity int) int PendingRequestCount func(childComplexity int) int Permission func(childComplexity int, action string) int State func(childComplexity int) int @@ -2083,6 +2109,10 @@ type DocumentVersionSignatureResolver interface { type DocumentVersionSignatureConnectionResolver interface { TotalCount(ctx context.Context, obj *types.DocumentVersionSignatureConnection) (int, error) } +type ElectronicSignatureResolver interface { + CertificateFileURL(ctx context.Context, obj *types.ElectronicSignature) (*string, error) + Events(ctx context.Context, obj *types.ElectronicSignature) ([]*types.ElectronicSignatureEvent, error) +} type EvidenceResolver interface { File(ctx context.Context, obj *types.Evidence) (*types.File, error) @@ -2432,6 +2462,8 @@ type TrustCenterResolver interface { Permission(ctx context.Context, obj *types.TrustCenter, action string) (bool, error) } type TrustCenterAccessResolver interface { + NdaSignature(ctx context.Context, obj *types.TrustCenterAccess) (*types.ElectronicSignature, error) + PendingRequestCount(ctx context.Context, obj *types.TrustCenterAccess) (int, error) ActiveCount(ctx context.Context, obj *types.TrustCenterAccess) (int, error) AvailableDocumentAccesses(ctx context.Context, obj *types.TrustCenterAccess, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.OrderBy[coredata.TrustCenterDocumentAccessOrderField]) (*types.TrustCenterDocumentAccessConnection, error) @@ -4555,6 +4587,116 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.DocumentVersionSignatureEdge.Node(childComplexity), true + case "ElectronicSignature.certificateFileUrl": + if e.complexity.ElectronicSignature.CertificateFileURL == nil { + break + } + + return e.complexity.ElectronicSignature.CertificateFileURL(childComplexity), true + case "ElectronicSignature.consentText": + if e.complexity.ElectronicSignature.ConsentText == nil { + break + } + + return e.complexity.ElectronicSignature.ConsentText(childComplexity), true + case "ElectronicSignature.createdAt": + if e.complexity.ElectronicSignature.CreatedAt == nil { + break + } + + return e.complexity.ElectronicSignature.CreatedAt(childComplexity), true + case "ElectronicSignature.documentType": + if e.complexity.ElectronicSignature.DocumentType == nil { + break + } + + return e.complexity.ElectronicSignature.DocumentType(childComplexity), true + case "ElectronicSignature.events": + if e.complexity.ElectronicSignature.Events == nil { + break + } + + return e.complexity.ElectronicSignature.Events(childComplexity), true + case "ElectronicSignature.id": + if e.complexity.ElectronicSignature.ID == nil { + break + } + + return e.complexity.ElectronicSignature.ID(childComplexity), true + case "ElectronicSignature.lastError": + if e.complexity.ElectronicSignature.LastError == nil { + break + } + + return e.complexity.ElectronicSignature.LastError(childComplexity), true + case "ElectronicSignature.signedAt": + if e.complexity.ElectronicSignature.SignedAt == nil { + break + } + + return e.complexity.ElectronicSignature.SignedAt(childComplexity), true + case "ElectronicSignature.status": + if e.complexity.ElectronicSignature.Status == nil { + break + } + + return e.complexity.ElectronicSignature.Status(childComplexity), true + case "ElectronicSignature.updatedAt": + if e.complexity.ElectronicSignature.UpdatedAt == nil { + break + } + + return e.complexity.ElectronicSignature.UpdatedAt(childComplexity), true + + case "ElectronicSignatureEvent.actorEmail": + if e.complexity.ElectronicSignatureEvent.ActorEmail == nil { + break + } + + return e.complexity.ElectronicSignatureEvent.ActorEmail(childComplexity), true + case "ElectronicSignatureEvent.actorIpAddress": + if e.complexity.ElectronicSignatureEvent.ActorIPAddress == nil { + break + } + + return e.complexity.ElectronicSignatureEvent.ActorIPAddress(childComplexity), true + case "ElectronicSignatureEvent.actorUserAgent": + if e.complexity.ElectronicSignatureEvent.ActorUserAgent == nil { + break + } + + return e.complexity.ElectronicSignatureEvent.ActorUserAgent(childComplexity), true + case "ElectronicSignatureEvent.createdAt": + if e.complexity.ElectronicSignatureEvent.CreatedAt == nil { + break + } + + return e.complexity.ElectronicSignatureEvent.CreatedAt(childComplexity), true + case "ElectronicSignatureEvent.eventSource": + if e.complexity.ElectronicSignatureEvent.EventSource == nil { + break + } + + return e.complexity.ElectronicSignatureEvent.EventSource(childComplexity), true + case "ElectronicSignatureEvent.eventType": + if e.complexity.ElectronicSignatureEvent.EventType == nil { + break + } + + return e.complexity.ElectronicSignatureEvent.EventType(childComplexity), true + case "ElectronicSignatureEvent.id": + if e.complexity.ElectronicSignatureEvent.ID == nil { + break + } + + return e.complexity.ElectronicSignatureEvent.ID(childComplexity), true + case "ElectronicSignatureEvent.occurredAt": + if e.complexity.ElectronicSignatureEvent.OccurredAt == nil { + break + } + + return e.complexity.ElectronicSignatureEvent.OccurredAt(childComplexity), true + case "Evidence.createdAt": if e.complexity.Evidence.CreatedAt == nil { break @@ -8759,6 +8901,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.TrustCenterAccess.Name(childComplexity), true + case "TrustCenterAccess.ndaSignature": + if e.complexity.TrustCenterAccess.NdaSignature == nil { + break + } + + return e.complexity.TrustCenterAccess.NdaSignature(childComplexity), true case "TrustCenterAccess.pendingRequestCount": if e.complexity.TrustCenterAccess.PendingRequestCount == nil { break @@ -13272,6 +13420,7 @@ type TrustCenterAccess implements Node { name: String! state: TrustCenterAccessState! hasAcceptedNonDisclosureAgreement: Boolean! + ndaSignature: ElectronicSignature @goField(forceResolver: true) createdAt: Datetime! updatedAt: Datetime! lastTokenExpiresAt: Datetime @@ -15945,6 +16094,152 @@ type CreateCustomDomainPayload { type DeleteCustomDomainPayload { deletedCustomDomainId: ID! } + +# Electronic Signature + +enum ElectronicSignatureStatus + @goModel( + model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatus" + ) { + PENDING + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusPending" + ) + ACCEPTED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusAccepted" + ) + PROCESSING + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusProcessing" + ) + COMPLETED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusCompleted" + ) + FAILED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusFailed" + ) +} + +enum ElectronicSignatureDocumentType + @goModel( + model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentType" + ) { + NDA + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeNDA" + ) + DPA + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeDPA" + ) + MSA + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeMSA" + ) + SOW + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeSOW" + ) + SLA + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeSLA" + ) + TOS + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeTOS" + ) + PRIVACY_POLICY + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypePrivacyPolicy" + ) + OTHER + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeOther" + ) +} + +enum ElectronicSignatureEventType + @goModel( + model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventType" + ) { + DOCUMENT_VIEWED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeDocumentViewed" + ) + CONSENT_GIVEN + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeConsentGiven" + ) + FULL_NAME_TYPED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeFullNameTyped" + ) + SIGNATURE_ACCEPTED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeSignatureAccepted" + ) + SIGNATURE_COMPLETED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeSignatureCompleted" + ) + SEAL_COMPUTED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeSealComputed" + ) + TIMESTAMP_REQUESTED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeTimestampRequested" + ) + CERTIFICATE_GENERATED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeCertificateGenerated" + ) + PROCESSING_ERROR + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeProcessingError" + ) +} + +enum ElectronicSignatureEventSource + @goModel( + model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventSource" + ) { + CLIENT + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventSourceClient" + ) + SERVER + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventSourceServer" + ) +} + +type ElectronicSignature implements Node { + id: ID! + status: ElectronicSignatureStatus! + documentType: ElectronicSignatureDocumentType! + consentText: String! + lastError: String + signedAt: Datetime + certificateFileUrl: String @goField(forceResolver: true) + events: [ElectronicSignatureEvent!]! @goField(forceResolver: true) + createdAt: Datetime! + updatedAt: Datetime! +} + +type ElectronicSignatureEvent { + id: ID! + eventType: ElectronicSignatureEventType! + eventSource: ElectronicSignatureEventSource! + actorEmail: String! + actorIpAddress: String! + actorUserAgent: String! + occurredAt: Datetime! + createdAt: Datetime! +} `, BuiltIn: false}, } var parsedSchema = gqlparser.MustLoadSchema(sources...) @@ -30822,6 +31117,546 @@ func (ec *executionContext) fieldContext_DocumentVersionSignatureEdge_node(_ con return fc, nil } +func (ec *executionContext) _ElectronicSignature_id(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_id, + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + ec.marshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + 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) _ElectronicSignature_status(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_status, + func(ctx context.Context) (any, error) { + return obj.Status, nil + }, + nil, + ec.marshalNElectronicSignatureStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureStatus, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_status(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ElectronicSignatureStatus does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ElectronicSignature_documentType(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_documentType, + func(ctx context.Context) (any, error) { + return obj.DocumentType, nil + }, + nil, + ec.marshalNElectronicSignatureDocumentType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureDocumentType, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_documentType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ElectronicSignatureDocumentType does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ElectronicSignature_consentText(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_consentText, + func(ctx context.Context) (any, error) { + return obj.ConsentText, nil + }, + nil, + ec.marshalNString2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_consentText(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + 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) _ElectronicSignature_lastError(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_lastError, + func(ctx context.Context) (any, error) { + return obj.LastError, nil + }, + nil, + ec.marshalOString2ᚖstring, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_lastError(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + 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) _ElectronicSignature_signedAt(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_signedAt, + func(ctx context.Context) (any, error) { + return obj.SignedAt, nil + }, + nil, + ec.marshalODatetime2ᚖtimeᚐTime, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_signedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + 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) _ElectronicSignature_certificateFileUrl(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_certificateFileUrl, + func(ctx context.Context) (any, error) { + return ec.resolvers.ElectronicSignature().CertificateFileURL(ctx, obj) + }, + nil, + ec.marshalOString2ᚖstring, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_certificateFileUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ElectronicSignature_events(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_events, + func(ctx context.Context) (any, error) { + return ec.resolvers.ElectronicSignature().Events(ctx, obj) + }, + nil, + ec.marshalNElectronicSignatureEvent2ᚕᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐElectronicSignatureEventᚄ, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_events(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_ElectronicSignatureEvent_id(ctx, field) + case "eventType": + return ec.fieldContext_ElectronicSignatureEvent_eventType(ctx, field) + case "eventSource": + return ec.fieldContext_ElectronicSignatureEvent_eventSource(ctx, field) + case "actorEmail": + return ec.fieldContext_ElectronicSignatureEvent_actorEmail(ctx, field) + case "actorIpAddress": + return ec.fieldContext_ElectronicSignatureEvent_actorIpAddress(ctx, field) + case "actorUserAgent": + return ec.fieldContext_ElectronicSignatureEvent_actorUserAgent(ctx, field) + case "occurredAt": + return ec.fieldContext_ElectronicSignatureEvent_occurredAt(ctx, field) + case "createdAt": + return ec.fieldContext_ElectronicSignatureEvent_createdAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ElectronicSignatureEvent", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _ElectronicSignature_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_createdAt, + func(ctx context.Context) (any, error) { + return obj.CreatedAt, nil + }, + nil, + ec.marshalNDatetime2timeᚐTime, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + 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) _ElectronicSignature_updatedAt(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_updatedAt, + func(ctx context.Context) (any, error) { + return obj.UpdatedAt, nil + }, + nil, + ec.marshalNDatetime2timeᚐTime, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + 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) _ElectronicSignatureEvent_id(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignatureEvent) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignatureEvent_id, + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + ec.marshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignatureEvent_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignatureEvent", + 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) _ElectronicSignatureEvent_eventType(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignatureEvent) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignatureEvent_eventType, + func(ctx context.Context) (any, error) { + return obj.EventType, nil + }, + nil, + ec.marshalNElectronicSignatureEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventType, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignatureEvent_eventType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignatureEvent", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ElectronicSignatureEventType does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ElectronicSignatureEvent_eventSource(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignatureEvent) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignatureEvent_eventSource, + func(ctx context.Context) (any, error) { + return obj.EventSource, nil + }, + nil, + ec.marshalNElectronicSignatureEventSource2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventSource, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignatureEvent_eventSource(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignatureEvent", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ElectronicSignatureEventSource does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ElectronicSignatureEvent_actorEmail(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignatureEvent) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignatureEvent_actorEmail, + func(ctx context.Context) (any, error) { + return obj.ActorEmail, nil + }, + nil, + ec.marshalNString2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignatureEvent_actorEmail(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignatureEvent", + 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) _ElectronicSignatureEvent_actorIpAddress(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignatureEvent) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignatureEvent_actorIpAddress, + func(ctx context.Context) (any, error) { + return obj.ActorIPAddress, nil + }, + nil, + ec.marshalNString2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignatureEvent_actorIpAddress(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignatureEvent", + 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) _ElectronicSignatureEvent_actorUserAgent(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignatureEvent) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignatureEvent_actorUserAgent, + func(ctx context.Context) (any, error) { + return obj.ActorUserAgent, nil + }, + nil, + ec.marshalNString2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignatureEvent_actorUserAgent(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignatureEvent", + 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) _ElectronicSignatureEvent_occurredAt(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignatureEvent) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignatureEvent_occurredAt, + func(ctx context.Context) (any, error) { + return obj.OccurredAt, nil + }, + nil, + ec.marshalNDatetime2timeᚐTime, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignatureEvent_occurredAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignatureEvent", + 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) _ElectronicSignatureEvent_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignatureEvent) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignatureEvent_createdAt, + func(ctx context.Context) (any, error) { + return obj.CreatedAt, nil + }, + nil, + ec.marshalNDatetime2timeᚐTime, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignatureEvent_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignatureEvent", + 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) _Evidence_id(ctx context.Context, field graphql.CollectedField, obj *types.Evidence) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -51548,6 +52383,57 @@ func (ec *executionContext) fieldContext_TrustCenterAccess_hasAcceptedNonDisclos return fc, nil } +func (ec *executionContext) _TrustCenterAccess_ndaSignature(ctx context.Context, field graphql.CollectedField, obj *types.TrustCenterAccess) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_TrustCenterAccess_ndaSignature, + func(ctx context.Context) (any, error) { + return ec.resolvers.TrustCenterAccess().NdaSignature(ctx, obj) + }, + nil, + ec.marshalOElectronicSignature2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐElectronicSignature, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_TrustCenterAccess_ndaSignature(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TrustCenterAccess", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_ElectronicSignature_id(ctx, field) + case "status": + return ec.fieldContext_ElectronicSignature_status(ctx, field) + case "documentType": + return ec.fieldContext_ElectronicSignature_documentType(ctx, field) + case "consentText": + return ec.fieldContext_ElectronicSignature_consentText(ctx, field) + case "lastError": + return ec.fieldContext_ElectronicSignature_lastError(ctx, field) + case "signedAt": + return ec.fieldContext_ElectronicSignature_signedAt(ctx, field) + case "certificateFileUrl": + return ec.fieldContext_ElectronicSignature_certificateFileUrl(ctx, field) + case "events": + return ec.fieldContext_ElectronicSignature_events(ctx, field) + case "createdAt": + return ec.fieldContext_ElectronicSignature_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_ElectronicSignature_updatedAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ElectronicSignature", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _TrustCenterAccess_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.TrustCenterAccess) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -51920,6 +52806,8 @@ func (ec *executionContext) fieldContext_TrustCenterAccessEdge_node(_ context.Co return ec.fieldContext_TrustCenterAccess_state(ctx, field) case "hasAcceptedNonDisclosureAgreement": return ec.fieldContext_TrustCenterAccess_hasAcceptedNonDisclosureAgreement(ctx, field) + case "ndaSignature": + return ec.fieldContext_TrustCenterAccess_ndaSignature(ctx, field) case "createdAt": return ec.fieldContext_TrustCenterAccess_createdAt(ctx, field) case "updatedAt": @@ -54718,6 +55606,8 @@ func (ec *executionContext) fieldContext_UpdateTrustCenterAccessPayload_trustCen return ec.fieldContext_TrustCenterAccess_state(ctx, field) case "hasAcceptedNonDisclosureAgreement": return ec.fieldContext_TrustCenterAccess_hasAcceptedNonDisclosureAgreement(ctx, field) + case "ndaSignature": + return ec.fieldContext_TrustCenterAccess_ndaSignature(ctx, field) case "createdAt": return ec.fieldContext_TrustCenterAccess_createdAt(ctx, field) case "updatedAt": @@ -70534,6 +71424,13 @@ func (ec *executionContext) _Node(ctx context.Context, sel ast.SelectionSet, obj return graphql.Null } return ec._Evidence(ctx, sel, obj) + case types.ElectronicSignature: + return ec._ElectronicSignature(ctx, sel, &obj) + case *types.ElectronicSignature: + if obj == nil { + return graphql.Null + } + return ec._ElectronicSignature(ctx, sel, obj) case types.DocumentVersionSignature: return ec._DocumentVersionSignature(ctx, sel, &obj) case *types.DocumentVersionSignature: @@ -77866,6 +78763,217 @@ func (ec *executionContext) _DocumentVersionSignatureEdge(ctx context.Context, s return out } +var electronicSignatureImplementors = []string{"ElectronicSignature", "Node"} + +func (ec *executionContext) _ElectronicSignature(ctx context.Context, sel ast.SelectionSet, obj *types.ElectronicSignature) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, electronicSignatureImplementors) + + 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("ElectronicSignature") + case "id": + out.Values[i] = ec._ElectronicSignature_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "status": + out.Values[i] = ec._ElectronicSignature_status(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "documentType": + out.Values[i] = ec._ElectronicSignature_documentType(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "consentText": + out.Values[i] = ec._ElectronicSignature_consentText(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "lastError": + out.Values[i] = ec._ElectronicSignature_lastError(ctx, field, obj) + case "signedAt": + out.Values[i] = ec._ElectronicSignature_signedAt(ctx, field, obj) + case "certificateFileUrl": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ElectronicSignature_certificateFileUrl(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "events": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ElectronicSignature_events(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "createdAt": + out.Values[i] = ec._ElectronicSignature_createdAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "updatedAt": + out.Values[i] = ec._ElectronicSignature_updatedAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + 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 electronicSignatureEventImplementors = []string{"ElectronicSignatureEvent"} + +func (ec *executionContext) _ElectronicSignatureEvent(ctx context.Context, sel ast.SelectionSet, obj *types.ElectronicSignatureEvent) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, electronicSignatureEventImplementors) + + 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("ElectronicSignatureEvent") + case "id": + out.Values[i] = ec._ElectronicSignatureEvent_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "eventType": + out.Values[i] = ec._ElectronicSignatureEvent_eventType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "eventSource": + out.Values[i] = ec._ElectronicSignatureEvent_eventSource(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "actorEmail": + out.Values[i] = ec._ElectronicSignatureEvent_actorEmail(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "actorIpAddress": + out.Values[i] = ec._ElectronicSignatureEvent_actorIpAddress(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "actorUserAgent": + out.Values[i] = ec._ElectronicSignatureEvent_actorUserAgent(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "occurredAt": + out.Values[i] = ec._ElectronicSignatureEvent_occurredAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "createdAt": + out.Values[i] = ec._ElectronicSignatureEvent_createdAt(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 evidenceImplementors = []string{"Evidence", "Node"} func (ec *executionContext) _Evidence(ctx context.Context, sel ast.SelectionSet, obj *types.Evidence) graphql.Marshaler { @@ -86440,6 +87548,39 @@ func (ec *executionContext) _TrustCenterAccess(ctx context.Context, sel ast.Sele if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "ndaSignature": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._TrustCenterAccess_ndaSignature(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "createdAt": out.Values[i] = ec._TrustCenterAccess_createdAt(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -95662,6 +96803,204 @@ var ( } ) +func (ec *executionContext) unmarshalNElectronicSignatureDocumentType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureDocumentType(ctx context.Context, v any) (coredata.ElectronicSignatureDocumentType, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNElectronicSignatureDocumentType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureDocumentType[tmp] + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNElectronicSignatureDocumentType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureDocumentType(ctx context.Context, sel ast.SelectionSet, v coredata.ElectronicSignatureDocumentType) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(marshalNElectronicSignatureDocumentType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureDocumentType[v]) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +var ( + unmarshalNElectronicSignatureDocumentType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureDocumentType = map[string]coredata.ElectronicSignatureDocumentType{ + "NDA": coredata.ElectronicSignatureDocumentTypeNDA, + "DPA": coredata.ElectronicSignatureDocumentTypeDPA, + "MSA": coredata.ElectronicSignatureDocumentTypeMSA, + "SOW": coredata.ElectronicSignatureDocumentTypeSOW, + "SLA": coredata.ElectronicSignatureDocumentTypeSLA, + "TOS": coredata.ElectronicSignatureDocumentTypeTOS, + "PRIVACY_POLICY": coredata.ElectronicSignatureDocumentTypePrivacyPolicy, + "OTHER": coredata.ElectronicSignatureDocumentTypeOther, + } + marshalNElectronicSignatureDocumentType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureDocumentType = map[coredata.ElectronicSignatureDocumentType]string{ + coredata.ElectronicSignatureDocumentTypeNDA: "NDA", + coredata.ElectronicSignatureDocumentTypeDPA: "DPA", + coredata.ElectronicSignatureDocumentTypeMSA: "MSA", + coredata.ElectronicSignatureDocumentTypeSOW: "SOW", + coredata.ElectronicSignatureDocumentTypeSLA: "SLA", + coredata.ElectronicSignatureDocumentTypeTOS: "TOS", + coredata.ElectronicSignatureDocumentTypePrivacyPolicy: "PRIVACY_POLICY", + coredata.ElectronicSignatureDocumentTypeOther: "OTHER", + } +) + +func (ec *executionContext) marshalNElectronicSignatureEvent2ᚕᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐElectronicSignatureEventᚄ(ctx context.Context, sel ast.SelectionSet, v []*types.ElectronicSignatureEvent) 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.marshalNElectronicSignatureEvent2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐElectronicSignatureEvent(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) marshalNElectronicSignatureEvent2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐElectronicSignatureEvent(ctx context.Context, sel ast.SelectionSet, v *types.ElectronicSignatureEvent) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._ElectronicSignatureEvent(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNElectronicSignatureEventSource2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventSource(ctx context.Context, v any) (coredata.ElectronicSignatureEventSource, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNElectronicSignatureEventSource2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventSource[tmp] + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNElectronicSignatureEventSource2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventSource(ctx context.Context, sel ast.SelectionSet, v coredata.ElectronicSignatureEventSource) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(marshalNElectronicSignatureEventSource2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventSource[v]) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +var ( + unmarshalNElectronicSignatureEventSource2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventSource = map[string]coredata.ElectronicSignatureEventSource{ + "CLIENT": coredata.ElectronicSignatureEventSourceClient, + "SERVER": coredata.ElectronicSignatureEventSourceServer, + } + marshalNElectronicSignatureEventSource2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventSource = map[coredata.ElectronicSignatureEventSource]string{ + coredata.ElectronicSignatureEventSourceClient: "CLIENT", + coredata.ElectronicSignatureEventSourceServer: "SERVER", + } +) + +func (ec *executionContext) unmarshalNElectronicSignatureEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventType(ctx context.Context, v any) (coredata.ElectronicSignatureEventType, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNElectronicSignatureEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventType[tmp] + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNElectronicSignatureEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventType(ctx context.Context, sel ast.SelectionSet, v coredata.ElectronicSignatureEventType) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(marshalNElectronicSignatureEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventType[v]) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +var ( + unmarshalNElectronicSignatureEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventType = map[string]coredata.ElectronicSignatureEventType{ + "DOCUMENT_VIEWED": coredata.ElectronicSignatureEventTypeDocumentViewed, + "CONSENT_GIVEN": coredata.ElectronicSignatureEventTypeConsentGiven, + "FULL_NAME_TYPED": coredata.ElectronicSignatureEventTypeFullNameTyped, + "SIGNATURE_ACCEPTED": coredata.ElectronicSignatureEventTypeSignatureAccepted, + "SIGNATURE_COMPLETED": coredata.ElectronicSignatureEventTypeSignatureCompleted, + "SEAL_COMPUTED": coredata.ElectronicSignatureEventTypeSealComputed, + "TIMESTAMP_REQUESTED": coredata.ElectronicSignatureEventTypeTimestampRequested, + "CERTIFICATE_GENERATED": coredata.ElectronicSignatureEventTypeCertificateGenerated, + "PROCESSING_ERROR": coredata.ElectronicSignatureEventTypeProcessingError, + } + marshalNElectronicSignatureEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventType = map[coredata.ElectronicSignatureEventType]string{ + coredata.ElectronicSignatureEventTypeDocumentViewed: "DOCUMENT_VIEWED", + coredata.ElectronicSignatureEventTypeConsentGiven: "CONSENT_GIVEN", + coredata.ElectronicSignatureEventTypeFullNameTyped: "FULL_NAME_TYPED", + coredata.ElectronicSignatureEventTypeSignatureAccepted: "SIGNATURE_ACCEPTED", + coredata.ElectronicSignatureEventTypeSignatureCompleted: "SIGNATURE_COMPLETED", + coredata.ElectronicSignatureEventTypeSealComputed: "SEAL_COMPUTED", + coredata.ElectronicSignatureEventTypeTimestampRequested: "TIMESTAMP_REQUESTED", + coredata.ElectronicSignatureEventTypeCertificateGenerated: "CERTIFICATE_GENERATED", + coredata.ElectronicSignatureEventTypeProcessingError: "PROCESSING_ERROR", + } +) + +func (ec *executionContext) unmarshalNElectronicSignatureStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureStatus(ctx context.Context, v any) (coredata.ElectronicSignatureStatus, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNElectronicSignatureStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureStatus[tmp] + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNElectronicSignatureStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureStatus(ctx context.Context, sel ast.SelectionSet, v coredata.ElectronicSignatureStatus) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(marshalNElectronicSignatureStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureStatus[v]) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +var ( + unmarshalNElectronicSignatureStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureStatus = map[string]coredata.ElectronicSignatureStatus{ + "PENDING": coredata.ElectronicSignatureStatusPending, + "ACCEPTED": coredata.ElectronicSignatureStatusAccepted, + "PROCESSING": coredata.ElectronicSignatureStatusProcessing, + "COMPLETED": coredata.ElectronicSignatureStatusCompleted, + "FAILED": coredata.ElectronicSignatureStatusFailed, + } + marshalNElectronicSignatureStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureStatus = map[coredata.ElectronicSignatureStatus]string{ + coredata.ElectronicSignatureStatusPending: "PENDING", + coredata.ElectronicSignatureStatusAccepted: "ACCEPTED", + coredata.ElectronicSignatureStatusProcessing: "PROCESSING", + coredata.ElectronicSignatureStatusCompleted: "COMPLETED", + coredata.ElectronicSignatureStatusFailed: "FAILED", + } +) + func (ec *executionContext) unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx context.Context, v any) (mail.Addr, error) { res, err := mail1.UnmarshalAddrScalar(v) return res, graphql.ErrorOnPath(ctx, err) @@ -101670,6 +103009,13 @@ func (ec *executionContext) marshalODuration2ᚖtimeᚐDuration(ctx context.Cont return res } +func (ec *executionContext) marshalOElectronicSignature2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐElectronicSignature(ctx context.Context, sel ast.SelectionSet, v *types.ElectronicSignature) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._ElectronicSignature(ctx, sel, v) +} + func (ec *executionContext) unmarshalOEmailAddr2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx context.Context, v any) (*mail.Addr, error) { if v == nil { return nil, nil diff --git a/pkg/server/api/console/v1/types/electronic_signature.go b/pkg/server/api/console/v1/types/electronic_signature.go new file mode 100644 index 000000000..8a84faad2 --- /dev/null +++ b/pkg/server/api/console/v1/types/electronic_signature.go @@ -0,0 +1,45 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package types + +import ( + "go.probo.inc/probo/pkg/coredata" +) + +func NewElectronicSignature(es *coredata.ElectronicSignature) *ElectronicSignature { + return &ElectronicSignature{ + ID: es.ID, + Status: es.Status, + DocumentType: es.DocumentType, + ConsentText: es.ConsentText, + LastError: es.LastError, + SignedAt: es.SignedAt, + CreatedAt: es.CreatedAt, + UpdatedAt: es.UpdatedAt, + } +} + +func NewElectronicSignatureEvent(ev *coredata.ElectronicSignatureEvent) *ElectronicSignatureEvent { + return &ElectronicSignatureEvent{ + ID: ev.ID, + EventType: ev.EventType, + EventSource: ev.EventSource, + ActorEmail: ev.ActorEmail, + ActorIPAddress: ev.ActorIPAddress, + ActorUserAgent: ev.ActorUserAgent, + OccurredAt: ev.OccurredAt, + CreatedAt: ev.CreatedAt, + } +} diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index 98ead6f16..02b6bc05f 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -1206,6 +1206,33 @@ type DocumentVersionSignatureOrder struct { Direction page.OrderDirection `json:"direction"` } +type ElectronicSignature struct { + ID gid.GID `json:"id"` + Status coredata.ElectronicSignatureStatus `json:"status"` + DocumentType coredata.ElectronicSignatureDocumentType `json:"documentType"` + ConsentText string `json:"consentText"` + LastError *string `json:"lastError,omitempty"` + SignedAt *time.Time `json:"signedAt,omitempty"` + CertificateFileURL *string `json:"certificateFileUrl,omitempty"` + Events []*ElectronicSignatureEvent `json:"events"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func (ElectronicSignature) IsNode() {} +func (this ElectronicSignature) GetID() gid.GID { return this.ID } + +type ElectronicSignatureEvent struct { + ID gid.GID `json:"id"` + EventType coredata.ElectronicSignatureEventType `json:"eventType"` + EventSource coredata.ElectronicSignatureEventSource `json:"eventSource"` + ActorEmail string `json:"actorEmail"` + ActorIPAddress string `json:"actorIpAddress"` + ActorUserAgent string `json:"actorUserAgent"` + OccurredAt time.Time `json:"occurredAt"` + CreatedAt time.Time `json:"createdAt"` +} + type Evidence struct { ID gid.GID `json:"id"` Size int `json:"size"` @@ -1847,6 +1874,7 @@ type TrustCenterAccess struct { Name string `json:"name"` State coredata.TrustCenterAccessState `json:"state"` HasAcceptedNonDisclosureAgreement bool `json:"hasAcceptedNonDisclosureAgreement"` + NdaSignature *ElectronicSignature `json:"ndaSignature,omitempty"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` LastTokenExpiresAt *time.Time `json:"lastTokenExpiresAt,omitempty"` diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index b50d5bb13..d5806000f 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -1241,6 +1241,40 @@ func (r *documentVersionSignatureConnectionResolver) TotalCount(ctx context.Cont panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver)) } +// CertificateFileURL is the resolver for the certificateFileUrl field. +func (r *electronicSignatureResolver) CertificateFileURL(ctx context.Context, obj *types.ElectronicSignature) (*string, error) { + sig, err := r.esign.LoadSignatureByID(ctx, obj.ID) + if err != nil { + return nil, fmt.Errorf("cannot load signature: %w", err) + } + + if sig.CertificateFileID == nil { + return nil, nil + } + + url, err := r.esign.GenerateCertificateFileURL(ctx, *sig.CertificateFileID, 1*time.Hour) + if err != nil { + return nil, fmt.Errorf("cannot generate certificate file URL: %w", err) + } + + return &url, nil +} + +// Events is the resolver for the events field. +func (r *electronicSignatureResolver) Events(ctx context.Context, obj *types.ElectronicSignature) ([]*types.ElectronicSignatureEvent, error) { + events, err := r.esign.LoadEventsBySignatureID(ctx, obj.ID) + if err != nil { + return nil, fmt.Errorf("cannot load signature events: %w", err) + } + + result := make([]*types.ElectronicSignatureEvent, len(events)) + for i := range events { + result[i] = types.NewElectronicSignatureEvent(events[i]) + } + + return result, nil +} + // File is the resolver for the file field. func (r *evidenceResolver) File(ctx context.Context, obj *types.Evidence) (*types.File, error) { if err := r.authorize(ctx, obj.ID, probo.ActionFileGet); err != nil { @@ -7785,6 +7819,42 @@ func (r *trustCenterResolver) Permission(ctx context.Context, obj *types.TrustCe return r.Resolver.Permission(ctx, obj, action) } +// NdaSignature is the resolver for the ndaSignature field. +func (r *trustCenterAccessResolver) NdaSignature(ctx context.Context, obj *types.TrustCenterAccess) (*types.ElectronicSignature, error) { + if err := r.authorize(ctx, obj.ID, probo.ActionTrustCenterAccessGet); err != nil { + return nil, err + } + + prb := r.ProboService(ctx, obj.ID.TenantID()) + + access, err := prb.TrustCenterAccesses.Get(ctx, obj.ID) + if err != nil { + return nil, fmt.Errorf("cannot load trust center access: %w", err) + } + + trustCenter, _, err := prb.TrustCenters.Get(ctx, access.TrustCenterID) + if err != nil { + return nil, fmt.Errorf("cannot load trust center: %w", err) + } + + if trustCenter.NonDisclosureAgreementFileID == nil { + return nil, nil + } + + sig, err := r.esign.LoadSignatureByOrgEmailAndDocType( + ctx, + access.OrganizationID, + string(access.Email), + coredata.ElectronicSignatureDocumentTypeNDA, + *trustCenter.NonDisclosureAgreementFileID, + ) + if err != nil { + return nil, nil // No signature row — pre-existing access + } + + return types.NewElectronicSignature(sig), nil +} + // PendingRequestCount is the resolver for the pendingRequestCount field. func (r *trustCenterAccessResolver) PendingRequestCount(ctx context.Context, obj *types.TrustCenterAccess) (int, error) { if err := r.authorize(ctx, obj.ID, probo.ActionTrustCenterAccessGet); err != nil { @@ -8784,6 +8854,11 @@ func (r *Resolver) DocumentVersionSignatureConnection() schema.DocumentVersionSi return &documentVersionSignatureConnectionResolver{r} } +// ElectronicSignature returns schema.ElectronicSignatureResolver implementation. +func (r *Resolver) ElectronicSignature() schema.ElectronicSignatureResolver { + return &electronicSignatureResolver{r} +} + // Evidence returns schema.EvidenceResolver implementation. func (r *Resolver) Evidence() schema.EvidenceResolver { return &evidenceResolver{r} } @@ -9029,6 +9104,7 @@ type documentVersionResolver struct{ *Resolver } type documentVersionConnectionResolver struct{ *Resolver } type documentVersionSignatureResolver struct{ *Resolver } type documentVersionSignatureConnectionResolver struct{ *Resolver } +type electronicSignatureResolver struct{ *Resolver } type evidenceResolver struct{ *Resolver } type evidenceConnectionResolver struct{ *Resolver } type fileResolver struct{ *Resolver } diff --git a/pkg/server/api/trust/v1/graphql_handler.go b/pkg/server/api/trust/v1/graphql_handler.go index 1305d5931..0ea0ad76f 100644 --- a/pkg/server/api/trust/v1/graphql_handler.go +++ b/pkg/server/api/trust/v1/graphql_handler.go @@ -19,6 +19,7 @@ import ( "go.gearno.de/kit/log" "go.probo.inc/probo/pkg/baseurl" + "go.probo.inc/probo/pkg/esign" "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/securecookie" "go.probo.inc/probo/pkg/server/api/authn" @@ -28,11 +29,12 @@ import ( "go.probo.inc/probo/pkg/trust" ) -func NewGraphQLHandler(iamSvc *iam.Service, trustSvc *trust.Service, logger *log.Logger, baseURL *baseurl.BaseURL, cookieConfig securecookie.Config) http.Handler { +func NewGraphQLHandler(iamSvc *iam.Service, trustSvc *trust.Service, esignSvc *esign.Service, logger *log.Logger, baseURL *baseurl.BaseURL, cookieConfig securecookie.Config) http.Handler { config := schema.Config{ Resolvers: &Resolver{ iam: iamSvc, trust: trustSvc, + esign: esignSvc, logger: logger, baseURL: baseURL, sessionCookie: authn.NewCookie(&cookieConfig), diff --git a/pkg/server/api/trust/v1/resolver.go b/pkg/server/api/trust/v1/resolver.go index f6214eec2..bb0b384cf 100644 --- a/pkg/server/api/trust/v1/resolver.go +++ b/pkg/server/api/trust/v1/resolver.go @@ -23,6 +23,7 @@ import ( "github.com/go-chi/chi/v5" "go.gearno.de/kit/log" "go.probo.inc/probo/pkg/baseurl" + "go.probo.inc/probo/pkg/esign" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/securecookie" @@ -46,6 +47,7 @@ type ( Resolver struct { trust *trust.Service + esign *esign.Service logger *log.Logger iam *iam.Service sessionCookie *authn.Cookie @@ -75,6 +77,7 @@ func NewMux( logger *log.Logger, iamSvc *iam.Service, trustSvc *trust.Service, + esignSvc *esign.Service, cookieConfig securecookie.Config, baseURL *baseurl.BaseURL, ) *chi.Mux { @@ -84,7 +87,7 @@ func NewMux( r.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig)) r.Use(compliancepage.NewMembershipMiddleware(trustSvc, logger)) - graphqlHandler := NewGraphQLHandler(iamSvc, trustSvc, logger, baseURL, cookieConfig) + graphqlHandler := NewGraphQLHandler(iamSvc, trustSvc, esignSvc, logger, baseURL, cookieConfig) r.Handle("/graphql", graphqlHandler) diff --git a/pkg/server/api/trust/v1/schema.graphql b/pkg/server/api/trust/v1/schema.graphql index 00bdc6915..70904cca0 100644 --- a/pkg/server/api/trust/v1/schema.graphql +++ b/pkg/server/api/trust/v1/schema.graphql @@ -496,6 +496,7 @@ type TrustCenter implements Node { darkLogoFileUrl: String @goField(forceResolver: true) ndaFileName: String ndaFileUrl: String @goField(forceResolver: true) + ndaSignature: ElectronicSignature @goField(forceResolver: true) organization: Organization! @goField(forceResolver: true) isViewerMember: Boolean! @goField(forceResolver: true) hasAcceptedNonDisclosureAgreement: Boolean! @goField(forceResolver: true) @@ -608,6 +609,143 @@ type AcceptNonDisclosureAgreementPayload { success: Boolean! } +# Electronic Signature + +enum ElectronicSignatureStatus + @goModel( + model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatus" + ) { + PENDING + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusPending" + ) + ACCEPTED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusAccepted" + ) + PROCESSING + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusProcessing" + ) + COMPLETED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusCompleted" + ) + FAILED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusFailed" + ) +} + +enum ElectronicSignatureDocumentType + @goModel( + model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentType" + ) { + NDA + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeNDA" + ) + DPA + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeDPA" + ) + MSA + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeMSA" + ) + SOW + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeSOW" + ) + SLA + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeSLA" + ) + TOS + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeTOS" + ) + PRIVACY_POLICY + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypePrivacyPolicy" + ) + OTHER + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeOther" + ) +} + +enum ElectronicSignatureEventType + @goModel( + model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventType" + ) { + DOCUMENT_VIEWED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeDocumentViewed" + ) + CONSENT_GIVEN + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeConsentGiven" + ) + FULL_NAME_TYPED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeFullNameTyped" + ) + SIGNATURE_ACCEPTED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeSignatureAccepted" + ) + SIGNATURE_COMPLETED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeSignatureCompleted" + ) + SEAL_COMPUTED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeSealComputed" + ) + TIMESTAMP_REQUESTED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeTimestampRequested" + ) + CERTIFICATE_GENERATED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeCertificateGenerated" + ) + PROCESSING_ERROR + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeProcessingError" + ) +} + +type ElectronicSignature implements Node { + id: ID! + status: ElectronicSignatureStatus! + documentType: ElectronicSignatureDocumentType! + consentText: String! + lastError: String + signedAt: Datetime + createdAt: Datetime! + updatedAt: Datetime! +} + +input AcceptElectronicSignatureInput { + signatureId: ID! + fullName: String! +} + +type AcceptElectronicSignaturePayload { + signature: ElectronicSignature! +} + +input RecordSigningEventInput { + signatureId: ID! + eventType: ElectronicSignatureEventType! +} + +type RecordSigningEventPayload { + success: Boolean! +} + type Query { viewer: Identity node(id: ID!): Node! @@ -647,4 +785,12 @@ type Mutation { requestTrustCenterFileAccess( input: RequestTrustCenterFileAccessInput! ): RequestAccessesPayload! @session(required: PRESENT) + + acceptElectronicSignature( + input: AcceptElectronicSignatureInput! + ): AcceptElectronicSignaturePayload @session(required: PRESENT) + + recordSigningEvent( + input: RecordSigningEventInput! + ): RecordSigningEventPayload @session(required: PRESENT) } diff --git a/pkg/server/api/trust/v1/schema/schema.go b/pkg/server/api/trust/v1/schema/schema.go index 98e53662e..bd9f590c6 100644 --- a/pkg/server/api/trust/v1/schema/schema.go +++ b/pkg/server/api/trust/v1/schema/schema.go @@ -65,6 +65,10 @@ type DirectiveRoot struct { } type ComplexityRoot struct { + AcceptElectronicSignaturePayload struct { + Signature func(childComplexity int) int + } + AcceptNonDisclosureAgreementPayload struct { Success func(childComplexity int) int } @@ -103,6 +107,17 @@ type ComplexityRoot struct { Node func(childComplexity int) int } + ElectronicSignature struct { + ConsentText func(childComplexity int) int + CreatedAt func(childComplexity int) int + DocumentType func(childComplexity int) int + ID func(childComplexity int) int + LastError func(childComplexity int) int + SignedAt func(childComplexity int) int + Status func(childComplexity int) int + UpdatedAt func(childComplexity int) int + } + ExportDocumentPDFPayload struct { Data func(childComplexity int) int } @@ -132,10 +147,12 @@ type ComplexityRoot struct { } Mutation struct { + AcceptElectronicSignature func(childComplexity int, input types.AcceptElectronicSignatureInput) int AcceptNonDisclosureAgreement func(childComplexity int, input types.AcceptNonDisclosureAgreementInput) int ExportDocumentPDF func(childComplexity int, input types.ExportDocumentPDFInput) int ExportReportPDF func(childComplexity int, input types.ExportReportPDFInput) int ExportTrustCenterFile func(childComplexity int, input types.ExportTrustCenterFileInput) int + RecordSigningEvent func(childComplexity int, input types.RecordSigningEventInput) int RequestAllAccesses func(childComplexity int) int RequestDocumentAccess func(childComplexity int, input types.RequestDocumentAccessInput) int RequestReportAccess func(childComplexity int, input types.RequestReportAccessInput) int @@ -167,6 +184,10 @@ type ComplexityRoot struct { Viewer func(childComplexity int) int } + RecordSigningEventPayload struct { + Success func(childComplexity int) int + } + Report struct { Filename func(childComplexity int) int HasUserRequestedAccess func(childComplexity int) int @@ -193,6 +214,7 @@ type ComplexityRoot struct { LogoFileURL func(childComplexity int) int NdaFileName func(childComplexity int) int NdaFileURL func(childComplexity int) int + NdaSignature func(childComplexity int) int Organization func(childComplexity int) int References func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int Slug func(childComplexity int) int @@ -293,6 +315,8 @@ type MutationResolver interface { RequestDocumentAccess(ctx context.Context, input types.RequestDocumentAccessInput) (*types.RequestAccessesPayload, error) RequestReportAccess(ctx context.Context, input types.RequestReportAccessInput) (*types.RequestAccessesPayload, error) RequestTrustCenterFileAccess(ctx context.Context, input types.RequestTrustCenterFileAccessInput) (*types.RequestAccessesPayload, error) + AcceptElectronicSignature(ctx context.Context, input types.AcceptElectronicSignatureInput) (*types.AcceptElectronicSignaturePayload, error) + RecordSigningEvent(ctx context.Context, input types.RecordSigningEventInput) (*types.RecordSigningEventPayload, error) } type OrganizationResolver interface { LogoURL(ctx context.Context, obj *types.Organization) (*string, error) @@ -311,6 +335,7 @@ type TrustCenterResolver interface { DarkLogoFileURL(ctx context.Context, obj *types.TrustCenter) (*string, error) NdaFileURL(ctx context.Context, obj *types.TrustCenter) (*string, error) + NdaSignature(ctx context.Context, obj *types.TrustCenter) (*types.ElectronicSignature, error) Organization(ctx context.Context, obj *types.TrustCenter) (*types.Organization, error) IsViewerMember(ctx context.Context, obj *types.TrustCenter) (bool, error) HasAcceptedNonDisclosureAgreement(ctx context.Context, obj *types.TrustCenter) (bool, error) @@ -350,6 +375,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin _ = ec switch typeName + "." + field { + case "AcceptElectronicSignaturePayload.signature": + if e.complexity.AcceptElectronicSignaturePayload.Signature == nil { + break + } + + return e.complexity.AcceptElectronicSignaturePayload.Signature(childComplexity), true + case "AcceptNonDisclosureAgreementPayload.success": if e.complexity.AcceptNonDisclosureAgreementPayload.Success == nil { break @@ -459,6 +491,55 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.DocumentEdge.Node(childComplexity), true + case "ElectronicSignature.consentText": + if e.complexity.ElectronicSignature.ConsentText == nil { + break + } + + return e.complexity.ElectronicSignature.ConsentText(childComplexity), true + case "ElectronicSignature.createdAt": + if e.complexity.ElectronicSignature.CreatedAt == nil { + break + } + + return e.complexity.ElectronicSignature.CreatedAt(childComplexity), true + case "ElectronicSignature.documentType": + if e.complexity.ElectronicSignature.DocumentType == nil { + break + } + + return e.complexity.ElectronicSignature.DocumentType(childComplexity), true + case "ElectronicSignature.id": + if e.complexity.ElectronicSignature.ID == nil { + break + } + + return e.complexity.ElectronicSignature.ID(childComplexity), true + case "ElectronicSignature.lastError": + if e.complexity.ElectronicSignature.LastError == nil { + break + } + + return e.complexity.ElectronicSignature.LastError(childComplexity), true + case "ElectronicSignature.signedAt": + if e.complexity.ElectronicSignature.SignedAt == nil { + break + } + + return e.complexity.ElectronicSignature.SignedAt(childComplexity), true + case "ElectronicSignature.status": + if e.complexity.ElectronicSignature.Status == nil { + break + } + + return e.complexity.ElectronicSignature.Status(childComplexity), true + case "ElectronicSignature.updatedAt": + if e.complexity.ElectronicSignature.UpdatedAt == nil { + break + } + + return e.complexity.ElectronicSignature.UpdatedAt(childComplexity), true + case "ExportDocumentPDFPayload.data": if e.complexity.ExportDocumentPDFPayload.Data == nil { break @@ -542,6 +623,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Identity.UpdatedAt(childComplexity), true + case "Mutation.acceptElectronicSignature": + if e.complexity.Mutation.AcceptElectronicSignature == nil { + break + } + + args, err := ec.field_Mutation_acceptElectronicSignature_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.AcceptElectronicSignature(childComplexity, args["input"].(types.AcceptElectronicSignatureInput)), true case "Mutation.acceptNonDisclosureAgreement": if e.complexity.Mutation.AcceptNonDisclosureAgreement == nil { break @@ -586,6 +678,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Mutation.ExportTrustCenterFile(childComplexity, args["input"].(types.ExportTrustCenterFileInput)), true + case "Mutation.recordSigningEvent": + if e.complexity.Mutation.RecordSigningEvent == nil { + break + } + + args, err := ec.field_Mutation_recordSigningEvent_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.RecordSigningEvent(childComplexity, args["input"].(types.RecordSigningEventInput)), true case "Mutation.requestAllAccesses": if e.complexity.Mutation.RequestAllAccesses == nil { break @@ -740,6 +843,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Query.Viewer(childComplexity), true + case "RecordSigningEventPayload.success": + if e.complexity.RecordSigningEventPayload.Success == nil { + break + } + + return e.complexity.RecordSigningEventPayload.Success(childComplexity), true + case "Report.filename": if e.complexity.Report.Filename == nil { break @@ -849,6 +959,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.TrustCenter.NdaFileURL(childComplexity), true + case "TrustCenter.ndaSignature": + if e.complexity.TrustCenter.NdaSignature == nil { + break + } + + return e.complexity.TrustCenter.NdaSignature(childComplexity), true case "TrustCenter.organization": if e.complexity.TrustCenter.Organization == nil { break @@ -1130,10 +1246,12 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { opCtx := graphql.GetOperationContext(ctx) ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)} inputUnmarshalMap := graphql.BuildUnmarshalerMap( + ec.unmarshalInputAcceptElectronicSignatureInput, ec.unmarshalInputAcceptNonDisclosureAgreementInput, ec.unmarshalInputExportDocumentPDFInput, ec.unmarshalInputExportReportPDFInput, ec.unmarshalInputExportTrustCenterFileInput, + ec.unmarshalInputRecordSigningEventInput, ec.unmarshalInputRequestDocumentAccessInput, ec.unmarshalInputRequestReportAccessInput, ec.unmarshalInputRequestTrustCenterFileAccessInput, @@ -1734,6 +1852,7 @@ type TrustCenter implements Node { darkLogoFileUrl: String @goField(forceResolver: true) ndaFileName: String ndaFileUrl: String @goField(forceResolver: true) + ndaSignature: ElectronicSignature @goField(forceResolver: true) organization: Organization! @goField(forceResolver: true) isViewerMember: Boolean! @goField(forceResolver: true) hasAcceptedNonDisclosureAgreement: Boolean! @goField(forceResolver: true) @@ -1846,6 +1965,143 @@ type AcceptNonDisclosureAgreementPayload { success: Boolean! } +# Electronic Signature + +enum ElectronicSignatureStatus + @goModel( + model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatus" + ) { + PENDING + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusPending" + ) + ACCEPTED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusAccepted" + ) + PROCESSING + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusProcessing" + ) + COMPLETED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusCompleted" + ) + FAILED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureStatusFailed" + ) +} + +enum ElectronicSignatureDocumentType + @goModel( + model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentType" + ) { + NDA + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeNDA" + ) + DPA + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeDPA" + ) + MSA + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeMSA" + ) + SOW + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeSOW" + ) + SLA + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeSLA" + ) + TOS + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeTOS" + ) + PRIVACY_POLICY + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypePrivacyPolicy" + ) + OTHER + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureDocumentTypeOther" + ) +} + +enum ElectronicSignatureEventType + @goModel( + model: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventType" + ) { + DOCUMENT_VIEWED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeDocumentViewed" + ) + CONSENT_GIVEN + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeConsentGiven" + ) + FULL_NAME_TYPED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeFullNameTyped" + ) + SIGNATURE_ACCEPTED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeSignatureAccepted" + ) + SIGNATURE_COMPLETED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeSignatureCompleted" + ) + SEAL_COMPUTED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeSealComputed" + ) + TIMESTAMP_REQUESTED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeTimestampRequested" + ) + CERTIFICATE_GENERATED + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeCertificateGenerated" + ) + PROCESSING_ERROR + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.ElectronicSignatureEventTypeProcessingError" + ) +} + +type ElectronicSignature implements Node { + id: ID! + status: ElectronicSignatureStatus! + documentType: ElectronicSignatureDocumentType! + consentText: String! + lastError: String + signedAt: Datetime + createdAt: Datetime! + updatedAt: Datetime! +} + +input AcceptElectronicSignatureInput { + signatureId: ID! + fullName: String! +} + +type AcceptElectronicSignaturePayload { + signature: ElectronicSignature! +} + +input RecordSigningEventInput { + signatureId: ID! + eventType: ElectronicSignatureEventType! +} + +type RecordSigningEventPayload { + success: Boolean! +} + type Query { viewer: Identity node(id: ID!): Node! @@ -1885,6 +2141,14 @@ type Mutation { requestTrustCenterFileAccess( input: RequestTrustCenterFileAccessInput! ): RequestAccessesPayload! @session(required: PRESENT) + + acceptElectronicSignature( + input: AcceptElectronicSignatureInput! + ): AcceptElectronicSignaturePayload @session(required: PRESENT) + + recordSigningEvent( + input: RecordSigningEventInput! + ): RecordSigningEventPayload @session(required: PRESENT) } `, BuiltIn: false}, {Name: "../../../../gqlutils/directives/session/schema.graphql", Input: `# Session directive for GraphQL APIs @@ -1944,6 +2208,17 @@ func (ec *executionContext) dir_session_args(ctx context.Context, rawArgs map[st return args, nil } +func (ec *executionContext) field_Mutation_acceptElectronicSignature_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNAcceptElectronicSignatureInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐAcceptElectronicSignatureInput) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_acceptNonDisclosureAgreement_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -1988,6 +2263,17 @@ func (ec *executionContext) field_Mutation_exportTrustCenterFile_args(ctx contex return args, nil } +func (ec *executionContext) field_Mutation_recordSigningEvent_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNRecordSigningEventInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐRecordSigningEventInput) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_requestDocumentAccess_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -2247,6 +2533,53 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg // region **************************** field.gotpl ***************************** +func (ec *executionContext) _AcceptElectronicSignaturePayload_signature(ctx context.Context, field graphql.CollectedField, obj *types.AcceptElectronicSignaturePayload) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_AcceptElectronicSignaturePayload_signature, + func(ctx context.Context) (any, error) { + return obj.Signature, nil + }, + nil, + ec.marshalNElectronicSignature2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐElectronicSignature, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_AcceptElectronicSignaturePayload_signature(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "AcceptElectronicSignaturePayload", + 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_ElectronicSignature_id(ctx, field) + case "status": + return ec.fieldContext_ElectronicSignature_status(ctx, field) + case "documentType": + return ec.fieldContext_ElectronicSignature_documentType(ctx, field) + case "consentText": + return ec.fieldContext_ElectronicSignature_consentText(ctx, field) + case "lastError": + return ec.fieldContext_ElectronicSignature_lastError(ctx, field) + case "signedAt": + return ec.fieldContext_ElectronicSignature_signedAt(ctx, field) + case "createdAt": + return ec.fieldContext_ElectronicSignature_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_ElectronicSignature_updatedAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ElectronicSignature", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _AcceptNonDisclosureAgreementPayload_success(ctx context.Context, field graphql.CollectedField, obj *types.AcceptNonDisclosureAgreementPayload) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -2812,6 +3145,238 @@ func (ec *executionContext) fieldContext_DocumentEdge_node(_ context.Context, fi return fc, nil } +func (ec *executionContext) _ElectronicSignature_id(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_id, + func(ctx context.Context) (any, error) { + return obj.ID, nil + }, + nil, + ec.marshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + 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) _ElectronicSignature_status(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_status, + func(ctx context.Context) (any, error) { + return obj.Status, nil + }, + nil, + ec.marshalNElectronicSignatureStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureStatus, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_status(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ElectronicSignatureStatus does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ElectronicSignature_documentType(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_documentType, + func(ctx context.Context) (any, error) { + return obj.DocumentType, nil + }, + nil, + ec.marshalNElectronicSignatureDocumentType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureDocumentType, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_documentType(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ElectronicSignatureDocumentType does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ElectronicSignature_consentText(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_consentText, + func(ctx context.Context) (any, error) { + return obj.ConsentText, nil + }, + nil, + ec.marshalNString2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_consentText(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + 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) _ElectronicSignature_lastError(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_lastError, + func(ctx context.Context) (any, error) { + return obj.LastError, nil + }, + nil, + ec.marshalOString2ᚖstring, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_lastError(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + 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) _ElectronicSignature_signedAt(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_signedAt, + func(ctx context.Context) (any, error) { + return obj.SignedAt, nil + }, + nil, + ec.marshalODatetime2ᚖtimeᚐTime, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_signedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + 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) _ElectronicSignature_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_createdAt, + func(ctx context.Context) (any, error) { + return obj.CreatedAt, nil + }, + nil, + ec.marshalNDatetime2timeᚐTime, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + 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) _ElectronicSignature_updatedAt(ctx context.Context, field graphql.CollectedField, obj *types.ElectronicSignature) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_ElectronicSignature_updatedAt, + func(ctx context.Context) (any, error) { + return obj.UpdatedAt, nil + }, + nil, + ec.marshalNDatetime2timeᚐTime, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_ElectronicSignature_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ElectronicSignature", + 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) _ExportDocumentPDFPayload_data(ctx context.Context, field graphql.CollectedField, obj *types.ExportDocumentPDFPayload) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -3807,6 +4372,132 @@ func (ec *executionContext) fieldContext_Mutation_requestTrustCenterFileAccess(c return fc, nil } +func (ec *executionContext) _Mutation_acceptElectronicSignature(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Mutation_acceptElectronicSignature, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.Mutation().AcceptElectronicSignature(ctx, fc.Args["input"].(types.AcceptElectronicSignatureInput)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx, "PRESENT") + if err != nil { + var zeroVal *types.AcceptElectronicSignaturePayload + return zeroVal, err + } + if ec.directives.Session == nil { + var zeroVal *types.AcceptElectronicSignaturePayload + return zeroVal, errors.New("directive session is not implemented") + } + return ec.directives.Session(ctx, nil, directive0, required) + } + + next = directive1 + return next + }, + ec.marshalOAcceptElectronicSignaturePayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐAcceptElectronicSignaturePayload, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_Mutation_acceptElectronicSignature(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "signature": + return ec.fieldContext_AcceptElectronicSignaturePayload_signature(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type AcceptElectronicSignaturePayload", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_acceptElectronicSignature_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_recordSigningEvent(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Mutation_recordSigningEvent, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.Mutation().RecordSigningEvent(ctx, fc.Args["input"].(types.RecordSigningEventInput)) + }, + func(ctx context.Context, next graphql.Resolver) graphql.Resolver { + directive0 := next + + directive1 := func(ctx context.Context) (any, error) { + required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx, "PRESENT") + if err != nil { + var zeroVal *types.RecordSigningEventPayload + return zeroVal, err + } + if ec.directives.Session == nil { + var zeroVal *types.RecordSigningEventPayload + return zeroVal, errors.New("directive session is not implemented") + } + return ec.directives.Session(ctx, nil, directive0, required) + } + + next = directive1 + return next + }, + ec.marshalORecordSigningEventPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐRecordSigningEventPayload, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_Mutation_recordSigningEvent(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "success": + return ec.fieldContext_RecordSigningEventPayload_success(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type RecordSigningEventPayload", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_recordSigningEvent_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Organization_id(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -4248,6 +4939,8 @@ func (ec *executionContext) fieldContext_Query_currentTrustCenter(_ context.Cont return ec.fieldContext_TrustCenter_ndaFileName(ctx, field) case "ndaFileUrl": return ec.fieldContext_TrustCenter_ndaFileUrl(ctx, field) + case "ndaSignature": + return ec.fieldContext_TrustCenter_ndaSignature(ctx, field) case "organization": return ec.fieldContext_TrustCenter_organization(ctx, field) case "isViewerMember": @@ -4379,6 +5072,35 @@ func (ec *executionContext) fieldContext_Query___schema(_ context.Context, field return fc, nil } +func (ec *executionContext) _RecordSigningEventPayload_success(ctx context.Context, field graphql.CollectedField, obj *types.RecordSigningEventPayload) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_RecordSigningEventPayload_success, + func(ctx context.Context) (any, error) { + return obj.Success, nil + }, + nil, + ec.marshalNBoolean2bool, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_RecordSigningEventPayload_success(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "RecordSigningEventPayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Report_id(ctx context.Context, field graphql.CollectedField, obj *types.Report) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -4768,6 +5490,53 @@ func (ec *executionContext) fieldContext_TrustCenter_ndaFileUrl(_ context.Contex return fc, nil } +func (ec *executionContext) _TrustCenter_ndaSignature(ctx context.Context, field graphql.CollectedField, obj *types.TrustCenter) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_TrustCenter_ndaSignature, + func(ctx context.Context) (any, error) { + return ec.resolvers.TrustCenter().NdaSignature(ctx, obj) + }, + nil, + ec.marshalOElectronicSignature2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐElectronicSignature, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_TrustCenter_ndaSignature(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TrustCenter", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_ElectronicSignature_id(ctx, field) + case "status": + return ec.fieldContext_ElectronicSignature_status(ctx, field) + case "documentType": + return ec.fieldContext_ElectronicSignature_documentType(ctx, field) + case "consentText": + return ec.fieldContext_ElectronicSignature_consentText(ctx, field) + case "lastError": + return ec.fieldContext_ElectronicSignature_lastError(ctx, field) + case "signedAt": + return ec.fieldContext_ElectronicSignature_signedAt(ctx, field) + case "createdAt": + return ec.fieldContext_ElectronicSignature_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_ElectronicSignature_updatedAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ElectronicSignature", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _TrustCenter_organization(ctx context.Context, field graphql.CollectedField, obj *types.TrustCenter) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -7686,6 +8455,40 @@ func (ec *executionContext) fieldContext___Type_isOneOf(_ context.Context, field // region **************************** input.gotpl ***************************** +func (ec *executionContext) unmarshalInputAcceptElectronicSignatureInput(ctx context.Context, obj any) (types.AcceptElectronicSignatureInput, error) { + var it types.AcceptElectronicSignatureInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"signatureId", "fullName"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "signatureId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("signatureId")) + data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.SignatureID = data + case "fullName": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.FullName = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputAcceptNonDisclosureAgreementInput(ctx context.Context, obj any) (types.AcceptNonDisclosureAgreementInput, error) { var it types.AcceptNonDisclosureAgreementInput asMap := map[string]any{} @@ -7794,6 +8597,40 @@ func (ec *executionContext) unmarshalInputExportTrustCenterFileInput(ctx context return it, nil } +func (ec *executionContext) unmarshalInputRecordSigningEventInput(ctx context.Context, obj any) (types.RecordSigningEventInput, error) { + var it types.RecordSigningEventInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"signatureId", "eventType"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "signatureId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("signatureId")) + data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.SignatureID = data + case "eventType": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("eventType")) + data, err := ec.unmarshalNElectronicSignatureEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventType(ctx, v) + if err != nil { + return it, err + } + it.EventType = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputRequestDocumentAccessInput(ctx context.Context, obj any) (types.RequestDocumentAccessInput, error) { var it types.RequestDocumentAccessInput asMap := map[string]any{} @@ -8000,6 +8837,13 @@ func (ec *executionContext) _Node(ctx context.Context, sel ast.SelectionSet, obj return graphql.Null } return ec._Framework(ctx, sel, obj) + case types.ElectronicSignature: + return ec._ElectronicSignature(ctx, sel, &obj) + case *types.ElectronicSignature: + if obj == nil { + return graphql.Null + } + return ec._ElectronicSignature(ctx, sel, obj) case types.Document: return ec._Document(ctx, sel, &obj) case *types.Document: @@ -8027,6 +8871,45 @@ func (ec *executionContext) _Node(ctx context.Context, sel ast.SelectionSet, obj // region **************************** object.gotpl **************************** +var acceptElectronicSignaturePayloadImplementors = []string{"AcceptElectronicSignaturePayload"} + +func (ec *executionContext) _AcceptElectronicSignaturePayload(ctx context.Context, sel ast.SelectionSet, obj *types.AcceptElectronicSignaturePayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, acceptElectronicSignaturePayloadImplementors) + + 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("AcceptElectronicSignaturePayload") + case "signature": + out.Values[i] = ec._AcceptElectronicSignaturePayload_signature(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 acceptNonDisclosureAgreementPayloadImplementors = []string{"AcceptNonDisclosureAgreementPayload"} func (ec *executionContext) _AcceptNonDisclosureAgreementPayload(ctx context.Context, sel ast.SelectionSet, obj *types.AcceptNonDisclosureAgreementPayload) graphql.Marshaler { @@ -8471,6 +9354,74 @@ func (ec *executionContext) _DocumentEdge(ctx context.Context, sel ast.Selection return out } +var electronicSignatureImplementors = []string{"ElectronicSignature", "Node"} + +func (ec *executionContext) _ElectronicSignature(ctx context.Context, sel ast.SelectionSet, obj *types.ElectronicSignature) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, electronicSignatureImplementors) + + 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("ElectronicSignature") + case "id": + out.Values[i] = ec._ElectronicSignature_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "status": + out.Values[i] = ec._ElectronicSignature_status(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "documentType": + out.Values[i] = ec._ElectronicSignature_documentType(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "consentText": + out.Values[i] = ec._ElectronicSignature_consentText(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "lastError": + out.Values[i] = ec._ElectronicSignature_lastError(ctx, field, obj) + case "signedAt": + out.Values[i] = ec._ElectronicSignature_signedAt(ctx, field, obj) + case "createdAt": + out.Values[i] = ec._ElectronicSignature_createdAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "updatedAt": + out.Values[i] = ec._ElectronicSignature_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 exportDocumentPDFPayloadImplementors = []string{"ExportDocumentPDFPayload"} func (ec *executionContext) _ExportDocumentPDFPayload(ctx context.Context, sel ast.SelectionSet, obj *types.ExportDocumentPDFPayload) graphql.Marshaler { @@ -8842,6 +9793,14 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "acceptElectronicSignature": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_acceptElectronicSignature(ctx, field) + }) + case "recordSigningEvent": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_recordSigningEvent(ctx, field) + }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -9108,6 +10067,45 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr return out } +var recordSigningEventPayloadImplementors = []string{"RecordSigningEventPayload"} + +func (ec *executionContext) _RecordSigningEventPayload(ctx context.Context, sel ast.SelectionSet, obj *types.RecordSigningEventPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, recordSigningEventPayloadImplementors) + + 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("RecordSigningEventPayload") + case "success": + out.Values[i] = ec._RecordSigningEventPayload_success(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 reportImplementors = []string{"Report", "Node"} func (ec *executionContext) _Report(ctx context.Context, sel ast.SelectionSet, obj *types.Report) graphql.Marshaler { @@ -9428,6 +10426,39 @@ func (ec *executionContext) _TrustCenter(ctx context.Context, sel ast.SelectionS continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "ndaSignature": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._TrustCenter_ndaSignature(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "organization": field := field @@ -10741,6 +11772,11 @@ func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, o // region ***************************** type.gotpl ***************************** +func (ec *executionContext) unmarshalNAcceptElectronicSignatureInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐAcceptElectronicSignatureInput(ctx context.Context, v any) (types.AcceptElectronicSignatureInput, error) { + res, err := ec.unmarshalInputAcceptElectronicSignatureInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNAcceptNonDisclosureAgreementInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐAcceptNonDisclosureAgreementInput(ctx context.Context, v any) (types.AcceptNonDisclosureAgreementInput, error) { res, err := ec.unmarshalInputAcceptNonDisclosureAgreementInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -11563,6 +12599,132 @@ var ( } ) +func (ec *executionContext) marshalNElectronicSignature2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐElectronicSignature(ctx context.Context, sel ast.SelectionSet, v *types.ElectronicSignature) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._ElectronicSignature(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNElectronicSignatureDocumentType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureDocumentType(ctx context.Context, v any) (coredata.ElectronicSignatureDocumentType, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNElectronicSignatureDocumentType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureDocumentType[tmp] + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNElectronicSignatureDocumentType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureDocumentType(ctx context.Context, sel ast.SelectionSet, v coredata.ElectronicSignatureDocumentType) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(marshalNElectronicSignatureDocumentType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureDocumentType[v]) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +var ( + unmarshalNElectronicSignatureDocumentType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureDocumentType = map[string]coredata.ElectronicSignatureDocumentType{ + "NDA": coredata.ElectronicSignatureDocumentTypeNDA, + "DPA": coredata.ElectronicSignatureDocumentTypeDPA, + "MSA": coredata.ElectronicSignatureDocumentTypeMSA, + "SOW": coredata.ElectronicSignatureDocumentTypeSOW, + "SLA": coredata.ElectronicSignatureDocumentTypeSLA, + "TOS": coredata.ElectronicSignatureDocumentTypeTOS, + "PRIVACY_POLICY": coredata.ElectronicSignatureDocumentTypePrivacyPolicy, + "OTHER": coredata.ElectronicSignatureDocumentTypeOther, + } + marshalNElectronicSignatureDocumentType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureDocumentType = map[coredata.ElectronicSignatureDocumentType]string{ + coredata.ElectronicSignatureDocumentTypeNDA: "NDA", + coredata.ElectronicSignatureDocumentTypeDPA: "DPA", + coredata.ElectronicSignatureDocumentTypeMSA: "MSA", + coredata.ElectronicSignatureDocumentTypeSOW: "SOW", + coredata.ElectronicSignatureDocumentTypeSLA: "SLA", + coredata.ElectronicSignatureDocumentTypeTOS: "TOS", + coredata.ElectronicSignatureDocumentTypePrivacyPolicy: "PRIVACY_POLICY", + coredata.ElectronicSignatureDocumentTypeOther: "OTHER", + } +) + +func (ec *executionContext) unmarshalNElectronicSignatureEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventType(ctx context.Context, v any) (coredata.ElectronicSignatureEventType, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNElectronicSignatureEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventType[tmp] + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNElectronicSignatureEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventType(ctx context.Context, sel ast.SelectionSet, v coredata.ElectronicSignatureEventType) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(marshalNElectronicSignatureEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventType[v]) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +var ( + unmarshalNElectronicSignatureEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventType = map[string]coredata.ElectronicSignatureEventType{ + "DOCUMENT_VIEWED": coredata.ElectronicSignatureEventTypeDocumentViewed, + "CONSENT_GIVEN": coredata.ElectronicSignatureEventTypeConsentGiven, + "FULL_NAME_TYPED": coredata.ElectronicSignatureEventTypeFullNameTyped, + "SIGNATURE_ACCEPTED": coredata.ElectronicSignatureEventTypeSignatureAccepted, + "SIGNATURE_COMPLETED": coredata.ElectronicSignatureEventTypeSignatureCompleted, + "SEAL_COMPUTED": coredata.ElectronicSignatureEventTypeSealComputed, + "TIMESTAMP_REQUESTED": coredata.ElectronicSignatureEventTypeTimestampRequested, + "CERTIFICATE_GENERATED": coredata.ElectronicSignatureEventTypeCertificateGenerated, + "PROCESSING_ERROR": coredata.ElectronicSignatureEventTypeProcessingError, + } + marshalNElectronicSignatureEventType2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureEventType = map[coredata.ElectronicSignatureEventType]string{ + coredata.ElectronicSignatureEventTypeDocumentViewed: "DOCUMENT_VIEWED", + coredata.ElectronicSignatureEventTypeConsentGiven: "CONSENT_GIVEN", + coredata.ElectronicSignatureEventTypeFullNameTyped: "FULL_NAME_TYPED", + coredata.ElectronicSignatureEventTypeSignatureAccepted: "SIGNATURE_ACCEPTED", + coredata.ElectronicSignatureEventTypeSignatureCompleted: "SIGNATURE_COMPLETED", + coredata.ElectronicSignatureEventTypeSealComputed: "SEAL_COMPUTED", + coredata.ElectronicSignatureEventTypeTimestampRequested: "TIMESTAMP_REQUESTED", + coredata.ElectronicSignatureEventTypeCertificateGenerated: "CERTIFICATE_GENERATED", + coredata.ElectronicSignatureEventTypeProcessingError: "PROCESSING_ERROR", + } +) + +func (ec *executionContext) unmarshalNElectronicSignatureStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureStatus(ctx context.Context, v any) (coredata.ElectronicSignatureStatus, error) { + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNElectronicSignatureStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureStatus[tmp] + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNElectronicSignatureStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureStatus(ctx context.Context, sel ast.SelectionSet, v coredata.ElectronicSignatureStatus) graphql.Marshaler { + _ = sel + res := graphql.MarshalString(marshalNElectronicSignatureStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureStatus[v]) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +var ( + unmarshalNElectronicSignatureStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureStatus = map[string]coredata.ElectronicSignatureStatus{ + "PENDING": coredata.ElectronicSignatureStatusPending, + "ACCEPTED": coredata.ElectronicSignatureStatusAccepted, + "PROCESSING": coredata.ElectronicSignatureStatusProcessing, + "COMPLETED": coredata.ElectronicSignatureStatusCompleted, + "FAILED": coredata.ElectronicSignatureStatusFailed, + } + marshalNElectronicSignatureStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐElectronicSignatureStatus = map[coredata.ElectronicSignatureStatus]string{ + coredata.ElectronicSignatureStatusPending: "PENDING", + coredata.ElectronicSignatureStatusAccepted: "ACCEPTED", + coredata.ElectronicSignatureStatusProcessing: "PROCESSING", + coredata.ElectronicSignatureStatusCompleted: "COMPLETED", + coredata.ElectronicSignatureStatusFailed: "FAILED", + } +) + func (ec *executionContext) unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx context.Context, v any) (mail.Addr, error) { res, err := mail1.UnmarshalAddrScalar(v) return res, graphql.ErrorOnPath(ctx, err) @@ -11720,6 +12882,11 @@ func (ec *executionContext) marshalNPageInfo2ᚖgoᚗproboᚗincᚋproboᚋpkg return ec._PageInfo(ctx, sel, v) } +func (ec *executionContext) unmarshalNRecordSigningEventInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐRecordSigningEventInput(ctx context.Context, v any) (types.RecordSigningEventInput, error) { + res, err := ec.unmarshalInputRecordSigningEventInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalNRequestAccessesPayload2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐRequestAccessesPayload(ctx context.Context, sel ast.SelectionSet, v types.RequestAccessesPayload) graphql.Marshaler { return ec._RequestAccessesPayload(ctx, sel, &v) } @@ -12370,6 +13537,13 @@ func (ec *executionContext) marshalN__TypeKind2string(ctx context.Context, sel a return res } +func (ec *executionContext) marshalOAcceptElectronicSignaturePayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐAcceptElectronicSignaturePayload(ctx context.Context, sel ast.SelectionSet, v *types.AcceptElectronicSignaturePayload) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._AcceptElectronicSignaturePayload(ctx, sel, v) +} + func (ec *executionContext) marshalOAcceptNonDisclosureAgreementPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐAcceptNonDisclosureAgreementPayload(ctx context.Context, sel ast.SelectionSet, v *types.AcceptNonDisclosureAgreementPayload) graphql.Marshaler { if v == nil { return graphql.Null @@ -12425,6 +13599,31 @@ func (ec *executionContext) marshalOCursorKey2ᚖgoᚗproboᚗincᚋproboᚋpkg return res } +func (ec *executionContext) unmarshalODatetime2ᚖtimeᚐTime(ctx context.Context, v any) (*time.Time, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalTime(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalODatetime2ᚖtimeᚐTime(ctx context.Context, sel ast.SelectionSet, v *time.Time) graphql.Marshaler { + if v == nil { + return graphql.Null + } + _ = sel + _ = ctx + res := graphql.MarshalTime(*v) + return res +} + +func (ec *executionContext) marshalOElectronicSignature2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐElectronicSignature(ctx context.Context, sel ast.SelectionSet, v *types.ElectronicSignature) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._ElectronicSignature(ctx, sel, v) +} + func (ec *executionContext) marshalOIdentity2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐIdentity(ctx context.Context, sel ast.SelectionSet, v *types.Identity) graphql.Marshaler { if v == nil { return graphql.Null @@ -12450,6 +13649,13 @@ func (ec *executionContext) marshalOInt2ᚖint(ctx context.Context, sel ast.Sele return res } +func (ec *executionContext) marshalORecordSigningEventPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐRecordSigningEventPayload(ctx context.Context, sel ast.SelectionSet, v *types.RecordSigningEventPayload) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._RecordSigningEventPayload(ctx, sel, v) +} + func (ec *executionContext) marshalOReport2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐReport(ctx context.Context, sel ast.SelectionSet, v *types.Report) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/pkg/server/api/trust/v1/types/electronic_signature.go b/pkg/server/api/trust/v1/types/electronic_signature.go new file mode 100644 index 000000000..dbc541bfb --- /dev/null +++ b/pkg/server/api/trust/v1/types/electronic_signature.go @@ -0,0 +1,32 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package types + +import ( + "go.probo.inc/probo/pkg/coredata" +) + +func NewElectronicSignature(es *coredata.ElectronicSignature) *ElectronicSignature { + return &ElectronicSignature{ + ID: es.ID, + Status: es.Status, + DocumentType: es.DocumentType, + ConsentText: es.ConsentText, + LastError: es.LastError, + SignedAt: es.SignedAt, + CreatedAt: es.CreatedAt, + UpdatedAt: es.UpdatedAt, + } +} diff --git a/pkg/server/api/trust/v1/types/types.go b/pkg/server/api/trust/v1/types/types.go index 72e380461..f0ad66e50 100644 --- a/pkg/server/api/trust/v1/types/types.go +++ b/pkg/server/api/trust/v1/types/types.go @@ -16,6 +16,15 @@ type Node interface { GetID() gid.GID } +type AcceptElectronicSignatureInput struct { + SignatureID gid.GID `json:"signatureId"` + FullName string `json:"fullName"` +} + +type AcceptElectronicSignaturePayload struct { + Signature *ElectronicSignature `json:"signature"` +} + type AcceptNonDisclosureAgreementInput struct { FullName string `json:"fullName"` } @@ -64,6 +73,20 @@ type DocumentEdge struct { Node *Document `json:"node"` } +type ElectronicSignature struct { + ID gid.GID `json:"id"` + Status coredata.ElectronicSignatureStatus `json:"status"` + DocumentType coredata.ElectronicSignatureDocumentType `json:"documentType"` + ConsentText string `json:"consentText"` + LastError *string `json:"lastError,omitempty"` + SignedAt *time.Time `json:"signedAt,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func (ElectronicSignature) IsNode() {} +func (this ElectronicSignature) GetID() gid.GID { return this.ID } + type ExportDocumentPDFInput struct { DocumentID gid.GID `json:"documentId"` } @@ -136,6 +159,15 @@ type PageInfo struct { type Query struct { } +type RecordSigningEventInput struct { + SignatureID gid.GID `json:"signatureId"` + EventType coredata.ElectronicSignatureEventType `json:"eventType"` +} + +type RecordSigningEventPayload struct { + Success bool `json:"success"` +} + type Report struct { ID gid.GID `json:"id"` Filename string `json:"filename"` @@ -178,6 +210,7 @@ type TrustCenter struct { DarkLogoFileURL *string `json:"darkLogoFileUrl,omitempty"` NdaFileName *string `json:"ndaFileName,omitempty"` NdaFileURL *string `json:"ndaFileUrl,omitempty"` + NdaSignature *ElectronicSignature `json:"ndaSignature,omitempty"` Organization *Organization `json:"organization"` IsViewerMember bool `json:"isViewerMember"` HasAcceptedNonDisclosureAgreement bool `json:"hasAcceptedNonDisclosureAgreement"` diff --git a/pkg/server/api/trust/v1/v1_resolver.go b/pkg/server/api/trust/v1/v1_resolver.go index 1f845b96d..e0d725cbd 100644 --- a/pkg/server/api/trust/v1/v1_resolver.go +++ b/pkg/server/api/trust/v1/v1_resolver.go @@ -14,6 +14,7 @@ import ( "go.gearno.de/kit/log" "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/esign" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/page" @@ -648,6 +649,71 @@ func (r *mutationResolver) RequestTrustCenterFileAccess(ctx context.Context, inp }, nil } +// AcceptElectronicSignature is the resolver for the acceptElectronicSignature field. +func (r *mutationResolver) AcceptElectronicSignature(ctx context.Context, input types.AcceptElectronicSignatureInput) (*types.AcceptElectronicSignaturePayload, error) { + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated") + } + + httpReq := gqlutils.HTTPRequestFromContext(ctx) + + if _, err := r.iam.AuthService.UpdateIdentity(ctx, identity.ID, input.FullName); err != nil { + var errNotFound *iam.ErrIdentityNotFound + if errors.As(err, &errNotFound) { + return nil, gqlutils.NotFound(ctx, err) + } + + r.logger.ErrorCtx(ctx, "cannot update identity", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + if err := r.esign.AcceptSignature(ctx, &esign.AcceptSignatureRequest{ + SignatureID: input.SignatureID, + SignerFullName: input.FullName, + SignerEmail: identity.EmailAddress, + SignerIPAddr: httpReq.RemoteAddr, + SignerUA: httpReq.UserAgent(), + }); err != nil { + r.logger.ErrorCtx(ctx, "cannot accept electronic signature", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + sig, err := r.esign.LoadSignatureByID(ctx, input.SignatureID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot load electronic signature", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return &types.AcceptElectronicSignaturePayload{ + Signature: types.NewElectronicSignature(sig), + }, nil +} + +// RecordSigningEvent is the resolver for the recordSigningEvent field. +func (r *mutationResolver) RecordSigningEvent(ctx context.Context, input types.RecordSigningEventInput) (*types.RecordSigningEventPayload, error) { + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated") + } + + httpReq := gqlutils.HTTPRequestFromContext(ctx) + + if err := r.esign.RecordEvent(ctx, &esign.RecordEventRequest{ + SignatureID: input.SignatureID, + EventType: input.EventType, + EventSource: coredata.ElectronicSignatureEventSourceClient, + ActorEmail: identity.EmailAddress, + ActorIPAddr: httpReq.RemoteAddr, + ActorUA: httpReq.UserAgent(), + }); err != nil { + r.logger.ErrorCtx(ctx, "cannot record signing event", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return &types.RecordSigningEventPayload{Success: true}, nil +} + // LogoURL is the resolver for the logoUrl field. func (r *organizationResolver) LogoURL(ctx context.Context, obj *types.Organization) (*string, error) { trustService := r.TrustService(ctx, obj.ID.TenantID()) @@ -868,6 +934,32 @@ func (r *trustCenterResolver) NdaFileURL(ctx context.Context, obj *types.TrustCe return &fileURL, nil } +// NdaSignature is the resolver for the ndaSignature field. +func (r *trustCenterResolver) NdaSignature(ctx context.Context, obj *types.TrustCenter) (*types.ElectronicSignature, error) { + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return nil, nil + } + + trustCenter := compliancepage.CompliancePageFromContext(ctx) + if trustCenter == nil || trustCenter.NonDisclosureAgreementFileID == nil { + return nil, nil + } + + sig, err := r.esign.LoadSignatureByOrgEmailAndDocType( + ctx, + trustCenter.OrganizationID, + identity.EmailAddress.String(), + coredata.ElectronicSignatureDocumentTypeNDA, + *trustCenter.NonDisclosureAgreementFileID, + ) + if err != nil { + return nil, nil + } + + return types.NewElectronicSignature(sig), nil +} + // Organization is the resolver for the organization field. func (r *trustCenterResolver) Organization(ctx context.Context, obj *types.TrustCenter) (*types.Organization, error) { return obj.Organization, nil diff --git a/pkg/server/server.go b/pkg/server/server.go index 5874e893a..4f63580c4 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -25,6 +25,7 @@ import ( "go.probo.inc/probo/pkg/agents" "go.probo.inc/probo/pkg/baseurl" "go.probo.inc/probo/pkg/connector" + "go.probo.inc/probo/pkg/esign" "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/probo" "go.probo.inc/probo/pkg/securecookie" @@ -43,6 +44,7 @@ type Config struct { Probo *probo.Service IAM *iam.Service Trust *trust.Service + ESign *esign.Service Slack *slack.Service Cookie securecookie.Config TokenSecret string @@ -70,6 +72,7 @@ func NewServer(cfg Config) (*Server, error) { Probo: cfg.Probo, IAM: cfg.IAM, Trust: cfg.Trust, + ESign: cfg.ESign, Slack: cfg.Slack, Cookie: cfg.Cookie, TokenSecret: cfg.TokenSecret, diff --git a/pkg/trust/service.go b/pkg/trust/service.go index ab357b097..ef1dccff9 100644 --- a/pkg/trust/service.go +++ b/pkg/trust/service.go @@ -22,8 +22,10 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "go.gearno.de/kit/log" "go.gearno.de/kit/pg" + "go.probo.inc/probo/packages/emails" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/crypto/cipher" + "go.probo.inc/probo/pkg/esign" "go.probo.inc/probo/pkg/filemanager" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/html2pdf" @@ -43,6 +45,7 @@ type ( slackSigningSecret string baseURL string iam *iam.Service + esign *esign.Service html2pdfConverter *html2pdf.Converter fileManager *filemanager.Service logger *log.Logger @@ -58,6 +61,7 @@ type ( encryptionKey cipher.EncryptionKey baseURL string iam *iam.Service + esign *esign.Service html2pdfConverter *html2pdf.Converter fileManager *filemanager.Service logger *log.Logger @@ -83,6 +87,7 @@ func NewService( encryptionKey cipher.EncryptionKey, slackSigningSecret string, iam *iam.Service, + esignSvc *esign.Service, html2pdfConverter *html2pdf.Converter, fileManagerService *filemanager.Service, logger *log.Logger, @@ -96,6 +101,7 @@ func NewService( slackSigningSecret: slackSigningSecret, baseURL: baseURL, iam: iam, + esign: esignSvc, html2pdfConverter: html2pdfConverter, fileManager: fileManagerService, logger: logger, @@ -113,6 +119,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService { encryptionKey: s.encryptionKey, baseURL: s.baseURL, iam: s.iam, + esign: s.esign, html2pdfConverter: s.html2pdfConverter, fileManager: s.fileManager, logger: s.logger, @@ -253,6 +260,21 @@ func (s *Service) GetCustomDomainByOrganizationID(ctx context.Context, organizat return customDomain, err } +// EmailPresenterConfigByOrganizationID resolves the emails.PresenterConfig for +// the trust center that belongs to the given organization. This is used by the +// esign certificate worker which needs per-org branding at render time. +func (s *Service) EmailPresenterConfigByOrganizationID(ctx context.Context, orgID gid.GID) (emails.PresenterConfig, error) { + var trustCenter coredata.TrustCenter + scope := coredata.NewScopeFromObjectID(orgID) + err := s.pg.WithConn(ctx, func(conn pg.Conn) error { + return trustCenter.LoadByOrganizationID(ctx, conn, scope, orgID) + }) + if err != nil { + return emails.PresenterConfig{}, fmt.Errorf("cannot load trust center for org %s: %w", orgID, err) + } + return s.WithTenant(orgID.TenantID()).TrustCenters.EmailPresenterConfig(ctx, trustCenter.ID) +} + func (s *Service) GetMembershipByCompliancePageIDAndEmail(ctx context.Context, compliancePageID gid.GID, email mail.Addr) (*coredata.TrustCenterAccess, error) { membership := &coredata.TrustCenterAccess{} diff --git a/pkg/trust/trust_center_access_service.go b/pkg/trust/trust_center_access_service.go index 6e63c2856..c168956f1 100644 --- a/pkg/trust/trust_center_access_service.go +++ b/pkg/trust/trust_center_access_service.go @@ -25,6 +25,7 @@ import ( "go.gearno.de/kit/pg" "go.probo.inc/probo/packages/emails" "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/esign" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/mail" @@ -157,6 +158,23 @@ func (s TrustCenterAccessService) Request( if err := access.Insert(ctx, tx, s.svc.scope); err != nil { return fmt.Errorf("cannot insert trust center access: %w", err) } + + // Create PENDING electronic signature when NDA is configured. + if trustCenter.NonDisclosureAgreementFileID != nil && s.svc.esign != nil { + _, err := s.svc.esign.CreateSignature( + ctx, + tx, + &esign.CreateSignatureRequest{ + OrganizationID: access.OrganizationID, + DocumentType: coredata.ElectronicSignatureDocumentTypeNDA, + FileID: *trustCenter.NonDisclosureAgreementFileID, + SignerEmail: access.Email, + }, + ) + if err != nil { + return fmt.Errorf("cannot create pending signature: %w", err) + } + } } var existingAccesses coredata.TrustCenterDocumentAccesses @@ -226,21 +244,50 @@ func (s TrustCenterAccessService) Request( } func (s TrustCenterAccessService) HasAcceptedNonDisclosureAgreement(ctx context.Context, trustCenterID gid.GID, email mail.Addr) (bool, error) { - access := &coredata.TrustCenterAccess{} + var access coredata.TrustCenterAccess err := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error { - err := access.LoadByTrustCenterIDAndEmail(ctx, conn, s.svc.scope, trustCenterID, email) - if err != nil { - return fmt.Errorf("cannot load trust center access: %w", err) - } - - return nil + return access.LoadByTrustCenterIDAndEmail(ctx, conn, s.svc.scope, trustCenterID, email) }) if err != nil { return false, nil } - return access.HasAcceptedNonDisclosureAgreement, nil + // Legacy path: check the old boolean field. + if access.HasAcceptedNonDisclosureAgreement { + return true, nil + } + + // New path: check for an electronic signature in ACCEPTED or later status. + if s.svc.esign != nil { + trustCenter := &coredata.TrustCenter{} + err := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error { + return trustCenter.LoadByID(ctx, conn, s.svc.scope, trustCenterID) + }) + if err != nil { + return false, nil + } + + if trustCenter.NonDisclosureAgreementFileID != nil { + sig, err := s.svc.esign.LoadSignatureByOrgEmailAndDocType( + ctx, + trustCenter.OrganizationID, + email.String(), + coredata.ElectronicSignatureDocumentTypeNDA, + *trustCenter.NonDisclosureAgreementFileID, + ) + if err == nil { + switch sig.Status { + case coredata.ElectronicSignatureStatusAccepted, + coredata.ElectronicSignatureStatusProcessing, + coredata.ElectronicSignatureStatusCompleted: + return true, nil + } + } + } + } + + return false, nil } type AcceptNDARequest struct {