From 7322201dab85bd15452e1eabb17e20d58938a7a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Fri, 9 Jan 2026 19:14:17 +0100 Subject: [PATCH] Plug trust center part 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- .../src/components/OrganizationSidebar.tsx | 6 +- .../src/components/RequestAccessDialog.tsx | 46 +- ...estAccessDialogDocumentMutation.graphql.ts | 6 +- .../RequestAccessDialogMutation.graphql.ts | 6 +- ...questAccessDialogReportMutation.graphql.ts | 6 +- ...ssDialogTrustCenterFileMutation.graphql.ts | 6 +- apps/trust/src/hooks/useIsAuthenticated.ts | 6 - apps/trust/src/layouts/MainLayout.tsx | 14 +- apps/trust/src/providers/Viewer.tsx | 11 + apps/trust/src/queries/TrustGraph.ts | 4 + .../TrustGraphCurrentQuery.graphql.ts | 148 +- apps/trust/src/routes.tsx | 10 +- apps/trust/vite.config.ts | 6 +- packages/emails/emails.go | 17 + packages/emails/scripts/build.ts | 68 +- packages/emails/src/MagicLink.tsx | 28 + packages/emails/src/TrustCenterAccess.tsx | 23 +- packages/emails/templates/magic-link.txt | 11 + packages/prettier/prettier.config.js | 2 +- pkg/iam/auth_service.go | 164 ++ pkg/iam/service.go | 3 + pkg/mail/addr.go | 13 + pkg/probo/service.go | 22 + pkg/probo/trust_center_access_service.go | 6 +- pkg/probod/auth_config.go | 1 + pkg/probod/probod.go | 2 + pkg/server/api/authn/context.go | 1 + pkg/server/api/connect/v1/v1_resolver.go | 6 +- pkg/server/api/console/v1/graphql_handler.go | 1 + pkg/server/api/console/v1/resolver.go | 8 +- pkg/server/api/console/v1/types/audit.go | 10 +- pkg/server/api/console/v1/v1_resolver.go | 25 +- pkg/server/api/trust/v1/resolver.go | 53 +- pkg/server/api/trust/v1/schema.graphql | 36 +- pkg/server/api/trust/v1/schema/schema.go | 662 ++++++++- .../trust/v1/trust_center_access_handler.go | 139 -- pkg/server/api/trust/v1/types/types.go | 50 +- pkg/server/api/trust/v1/v1_resolver.go | 1315 +++++++++-------- pkg/server/gqlutils/errors.go | 8 + pkg/server/server.go | 19 +- pkg/trust/trust_center_access_service.go | 24 +- 41 files changed, 1959 insertions(+), 1033 deletions(-) delete mode 100644 apps/trust/src/hooks/useIsAuthenticated.ts create mode 100644 apps/trust/src/providers/Viewer.tsx create mode 100644 packages/emails/src/MagicLink.tsx create mode 100644 packages/emails/templates/magic-link.txt delete mode 100644 pkg/server/api/trust/v1/trust_center_access_handler.go diff --git a/apps/trust/src/components/OrganizationSidebar.tsx b/apps/trust/src/components/OrganizationSidebar.tsx index ca8bfa377..8060db6c3 100644 --- a/apps/trust/src/components/OrganizationSidebar.tsx +++ b/apps/trust/src/components/OrganizationSidebar.tsx @@ -1,11 +1,11 @@ import { useTranslate } from "@probo/i18n"; import { Button, Card, IconBlock, IconLock, IconMedal } from "@probo/ui"; import type { TrustGraphQuery$data } from "/queries/__generated__/TrustGraphQuery.graphql"; -import type { PropsWithChildren } from "react"; +import { use, type PropsWithChildren } from "react"; import { domain } from "@probo/helpers"; import { AuditRowAvatar } from "./AuditRow"; import { RequestAccessDialog } from "./RequestAccessDialog"; -import { useIsAuthenticated } from "/hooks/useIsAuthenticated"; +import { Viewer } from "/providers/Viewer"; export function OrganizationSidebar({ trustCenter, @@ -13,7 +13,7 @@ export function OrganizationSidebar({ trustCenter: TrustGraphQuery$data["trustCenterBySlug"]; }) { const { __ } = useTranslate(); - const isAuthenticated = useIsAuthenticated(); + const isAuthenticated = !!use(Viewer); if (!trustCenter) { return null; diff --git a/apps/trust/src/components/RequestAccessDialog.tsx b/apps/trust/src/components/RequestAccessDialog.tsx index 6597c9cad..84eb1360f 100644 --- a/apps/trust/src/components/RequestAccessDialog.tsx +++ b/apps/trust/src/components/RequestAccessDialog.tsx @@ -15,9 +15,9 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { graphql } from "relay-runtime"; import { useMutationWithToasts } from "/hooks/useMutationWithToast"; import { useTrustCenter } from "/hooks/useTrustCenter"; -import { type FormEventHandler, type PropsWithChildren } from "react"; -import { useIsAuthenticated } from "/hooks/useIsAuthenticated.ts"; +import { use, type FormEventHandler, type PropsWithChildren } from "react"; import { InvalidError } from "/providers/RelayProviders"; +import { Viewer } from "/providers/Viewer"; type Props = PropsWithChildren<{ documentId?: string; @@ -27,7 +27,7 @@ type Props = PropsWithChildren<{ }>; const schema = z.object({ - name: z.string(), + fullName: z.string(), email: z.string().email(), }); @@ -41,18 +41,28 @@ export function RequestAccessDialog({ const trustCenter = useTrustCenter(); const { toast } = useToast(); const { __ } = useTranslate(); - const { handleSubmit, register, setError, formState } = useFormWithSchema(schema, { - defaultValues: { - name: "", - email: "", + const viewer = use(Viewer); + const { handleSubmit, register, setError, formState } = useFormWithSchema( + schema, + { + defaultValues: { + fullName: "", + email: "", + }, }, - }); - const isAuthenticated = useIsAuthenticated(); + ); const dialogRef = useDialogRef(); - const [commitMutation, isMutating] = useMutation({ documentId, reportId, trustCenterFileId }); + const [commitMutation, isMutating] = useMutation({ + documentId, + reportId, + trustCenterFileId, + }); const submitCallback = (data: z.infer | null) => { - commitMutation(data) + commitMutation({ + email: data?.email ?? viewer?.email ?? "", + fullName: data?.fullName ?? viewer?.fullName ?? "", + }) .then(() => { onSuccess?.(); toast({ @@ -65,7 +75,7 @@ export function RequestAccessDialog({ .catch((error) => { if (error instanceof InvalidError) { if (error.field === "email") { - setError(error.field, {message: error.message}) + setError(error.field, { message: error.message }); } return; } @@ -77,7 +87,7 @@ export function RequestAccessDialog({ }); }; - const onSubmit: FormEventHandler = isAuthenticated + const onSubmit: FormEventHandler = viewer ? (e) => { e.preventDefault(); submitCallback(null); @@ -102,12 +112,12 @@ export function RequestAccessDialog({ trustCenter.organization.name, )}

- {!isAuthenticated && ( + {!viewer && (
> + * @generated SignedSource<<5113feef2c7f7b13a2d6348202086916>> * @lightSyntaxTransform * @nogrep */ @@ -11,8 +11,8 @@ import { ConcreteRequest } from 'relay-runtime'; export type RequestDocumentAccessInput = { documentId: string; - email?: any | null | undefined; - name?: string | null | undefined; + email: any; + fullName: string; trustCenterId: string; }; export type RequestAccessDialogDocumentMutation$variables = { diff --git a/apps/trust/src/components/__generated__/RequestAccessDialogMutation.graphql.ts b/apps/trust/src/components/__generated__/RequestAccessDialogMutation.graphql.ts index 3df881031..7e568a46f 100644 --- a/apps/trust/src/components/__generated__/RequestAccessDialogMutation.graphql.ts +++ b/apps/trust/src/components/__generated__/RequestAccessDialogMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<19ada29ea9b8f557b4fcc39f57bc3aec>> + * @generated SignedSource<<294b6a25a178cf3bd9269591ef61381f>> * @lightSyntaxTransform * @nogrep */ @@ -10,8 +10,8 @@ import { ConcreteRequest } from 'relay-runtime'; export type RequestAllAccessesInput = { - email?: any | null | undefined; - name?: string | null | undefined; + email: any; + fullName: string; trustCenterId: string; }; export type RequestAccessDialogMutation$variables = { diff --git a/apps/trust/src/components/__generated__/RequestAccessDialogReportMutation.graphql.ts b/apps/trust/src/components/__generated__/RequestAccessDialogReportMutation.graphql.ts index 05c0f1997..8759c2321 100644 --- a/apps/trust/src/components/__generated__/RequestAccessDialogReportMutation.graphql.ts +++ b/apps/trust/src/components/__generated__/RequestAccessDialogReportMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<858effd78b88a123430edcf6fcb100ed>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -10,8 +10,8 @@ import { ConcreteRequest } from 'relay-runtime'; export type RequestReportAccessInput = { - email?: any | null | undefined; - name?: string | null | undefined; + email: any; + fullName: string; reportId: string; trustCenterId: string; }; diff --git a/apps/trust/src/components/__generated__/RequestAccessDialogTrustCenterFileMutation.graphql.ts b/apps/trust/src/components/__generated__/RequestAccessDialogTrustCenterFileMutation.graphql.ts index f4c55b39f..e57bdc053 100644 --- a/apps/trust/src/components/__generated__/RequestAccessDialogTrustCenterFileMutation.graphql.ts +++ b/apps/trust/src/components/__generated__/RequestAccessDialogTrustCenterFileMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<32242c5e0b4dc9c9368c0e2f4034d95e>> + * @generated SignedSource<<1e744bd11b2d5dd4d982bf1ce8b2a721>> * @lightSyntaxTransform * @nogrep */ @@ -10,8 +10,8 @@ import { ConcreteRequest } from 'relay-runtime'; export type RequestTrustCenterFileAccessInput = { - email?: any | null | undefined; - name?: string | null | undefined; + email: any; + fullName: string; trustCenterFileId: string; trustCenterId: string; }; diff --git a/apps/trust/src/hooks/useIsAuthenticated.ts b/apps/trust/src/hooks/useIsAuthenticated.ts deleted file mode 100644 index 64537af87..000000000 --- a/apps/trust/src/hooks/useIsAuthenticated.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { useContext } from "react"; -import { AuthContext } from "/providers/AuthProvider"; - -export function useIsAuthenticated(): boolean { - return useContext(AuthContext).isAuthenticated; -} diff --git a/apps/trust/src/layouts/MainLayout.tsx b/apps/trust/src/layouts/MainLayout.tsx index ae7c96ad3..425c1ab6e 100644 --- a/apps/trust/src/layouts/MainLayout.tsx +++ b/apps/trust/src/layouts/MainLayout.tsx @@ -6,8 +6,8 @@ import { useTranslate } from "@probo/i18n"; import { OrganizationSidebar } from "/components/OrganizationSidebar"; import { Outlet } from "react-router"; import { NDADialog } from "/components/NDADialog"; -import { AuthProvider } from "/providers/AuthProvider"; import { TrustCenterProvider } from "/providers/TrustCenterProvider"; +import { Viewer } from "/providers/Viewer"; type Props = { queryRef: PreloadedQuery; @@ -26,7 +26,7 @@ export function MainLayout(props: Props) { !trustCenter.hasAcceptedNonDisclosureAgreement && trustCenter.ndaFileUrl; return ( - + {showNDADialog && ( {__("Overview")} - - {__("Documents")} - - - {__("Subprocessors")} - + {__("Documents")} + {__("Subprocessors")} @@ -59,6 +55,6 @@ export function MainLayout(props: Props) { {__("Powered by")} - + ); } diff --git a/apps/trust/src/providers/Viewer.tsx b/apps/trust/src/providers/Viewer.tsx new file mode 100644 index 000000000..96f9a27dc --- /dev/null +++ b/apps/trust/src/providers/Viewer.tsx @@ -0,0 +1,11 @@ +import { createContext } from "react"; + +interface ViewerContextValue { + fullName: string; + email: string; +} + +export const Viewer = createContext({ + email: "", + fullName: "", +}); diff --git a/apps/trust/src/queries/TrustGraph.ts b/apps/trust/src/queries/TrustGraph.ts index 95ddd9d89..9a1240c35 100644 --- a/apps/trust/src/queries/TrustGraph.ts +++ b/apps/trust/src/queries/TrustGraph.ts @@ -82,6 +82,10 @@ export const trustVendorsQuery = graphql` // Queries for custom domain (subdomain) approach export const currentTrustGraphQuery = graphql` query TrustGraphCurrentQuery { + viewer { + email + fullName + } currentTrustCenter { id slug diff --git a/apps/trust/src/queries/__generated__/TrustGraphCurrentQuery.graphql.ts b/apps/trust/src/queries/__generated__/TrustGraphCurrentQuery.graphql.ts index 2e82a16b4..9b3aa7239 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<> + * @generated SignedSource<<1f5a4da3a77a893daebeceb249612a72>> * @lightSyntaxTransform * @nogrep */ @@ -37,6 +37,10 @@ export type TrustGraphCurrentQuery$data = { readonly slug: string; readonly " $fragmentSpreads": FragmentRefs<"OverviewPageFragment">; } | null | undefined; + readonly viewer: { + readonly email: any; + readonly fullName: string; + } | null | undefined; }; export type TrustGraphCurrentQuery = { response: TrustGraphCurrentQuery$data; @@ -48,115 +52,122 @@ var v0 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "id", + "name": "email", "storageKey": null }, v1 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "slug", + "name": "fullName", "storageKey": null }, v2 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "isUserAuthenticated", + "name": "id", "storageKey": null }, v3 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "hasAcceptedNonDisclosureAgreement", + "name": "slug", "storageKey": null }, v4 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "ndaFileName", + "name": "isUserAuthenticated", "storageKey": null }, v5 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "ndaFileUrl", + "name": "hasAcceptedNonDisclosureAgreement", "storageKey": null }, v6 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "name", + "name": "ndaFileName", "storageKey": null }, v7 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "description", + "name": "ndaFileUrl", "storageKey": null }, v8 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "websiteUrl", + "name": "name", "storageKey": null }, v9 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "logoUrl", + "name": "description", "storageKey": null }, v10 = { "alias": null, "args": null, "kind": "ScalarField", - "name": "email", + "name": "websiteUrl", "storageKey": null }, v11 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "logoUrl", + "storageKey": null +}, +v12 = { "alias": null, "args": null, "kind": "ScalarField", "name": "headquarterAddress", "storageKey": null }, -v12 = [ +v13 = [ { "kind": "Literal", "name": "first", "value": 50 } ], -v13 = { +v14 = { "alias": null, "args": null, "kind": "ScalarField", "name": "category", "storageKey": null }, -v14 = [ +v15 = [ { "kind": "Literal", "name": "first", "value": 5 } ], -v15 = { +v16 = { "alias": null, "args": null, "kind": "ScalarField", "name": "isUserAuthorized", "storageKey": null }, -v16 = { +v17 = { "alias": null, "args": null, "kind": "ScalarField", @@ -170,6 +181,19 @@ return { "metadata": null, "name": "TrustGraphCurrentQuery", "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Identity", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "storageKey": null + }, { "alias": null, "args": null, @@ -178,12 +202,12 @@ return { "name": "currentTrustCenter", "plural": false, "selections": [ - (v0/*: any*/), - (v1/*: any*/), (v2/*: any*/), (v3/*: any*/), (v4/*: any*/), (v5/*: any*/), + (v6/*: any*/), + (v7/*: any*/), { "alias": null, "args": null, @@ -192,12 +216,12 @@ return { "name": "organization", "plural": false, "selections": [ - (v6/*: any*/), - (v7/*: any*/), (v8/*: any*/), (v9/*: any*/), (v10/*: any*/), - (v11/*: any*/) + (v11/*: any*/), + (v0/*: any*/), + (v12/*: any*/) ], "storageKey": null }, @@ -208,7 +232,7 @@ return { }, { "alias": null, - "args": (v12/*: any*/), + "args": (v13/*: any*/), "concreteType": "AuditConnection", "kind": "LinkedField", "name": "audits", @@ -230,7 +254,7 @@ return { "name": "node", "plural": false, "selections": [ - (v0/*: any*/), + (v2/*: any*/), { "args": null, "kind": "FragmentSpread", @@ -258,6 +282,20 @@ return { "kind": "Operation", "name": "TrustGraphCurrentQuery", "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Identity", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/) + ], + "storageKey": null + }, { "alias": null, "args": null, @@ -266,12 +304,12 @@ return { "name": "currentTrustCenter", "plural": false, "selections": [ - (v0/*: any*/), - (v1/*: any*/), (v2/*: any*/), (v3/*: any*/), (v4/*: any*/), (v5/*: any*/), + (v6/*: any*/), + (v7/*: any*/), { "alias": null, "args": null, @@ -280,13 +318,13 @@ return { "name": "organization", "plural": false, "selections": [ - (v6/*: any*/), - (v7/*: any*/), (v8/*: any*/), (v9/*: any*/), (v10/*: any*/), (v11/*: any*/), - (v0/*: any*/) + (v0/*: any*/), + (v12/*: any*/), + (v2/*: any*/) ], "storageKey": null }, @@ -320,10 +358,10 @@ return { "name": "node", "plural": false, "selections": [ - (v0/*: any*/), - (v6/*: any*/), - (v9/*: any*/), - (v8/*: any*/) + (v2/*: any*/), + (v8/*: any*/), + (v11/*: any*/), + (v10/*: any*/) ], "storageKey": null } @@ -363,7 +401,7 @@ return { "name": "node", "plural": false, "selections": [ - (v0/*: any*/), + (v2/*: any*/), { "alias": null, "args": null, @@ -371,9 +409,9 @@ return { "name": "countries", "storageKey": null }, - (v6/*: any*/), - (v13/*: any*/), (v8/*: any*/), + (v14/*: any*/), + (v10/*: any*/), { "alias": null, "args": null, @@ -392,7 +430,7 @@ return { }, { "alias": null, - "args": (v14/*: any*/), + "args": (v15/*: any*/), "concreteType": "DocumentConnection", "kind": "LinkedField", "name": "documents", @@ -414,7 +452,7 @@ return { "name": "node", "plural": false, "selections": [ - (v0/*: any*/), + (v2/*: any*/), { "alias": null, "args": null, @@ -422,8 +460,8 @@ return { "name": "title", "storageKey": null }, - (v15/*: any*/), (v16/*: any*/), + (v17/*: any*/), { "alias": null, "args": null, @@ -442,7 +480,7 @@ return { }, { "alias": null, - "args": (v14/*: any*/), + "args": (v15/*: any*/), "concreteType": "TrustCenterFileConnection", "kind": "LinkedField", "name": "trustCenterFiles", @@ -464,11 +502,11 @@ return { "name": "node", "plural": false, "selections": [ - (v0/*: any*/), - (v13/*: any*/), - (v6/*: any*/), - (v15/*: any*/), - (v16/*: any*/) + (v2/*: any*/), + (v14/*: any*/), + (v8/*: any*/), + (v16/*: any*/), + (v17/*: any*/) ], "storageKey": null } @@ -480,7 +518,7 @@ return { }, { "alias": null, - "args": (v12/*: any*/), + "args": (v13/*: any*/), "concreteType": "AuditConnection", "kind": "LinkedField", "name": "audits", @@ -502,7 +540,7 @@ return { "name": "node", "plural": false, "selections": [ - (v0/*: any*/), + (v2/*: any*/), { "alias": null, "args": null, @@ -511,7 +549,7 @@ return { "name": "report", "plural": false, "selections": [ - (v0/*: any*/), + (v2/*: any*/), { "alias": null, "args": null, @@ -519,8 +557,8 @@ return { "name": "filename", "storageKey": null }, - (v15/*: any*/), - (v16/*: any*/) + (v16/*: any*/), + (v17/*: any*/) ], "storageKey": null }, @@ -532,8 +570,8 @@ return { "name": "framework", "plural": false, "selections": [ - (v0/*: any*/), - (v6/*: any*/), + (v2/*: any*/), + (v8/*: any*/), { "alias": null, "args": null, @@ -566,16 +604,16 @@ return { ] }, "params": { - "cacheID": "f098177c07810cbe7609821d63c22b0b", + "cacheID": "9d22288e0159409cf43bece191e3dc47", "id": null, "metadata": {}, "name": "TrustGraphCurrentQuery", "operationKind": "query", - "text": "query TrustGraphCurrentQuery {\n currentTrustCenter {\n id\n slug\n isUserAuthenticated\n hasAcceptedNonDisclosureAgreement\n ndaFileName\n ndaFileUrl\n organization {\n name\n description\n websiteUrl\n logoUrl\n email\n headquarterAddress\n id\n }\n ...OverviewPageFragment\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 id\n name\n category\n websiteUrl\n privacyPolicyUrl\n countries\n}\n" + "text": "query TrustGraphCurrentQuery {\n viewer {\n email\n fullName\n id\n }\n currentTrustCenter {\n id\n slug\n isUserAuthenticated\n hasAcceptedNonDisclosureAgreement\n ndaFileName\n ndaFileUrl\n organization {\n name\n description\n websiteUrl\n logoUrl\n email\n headquarterAddress\n id\n }\n ...OverviewPageFragment\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 id\n name\n category\n websiteUrl\n privacyPolicyUrl\n countries\n}\n" } }; })(); -(node as any).hash = "2fc7c45c2636a551c13d806f89e53c4a"; +(node as any).hash = "dd2040667d5cc7b9af9659e993ba6a5b"; export default node; diff --git a/apps/trust/src/routes.tsx b/apps/trust/src/routes.tsx index 5dfc6841b..b63841c19 100644 --- a/apps/trust/src/routes.tsx +++ b/apps/trust/src/routes.tsx @@ -56,7 +56,7 @@ const routes = [ { path: "/overview", loader: loaderFromQueryLoader(() => - loadQuery(consoleEnvironment, currentTrustGraphQuery, {}) + loadQuery(consoleEnvironment, currentTrustGraphQuery, {}), ), Component: withQueryRef(MainLayout), Fallback: MainSkeleton, @@ -72,7 +72,7 @@ const routes = [ { path: "/documents", loader: loaderFromQueryLoader(() => - loadQuery(consoleEnvironment, currentTrustGraphQuery, {}) + loadQuery(consoleEnvironment, currentTrustGraphQuery, {}), ), Component: withQueryRef(MainLayout), Fallback: MainSkeleton, @@ -81,7 +81,7 @@ const routes = [ { path: "", loader: loaderFromQueryLoader(() => - loadQuery(consoleEnvironment, currentTrustDocumentsQuery, {}) + loadQuery(consoleEnvironment, currentTrustDocumentsQuery, {}), ), Fallback: TabSkeleton, Component: withQueryRef(DocumentsPage), @@ -91,7 +91,7 @@ const routes = [ { path: "/subprocessors", loader: loaderFromQueryLoader(() => - loadQuery(consoleEnvironment, currentTrustGraphQuery, {}) + loadQuery(consoleEnvironment, currentTrustGraphQuery, {}), ), Component: withQueryRef(MainLayout), Fallback: MainSkeleton, @@ -100,7 +100,7 @@ const routes = [ { path: "", loader: loaderFromQueryLoader(() => - loadQuery(consoleEnvironment, currentTrustVendorsQuery, {}) + loadQuery(consoleEnvironment, currentTrustVendorsQuery, {}), ), Fallback: TabSkeleton, Component: withQueryRef(SubprocessorsPage), diff --git a/apps/trust/vite.config.ts b/apps/trust/vite.config.ts index 3b281c25e..9787ddda2 100644 --- a/apps/trust/vite.config.ts +++ b/apps/trust/vite.config.ts @@ -17,13 +17,17 @@ export default defineConfig({ target: "http://localhost:8080", changeOrigin: true, }, + "/trust/YJwjPEJCAAEAFgAAAZsTYtQt-FLmpawO/api": { + target: "http://localhost:8080", + changeOrigin: true, + }, }, }, resolve: { alias: { "/type": fileURLToPath(new URL("./src/type.ts", import.meta.url)), "/components": fileURLToPath( - new URL("./src/components", import.meta.url) + new URL("./src/components", import.meta.url), ), "/queries": fileURLToPath(new URL("./src/queries", import.meta.url)), "/helpers": fileURLToPath(new URL("./src/helpers", import.meta.url)), diff --git a/packages/emails/emails.go b/packages/emails/emails.go index 39024d830..1d84f769b 100644 --- a/packages/emails/emails.go +++ b/packages/emails/emails.go @@ -196,6 +196,23 @@ func RenderTrustCenterDocumentAccessRejected( return fmt.Sprintf(subjectTrustCenterDocumentAccessRejected, organizationName), textBody, htmlBody, err } +func RenderMagicLink(baseURL, fullName, magicLinkUrl string, tokenDuration time.Duration) (subject string, textBody string, htmlBody *string, err error) { + data := struct { + FullName string + MagicLinkUrl string + LogoURL string + DurationInMinutes int + }{ + FullName: fullName, + MagicLinkUrl: magicLinkUrl, + LogoURL: baseURL + logoURLPath, + DurationInMinutes: int(tokenDuration.Minutes()), + } + + textBody, htmlBody, err = renderEmail(trustCenterAccessTextTemplate, trustCenterAccessHTMLTemplate, data) + return subjectTrustCenterAccess, 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 347f34ed5..7c26b5827 100644 --- a/packages/emails/scripts/build.ts +++ b/packages/emails/scripts/build.ts @@ -1,17 +1,17 @@ -import { render } from '@react-email/components'; -import { copyFile, mkdir, writeFile } from 'node:fs/promises'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import * as React from 'react'; +import { render } from "@react-email/components"; +import { copyFile, mkdir, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import * as React from "react"; -import ConfirmEmail from '../src/ConfirmEmail'; -import DocumentExport from '../src/DocumentExport'; -import DocumentSigning from '../src/DocumentSigning'; -import FrameworkExport from '../src/FrameworkExport'; -import Invitation from '../src/Invitation'; -import PasswordReset from '../src/PasswordReset'; -import TrustCenterAccess from '../src/TrustCenterAccess'; -import TrustCenterDocumentAccessRejected from '../src/TrustCenterDocumentAccessRejected'; +import ConfirmEmail from "../src/ConfirmEmail"; +import DocumentExport from "../src/DocumentExport"; +import DocumentSigning from "../src/DocumentSigning"; +import FrameworkExport from "../src/FrameworkExport"; +import Invitation from "../src/Invitation"; +import PasswordReset from "../src/PasswordReset"; +import TrustCenterAccess from "../src/TrustCenterAccess"; +import TrustCenterDocumentAccessRejected from "../src/TrustCenterDocumentAccessRejected"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -23,42 +23,46 @@ type TemplateConfig = { const templates: TemplateConfig[] = [ { - name: 'confirm-email', - render: () => ConfirmEmail() + name: "confirm-email", + render: () => ConfirmEmail(), }, { - name: 'password-reset', - render: () => PasswordReset() + name: "password-reset", + render: () => PasswordReset(), }, { - name: 'invitation', - render: () => Invitation() + name: "invitation", + render: () => Invitation(), }, { - name: 'document-signing', - render: () => DocumentSigning() + name: "document-signing", + render: () => DocumentSigning(), }, { - name: 'document-export', - render: () => DocumentExport() + name: "document-export", + render: () => DocumentExport(), }, { - name: 'framework-export', - render: () => FrameworkExport() + name: "framework-export", + render: () => FrameworkExport(), }, { - name: 'trust-center-access', - render: () => TrustCenterAccess() + name: "trust-center-access", + render: () => TrustCenterAccess(), }, { - name: 'trust-center-document-access-rejected', - render: () => TrustCenterDocumentAccessRejected() + name: "trust-center-document-access-rejected", + render: () => TrustCenterDocumentAccessRejected(), + }, + { + name: "magic-link", + render: () => TrustCenterAccess(), }, ]; async function build() { - const outputDir = join(__dirname, '..', 'dist'); - const templatesDir = join(__dirname, '..', 'templates'); + const outputDir = join(__dirname, "..", "dist"); + const templatesDir = join(__dirname, "..", "templates"); await mkdir(outputDir, { recursive: true }); for (const template of templates) { @@ -74,6 +78,6 @@ async function build() { } build().catch((err) => { - console.error('Failed to build email templates:', err); + console.error("Failed to build email templates:", err); process.exit(1); }); diff --git a/packages/emails/src/MagicLink.tsx b/packages/emails/src/MagicLink.tsx new file mode 100644 index 000000000..6e45e70d1 --- /dev/null +++ b/packages/emails/src/MagicLink.tsx @@ -0,0 +1,28 @@ +import { Button, Section, Text } from "@react-email/components"; +import * as React from "react"; +import EmailLayout, { + bodyText, + button, + buttonContainer, + footerText, +} from "./components/EmailLayout"; + +export const MagicLink = () => { + return ( + + Please use this link to connect to Probo: + +
+ +
+ + + This link will expire in {"{{.DurationInMinutes}}"} minutes. + +
+ ); +}; + +export default MagicLink; diff --git a/packages/emails/src/TrustCenterAccess.tsx b/packages/emails/src/TrustCenterAccess.tsx index 02c392a0d..1a1f9ec77 100644 --- a/packages/emails/src/TrustCenterAccess.tsx +++ b/packages/emails/src/TrustCenterAccess.tsx @@ -1,22 +1,31 @@ -import { Button, Section, Text } from '@react-email/components'; -import * as React from 'react'; -import EmailLayout, { bodyText, button, buttonContainer, footerText } from './components/EmailLayout'; +import { Button, Section, Text } from "@react-email/components"; +import * as React from "react"; +import EmailLayout, { + bodyText, + button, + buttonContainer, + footerText, +} from "./components/EmailLayout"; export const TrustCenterAccess = () => { return ( - + - You have been granted access to {'{{.OrganizationName}}'}'s Trust Center! Click the button below to access it: + You have been granted access to{" "} + {"{{.OrganizationName}}"}'s Trust Center! Click the + button below to access it:
-
- This link will expire in {'{{.DurationInDays}}'} days. + This link will expire in {"{{.DurationInDays}}"} days.
); diff --git a/packages/emails/templates/magic-link.txt b/packages/emails/templates/magic-link.txt new file mode 100644 index 000000000..f55d29df1 --- /dev/null +++ b/packages/emails/templates/magic-link.txt @@ -0,0 +1,11 @@ +Probo + +Hi {{.FullName}}, + +Please use this link to connect to Probo: + +{{.MagicLinkURL}} + +This link will expire in {{.DurationInMinutes}} minutes. + +Probo Inc, 490 Post St, STE 640, San Francisco, CA, 94102, US diff --git a/packages/prettier/prettier.config.js b/packages/prettier/prettier.config.js index 3e650e73c..43caab550 100644 --- a/packages/prettier/prettier.config.js +++ b/packages/prettier/prettier.config.js @@ -1,3 +1,3 @@ module.exports = { - tabWidth: 2, + tabWidth: 2, }; diff --git a/pkg/iam/auth_service.go b/pkg/iam/auth_service.go index 7cdbe5d40..d6c08607a 100644 --- a/pkg/iam/auth_service.go +++ b/pkg/iam/auth_service.go @@ -16,6 +16,7 @@ package iam import ( "context" + "errors" "fmt" "time" @@ -50,6 +51,11 @@ type ( FullName string } + LoadOrCreateIdentityRequest struct { + Email mail.Addr + FullName string + } + CreateIdentityWithPasswordRequest struct { Email mail.Addr Password string @@ -59,11 +65,16 @@ type ( PasswordResetData struct { Email mail.Addr `json:"email"` } + + MagicLinkData struct { + Email mail.Addr `json:"email"` + } ) const ( TokenTypeOrganizationInvitation = "organization_invitation" TokenTypePasswordReset = "password_reset" + TokenTypeMagicLink = "magic_link" ) func NewAuthService(svc *Service) *AuthService { @@ -98,6 +109,14 @@ func (req ChangePasswordRequest) Validate() error { return v.Error() } +func (req LoadOrCreateIdentityRequest) Validate() error { + v := validator.New() + + v.Check(req.FullName, "fullName", validator.NotEmpty(), validator.MinLen(1), validator.MaxLen(255)) + + return v.Error() +} + func (req CreateIdentityWithPasswordRequest) Validate() error { v := validator.New() @@ -300,6 +319,51 @@ func (s AuthService) SendPasswordResetInstructionByEmail( ) } +func (s AuthService) LoadOrCreateIdentity( + ctx context.Context, + req *LoadOrCreateIdentityRequest, +) (*coredata.Identity, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("invalid request: %w", err) + } + + var ( + identity *coredata.Identity + now = time.Now() + ) + + if err := s.pg.WithTx(ctx, func(tx pg.Conn) error { + identity = &coredata.Identity{} + + if err := identity.LoadByEmail(ctx, tx, req.Email); err != nil { + if !errors.Is(err, coredata.ErrResourceNotFound) { + return fmt.Errorf("cannot load identity: %w", err) + } + + identity = &coredata.Identity{ + ID: gid.New(gid.NilTenant, coredata.IdentityEntityType), + EmailAddress: req.Email, + FullName: req.FullName, + HashedPassword: nil, + EmailAddressVerified: false, + CreatedAt: now, + UpdatedAt: now, + } + + err = identity.Insert(ctx, tx) + if err != nil { + return fmt.Errorf("cannot insert identity: %w", err) + } + } + + return nil + }); err != nil { + return nil, err + } + + return identity, nil +} + func (s AuthService) CreateIdentityWithPassword( ctx context.Context, req *CreateIdentityWithPasswordRequest, @@ -476,3 +540,103 @@ func (s AuthService) OpenSessionWithPassword(ctx context.Context, email mail.Add return identity, session, err } + +func (s AuthService) SendMagicLink(ctx context.Context, email mail.Addr) error { + token, err := statelesstoken.NewToken( + s.tokenSecret, + TokenTypeMagicLink, + s.magicLinkTokenValidity, + MagicLinkData{ + Email: email, + }, + ) + if err != nil { + return fmt.Errorf("cannot generate magic link token: %w", err) + } + + base, err := baseurl.Parse(s.baseURL) + if err != nil { + return fmt.Errorf("cannot parse base URL: %w", err) + } + + magicLinkURL := base. + WithPath("/auth/magic-link"). + WithQuery("token", token). + MustString() + + return s.pg.WithTx( + ctx, + func(tx pg.Conn) error { + fullName := email.Username() + identity := &coredata.Identity{} + + err := identity.LoadByEmail(ctx, tx, email) + if err == nil { + fullName = identity.FullName + } else { + if !errors.Is(err, coredata.ErrResourceNotFound) { + return fmt.Errorf("cannot load identity: %w", err) + } + } + + subject, textBody, htmlBody, err := emails.RenderMagicLink( + s.baseURL, + fullName, + magicLinkURL, + s.invitationTokenValidity, + ) + if err != nil { + return fmt.Errorf("cannot render magic link email: %w", err) + } + + magicLinkEmail := coredata.NewEmail( + fullName, + email, + subject, + textBody, + htmlBody, + ) + + err = magicLinkEmail.Insert(ctx, tx) + if err != nil { + return fmt.Errorf("cannot insert email: %w", err) + } + + return nil + }, + ) +} + +func (s AuthService) OpenSessionWithMagicLink(ctx context.Context, token string) (*coredata.Identity, *coredata.Session, error) { + var ( + identity = &coredata.Identity{} + session = &coredata.Session{} + ) + + payload, err := statelesstoken.ValidateToken[MagicLinkData](s.tokenSecret, TokenTypeMagicLink, token) + if err != nil { + return nil, nil, NewInvalidTokenError() + } + + if err := s.pg.WithTx( + ctx, + func(conn pg.Conn) error { + err := identity.LoadByEmail(ctx, conn, payload.Data.Email) + if err != nil { + return fmt.Errorf("cannot load identity by email: %w", err) + } + + session = coredata.NewRootSession(identity.ID, coredata.AuthMethodPassword, s.sessionDuration) + err = session.Insert(ctx, conn) + if err != nil { + return fmt.Errorf("cannot insert session: %w", err) + } + + return nil + }, + ); err != nil { + return nil, nil, err + } + + return identity, session, err +} diff --git a/pkg/iam/service.go b/pkg/iam/service.go index 8ae4e389d..ba4b9b3be 100644 --- a/pkg/iam/service.go +++ b/pkg/iam/service.go @@ -31,6 +31,7 @@ type ( disableSignup bool invitationTokenValidity time.Duration passwordResetTokenValidity time.Duration + magicLinkTokenValidity time.Duration sessionDuration time.Duration bucket string certificate *x509.Certificate @@ -53,6 +54,7 @@ type ( DisableSignup bool InvitationTokenValidity time.Duration PasswordResetTokenValidity time.Duration + MagicLinkTokenValidity time.Duration SessionDuration time.Duration Bucket string TokenSecret string @@ -99,6 +101,7 @@ func NewService( disableSignup: cfg.DisableSignup, invitationTokenValidity: cfg.InvitationTokenValidity, passwordResetTokenValidity: cfg.PasswordResetTokenValidity, + magicLinkTokenValidity: cfg.MagicLinkTokenValidity, sessionDuration: cfg.SessionDuration, bucket: cfg.Bucket, certificate: cfg.Certificate, diff --git a/pkg/mail/addr.go b/pkg/mail/addr.go index 06e6b3766..f6e051e4b 100644 --- a/pkg/mail/addr.go +++ b/pkg/mail/addr.go @@ -16,6 +16,19 @@ func (a Addr) String() string { return string(a) } +func (a *Addr) Username() string { + if a == nil || *a == Nil { + return "" + } + + parts := strings.Split(a.String(), "@") + if len(parts) != 2 { + return "" + } + + return parts[0] +} + func (a *Addr) Domain() string { if a == nil || *a == Nil { return "" diff --git a/pkg/probo/service.go b/pkg/probo/service.go index f46f34b42..f01463e81 100644 --- a/pkg/probo/service.go +++ b/pkg/probo/service.go @@ -468,3 +468,25 @@ func (s *Service) LoadTrustCenterByID(ctx context.Context, id gid.GID) (*TrustCe return &info, err } + +func (s *Service) LoadTrustCenterByOrganizationID(ctx context.Context, organizationID gid.GID) (*TrustCenterInfo, error) { + var info TrustCenterInfo + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + scope := coredata.NewScope(organizationID.TenantID()) + var trustCenter coredata.TrustCenter + if err := trustCenter.LoadByOrganizationID(ctx, conn, scope, organizationID); err != nil { + return fmt.Errorf("cannot load trust center: %w", err) + } + + info.ID = trustCenter.ID + info.OrganizationID = trustCenter.OrganizationID + + return nil + }, + ) + + return &info, err +} diff --git a/pkg/probo/trust_center_access_service.go b/pkg/probo/trust_center_access_service.go index d78f4cd0b..0a54e3476 100644 --- a/pkg/probo/trust_center_access_service.go +++ b/pkg/probo/trust_center_access_service.go @@ -40,7 +40,7 @@ type ( CreateTrustCenterAccessRequest struct { TrustCenterID gid.GID Email mail.Addr - Name string + FullName string } UpdateTrustCenterDocumentAccessRequest struct { @@ -69,7 +69,7 @@ func (ctcar *CreateTrustCenterAccessRequest) Validate() error { v.Check(ctcar.TrustCenterID, "trust_center_id", validator.Required(), validator.GID(coredata.TrustCenterEntityType)) v.Check(ctcar.Email, "email", validator.Required(), validator.NotEmpty()) v.Check(ctcar.Email.Domain(), "email", validator.NotBlacklisted()) - v.Check(ctcar.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength)) + v.Check(ctcar.FullName, "name", validator.SafeTextNoNewLine(TitleMaxLength)) return v.Error() } @@ -244,7 +244,7 @@ func (s TrustCenterAccessService) Create( TenantID: s.svc.scope.GetTenantID(), TrustCenterID: req.TrustCenterID, Email: req.Email, - Name: req.Name, + Name: req.FullName, Active: false, HasAcceptedNonDisclosureAgreement: false, CreatedAt: now, diff --git a/pkg/probod/auth_config.go b/pkg/probod/auth_config.go index c60ff9c59..8ef68cadb 100644 --- a/pkg/probod/auth_config.go +++ b/pkg/probod/auth_config.go @@ -26,6 +26,7 @@ type ( DisableSignup bool `json:"disable-signup"` InvitationConfirmationTokenValidity int `json:"invitation-confirmation-token-validity"` PasswordResetTokenValidity int `json:"password-reset-token-validity"` + MagicLinkTokenValidity int `json:"magic-link-token-validity"` SAML samlConfig `json:"saml"` } diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go index 3267c4517..224aa82fb 100644 --- a/pkg/probod/probod.go +++ b/pkg/probod/probod.go @@ -125,6 +125,7 @@ func New() *Implm { DisableSignup: false, InvitationConfirmationTokenValidity: 3600, PasswordResetTokenValidity: 3600, + MagicLinkTokenValidity: 3600, SAML: samlConfig{ SessionDuration: 604800, CleanupIntervalSeconds: 86400, @@ -318,6 +319,7 @@ func (impl *Implm) Run( DisableSignup: impl.cfg.Auth.DisableSignup, InvitationTokenValidity: time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity) * time.Second, PasswordResetTokenValidity: time.Duration(impl.cfg.Auth.PasswordResetTokenValidity) * time.Second, + MagicLinkTokenValidity: time.Duration(impl.cfg.Auth.MagicLinkTokenValidity) * time.Second, SessionDuration: time.Duration(impl.cfg.Auth.Cookie.Duration) * time.Hour, Bucket: impl.cfg.AWS.Bucket, TokenSecret: impl.cfg.Auth.Cookie.Secret, diff --git a/pkg/server/api/authn/context.go b/pkg/server/api/authn/context.go index 54731fb92..0ea9596e5 100644 --- a/pkg/server/api/authn/context.go +++ b/pkg/server/api/authn/context.go @@ -28,6 +28,7 @@ var ( identityContextKey = &ctxKey{name: "identity"} sessionContextKey = &ctxKey{name: "session"} apiKeyContextKey = &ctxKey{name: "api_key"} + TrustCenterKey = &ctxKey{name: "trust_center"} ) func SessionFromContext(ctx context.Context) *coredata.Session { diff --git a/pkg/server/api/connect/v1/v1_resolver.go b/pkg/server/api/connect/v1/v1_resolver.go index b034a5a27..1ec34a145 100644 --- a/pkg/server/api/connect/v1/v1_resolver.go +++ b/pkg/server/api/connect/v1/v1_resolver.go @@ -359,11 +359,11 @@ func (r *membershipProfileResolver) Permission(ctx context.Context, obj *types.M func (r *mutationResolver) SignIn(ctx context.Context, input types.SignInInput) (*types.SignInPayload, error) { // TODO: handle existing session to only open child session and chnage root session auth method to PASSWORD - user, session, err := r.iam.AuthService.OpenSessionWithPassword(ctx, input.Email, input.Password) + identity, session, err := r.iam.AuthService.OpenSessionWithPassword(ctx, input.Email, input.Password) if err != nil { var errInvalidPassword *iam.ErrInvalidPassword if errors.As(err, &errInvalidPassword) { - return nil, graphql.ErrorOnPath(ctx, err) + return nil, gqlutils.Invalid(ctx, err) } var errInvalidCredentials *iam.ErrInvalidCredentials @@ -384,7 +384,7 @@ func (r *mutationResolver) SignIn(ctx context.Context, input types.SignInInput) r.sessionCookie.Set(w, session) return &types.SignInPayload{ - Identity: types.NewIdentity(user), + Identity: types.NewIdentity(identity), Session: types.NewSession(session), }, nil } diff --git a/pkg/server/api/console/v1/graphql_handler.go b/pkg/server/api/console/v1/graphql_handler.go index 53242be45..f5d15c29e 100644 --- a/pkg/server/api/console/v1/graphql_handler.go +++ b/pkg/server/api/console/v1/graphql_handler.go @@ -32,6 +32,7 @@ func NewGraphQLHandler(iamSvc *iam.Service, proboSvc *probo.Service, customDomai probo: proboSvc, iam: iamSvc, customDomainCname: customDomainCname, + logger: logger, }, } diff --git a/pkg/server/api/console/v1/resolver.go b/pkg/server/api/console/v1/resolver.go index f1e9906ce..bfd0463e0 100644 --- a/pkg/server/api/console/v1/resolver.go +++ b/pkg/server/api/console/v1/resolver.go @@ -46,6 +46,7 @@ type ( authorize authz.AuthorizeFunc probo *probo.Service iam *iam.Service + logger *log.Logger customDomainCname string } ) @@ -97,7 +98,10 @@ func NewMux( } w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(requests) + if err := json.NewEncoder(w).Encode(requests); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } }, ) @@ -151,7 +155,7 @@ func NewMux( w.Header().Set("Content-Type", "application/pdf") w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s.pdf\"", uuid.String())) w.WriteHeader(http.StatusOK) - w.Write(pdfData) + _, _ = w.Write(pdfData) }, ) diff --git a/pkg/server/api/console/v1/types/audit.go b/pkg/server/api/console/v1/types/audit.go index 7259ae75a..d89e7b9fe 100644 --- a/pkg/server/api/console/v1/types/audit.go +++ b/pkg/server/api/console/v1/types/audit.go @@ -60,7 +60,7 @@ func NewAuditEdge(a *coredata.Audit, orderField coredata.AuditOrderField) *Audit } func NewAudit(a *coredata.Audit) *Audit { - return &Audit{ + node := &Audit{ ID: a.ID, Organization: &Organization{ ID: a.OrganizationID, @@ -76,4 +76,12 @@ func NewAudit(a *coredata.Audit) *Audit { CreatedAt: a.CreatedAt, UpdatedAt: a.UpdatedAt, } + + if a.ReportID != nil { + node.Report = &Report{ + ID: *a.ReportID, + } + } + + return node } diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 4452ca20d..a851f3e23 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -14,6 +14,7 @@ import ( "time" pgx "github.com/jackc/pgx/v5" + "go.gearno.de/kit/log" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/iam" @@ -1674,12 +1675,28 @@ func (r *mutationResolver) CreateTrustCenterAccess(ctx context.Context, input ty prb := r.ProboService(ctx, input.TrustCenterID.TenantID()) + // TODO: when admin/owner creates trust center access, we should have an invite for it instead of directly creating the identity + identity := authn.IdentityFromContext(ctx) + if identity == nil { + var err error + identity, err = r.iam.AuthService.LoadOrCreateIdentity( + ctx, + &iam.LoadOrCreateIdentityRequest{ + Email: input.Email, + }, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot load or create identity", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + } + access, err := prb.TrustCenterAccesses.Create( ctx, &probo.CreateTrustCenterAccessRequest{ TrustCenterID: input.TrustCenterID, - Email: input.Email, - Name: input.Name, + Email: identity.EmailAddress, + FullName: identity.FullName, }, ) if err != nil { @@ -1687,8 +1704,8 @@ func (r *mutationResolver) CreateTrustCenterAccess(ctx context.Context, input ty return nil, gqlutils.Conflict(ctx, err) } - // TODO no panic use gqlutils.InternalError - panic(fmt.Errorf("cannot create trust center access: %w", err)) + r.logger.ErrorCtx(ctx, "cannot create trust center access", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return &types.CreateTrustCenterAccessPayload{ diff --git a/pkg/server/api/trust/v1/resolver.go b/pkg/server/api/trust/v1/resolver.go index 22f5c3be2..fd07a6b7b 100644 --- a/pkg/server/api/trust/v1/resolver.go +++ b/pkg/server/api/trust/v1/resolver.go @@ -20,13 +20,16 @@ import ( "context" "time" + "github.com/99designs/gqlgen/graphql" "github.com/go-chi/chi/v5" "go.gearno.de/kit/log" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/iam" + "go.probo.inc/probo/pkg/probo" "go.probo.inc/probo/pkg/securecookie" "go.probo.inc/probo/pkg/server/api/authn" "go.probo.inc/probo/pkg/server/api/trust/v1/schema" + "go.probo.inc/probo/pkg/server/api/trust/v1/types" "go.probo.inc/probo/pkg/server/gqlutils" "go.probo.inc/probo/pkg/trust" ) @@ -45,10 +48,28 @@ type ( } Resolver struct { - trust *trust.Service + trust *trust.Service + logger *log.Logger + iam *iam.Service + sessionCookie *authn.Cookie } ) +type ctxKey struct{ name string } + +var ( + TrustCenterKey = &ctxKey{name: "trust_center"} +) + +func TrustCenterFromContext(ctx context.Context) probo.TrustCenterInfo { + trustCenter, _ := ctx.Value(TrustCenterKey).(probo.TrustCenterInfo) + return trustCenter +} + +func ContextWithTrustCenter(ctx context.Context, trustCenter probo.TrustCenterInfo) context.Context { + return context.WithValue(ctx, TrustCenterKey, trustCenter) +} + func NewMux( logger *log.Logger, iamSvc *iam.Service, @@ -60,7 +81,25 @@ func NewMux( sessionMiddleware := authn.NewSessionMiddleware(iamSvc, cookieConfig) r.Use(sessionMiddleware) - config := schema.Config{Resolvers: &Resolver{trust: trustSvc}} + config := schema.Config{ + Resolvers: &Resolver{ + iam: iamSvc, + trust: trustSvc, + logger: logger, + sessionCookie: authn.NewCookie(&cookieConfig), + }, + Directives: schema.DirectiveRoot{ + MustBeAuthenticated: func(ctx context.Context, obj any, next graphql.Resolver, role *types.Role) (any, error) { + identity := authn.IdentityFromContext(ctx) + + if identity == nil { + return nil, gqlutils.Unauthenticatedf(ctx, "authentication required") + } + + return next(ctx) + }, + }, + } es := schema.NewExecutableSchema(config) graphqlHandler := gqlutils.NewHandler(es, logger) @@ -73,14 +112,6 @@ func (r *Resolver) RootTrustService(ctx context.Context) *trust.TenantService { return r.trust.WithTenant(gid.NewTenantID()) } -func (r *Resolver) PublicTrustService(ctx context.Context, tenantID gid.TenantID) *trust.TenantService { +func (r *Resolver) TrustService(ctx context.Context, tenantID gid.TenantID) *trust.TenantService { return r.trust.WithTenant(tenantID) } - -func (r *Resolver) PrivateTrustService(ctx context.Context, tenantID gid.TenantID) (*trust.TenantService, error) { - // if err := trustauth.ValidateTenantAccess(ctx, r, userTenantContextKey, tenantID); err != nil { - // return nil, fmt.Errorf("cannot access trust center: %w", err) - // } - - return r.trust.WithTenant(tenantID), nil -} diff --git a/pkg/server/api/trust/v1/schema.graphql b/pkg/server/api/trust/v1/schema.graphql index 6cbe275b5..713ade4a5 100644 --- a/pkg/server/api/trust/v1/schema.graphql +++ b/pkg/server/api/trust/v1/schema.graphql @@ -34,6 +34,15 @@ type PageInfo { endCursor: CursorKey } +type Identity implements Node { + id: ID! + email: EmailAddr! + fullName: String! + emailVerified: Boolean! + createdAt: Datetime! + updatedAt: Datetime! +} + type Organization implements Node { id: ID! name: String! @@ -535,10 +544,18 @@ type TrustCenterAccess implements Node { updatedAt: Datetime! } +input SignInWithTokenInput { + token: String! +} + +type SignInWithTokenPayload { + success: Boolean! +} + input RequestAllAccessesInput { trustCenterId: ID! - email: EmailAddr - name: String + email: EmailAddr! + fullName: String! } type RequestAccessesPayload { @@ -560,22 +577,22 @@ input AcceptNonDisclosureAgreementInput { input RequestDocumentAccessInput { trustCenterId: ID! documentId: ID! - email: EmailAddr - name: String + email: EmailAddr! + fullName: String! } input RequestReportAccessInput { trustCenterId: ID! reportId: ID! - email: EmailAddr - name: String + email: EmailAddr! + fullName: String! } input RequestTrustCenterFileAccessInput { trustCenterId: ID! trustCenterFileId: ID! - email: EmailAddr - name: String + email: EmailAddr! + fullName: String! } input ExportTrustCenterFileInput { @@ -599,12 +616,15 @@ type AcceptNonDisclosureAgreementPayload { } type Query { + viewer: Identity node(id: ID!): Node! trustCenterBySlug(slug: String!): TrustCenter @mustBeAuthenticated(role: NONE) currentTrustCenter: TrustCenter @mustBeAuthenticated(role: NONE) } type Mutation { + signInWithToken(input: SignInWithTokenInput!): SignInWithTokenPayload! + requestAllAccesses(input: RequestAllAccessesInput!): RequestAccessesPayload! @mustBeAuthenticated(role: NONE) diff --git a/pkg/server/api/trust/v1/schema/schema.go b/pkg/server/api/trust/v1/schema/schema.go index f703d2831..630eb48b1 100644 --- a/pkg/server/api/trust/v1/schema/schema.go +++ b/pkg/server/api/trust/v1/schema/schema.go @@ -120,6 +120,15 @@ type ComplexityRoot struct { Name func(childComplexity int) int } + Identity struct { + CreatedAt func(childComplexity int) int + Email func(childComplexity int) int + EmailVerified func(childComplexity int) int + FullName func(childComplexity int) int + ID func(childComplexity int) int + UpdatedAt func(childComplexity int) int + } + Mutation struct { AcceptNonDisclosureAgreement func(childComplexity int, input types.AcceptNonDisclosureAgreementInput) int ExportDocumentPDF func(childComplexity int, input types.ExportDocumentPDFInput) int @@ -129,6 +138,7 @@ type ComplexityRoot struct { RequestDocumentAccess func(childComplexity int, input types.RequestDocumentAccessInput) int RequestReportAccess func(childComplexity int, input types.RequestReportAccessInput) int RequestTrustCenterFileAccess func(childComplexity int, input types.RequestTrustCenterFileAccessInput) int + SignInWithToken func(childComplexity int, input types.SignInWithTokenInput) int } Organization struct { @@ -152,6 +162,7 @@ type ComplexityRoot struct { CurrentTrustCenter func(childComplexity int) int Node func(childComplexity int, id gid.GID) int TrustCenterBySlug func(childComplexity int, slug string) int + Viewer func(childComplexity int) int } Report struct { @@ -165,6 +176,10 @@ type ComplexityRoot struct { TrustCenterAccess func(childComplexity int) int } + SignInWithTokenPayload struct { + Success func(childComplexity int) int + } + TrustCenter struct { Active func(childComplexity int) int Audits func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int @@ -258,6 +273,7 @@ type FrameworkResolver interface { DarkLogoURL(ctx context.Context, obj *types.Framework) (*string, error) } type MutationResolver interface { + SignInWithToken(ctx context.Context, input types.SignInWithTokenInput) (*types.SignInWithTokenPayload, error) RequestAllAccesses(ctx context.Context, input types.RequestAllAccessesInput) (*types.RequestAccessesPayload, error) ExportDocumentPDF(ctx context.Context, input types.ExportDocumentPDFInput) (*types.ExportDocumentPDFPayload, error) ExportReportPDF(ctx context.Context, input types.ExportReportPDFInput) (*types.ExportReportPDFPayload, error) @@ -271,6 +287,7 @@ type OrganizationResolver interface { LogoURL(ctx context.Context, obj *types.Organization) (*string, error) } type QueryResolver interface { + Viewer(ctx context.Context) (*types.Identity, error) Node(ctx context.Context, id gid.GID) (types.Node, error) TrustCenterBySlug(ctx context.Context, slug string) (*types.TrustCenter, error) CurrentTrustCenter(ctx context.Context) (*types.TrustCenter, error) @@ -472,6 +489,43 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Framework.Name(childComplexity), true + case "Identity.createdAt": + if e.complexity.Identity.CreatedAt == nil { + break + } + + return e.complexity.Identity.CreatedAt(childComplexity), true + case "Identity.email": + if e.complexity.Identity.Email == nil { + break + } + + return e.complexity.Identity.Email(childComplexity), true + case "Identity.emailVerified": + if e.complexity.Identity.EmailVerified == nil { + break + } + + return e.complexity.Identity.EmailVerified(childComplexity), true + case "Identity.fullName": + if e.complexity.Identity.FullName == nil { + break + } + + return e.complexity.Identity.FullName(childComplexity), true + case "Identity.id": + if e.complexity.Identity.ID == nil { + break + } + + return e.complexity.Identity.ID(childComplexity), true + case "Identity.updatedAt": + if e.complexity.Identity.UpdatedAt == nil { + break + } + + return e.complexity.Identity.UpdatedAt(childComplexity), true + case "Mutation.acceptNonDisclosureAgreement": if e.complexity.Mutation.AcceptNonDisclosureAgreement == nil { break @@ -560,6 +614,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Mutation.RequestTrustCenterFileAccess(childComplexity, args["input"].(types.RequestTrustCenterFileAccessInput)), true + case "Mutation.signInWithToken": + if e.complexity.Mutation.SignInWithToken == nil { + break + } + + args, err := ec.field_Mutation_signInWithToken_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.SignInWithToken(childComplexity, args["input"].(types.SignInWithTokenInput)), true case "Organization.description": if e.complexity.Organization.Description == nil { @@ -657,6 +722,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Query.TrustCenterBySlug(childComplexity, args["slug"].(string)), true + case "Query.viewer": + if e.complexity.Query.Viewer == nil { + break + } + + return e.complexity.Query.Viewer(childComplexity), true case "Report.filename": if e.complexity.Report.Filename == nil { @@ -690,6 +761,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.RequestAccessesPayload.TrustCenterAccess(childComplexity), true + case "SignInWithTokenPayload.success": + if e.complexity.SignInWithTokenPayload.Success == nil { + break + } + + return e.complexity.SignInWithTokenPayload.Success(childComplexity), true + case "TrustCenter.active": if e.complexity.TrustCenter.Active == nil { break @@ -1018,6 +1096,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputRequestDocumentAccessInput, ec.unmarshalInputRequestReportAccessInput, ec.unmarshalInputRequestTrustCenterFileAccessInput, + ec.unmarshalInputSignInWithTokenInput, ) first := true @@ -1151,6 +1230,15 @@ type PageInfo { endCursor: CursorKey } +type Identity implements Node { + id: ID! + email: EmailAddr! + fullName: String! + emailVerified: Boolean! + createdAt: Datetime! + updatedAt: Datetime! +} + type Organization implements Node { id: ID! name: String! @@ -1652,10 +1740,18 @@ type TrustCenterAccess implements Node { updatedAt: Datetime! } +input SignInWithTokenInput { + token: String! +} + +type SignInWithTokenPayload { + success: Boolean! +} + input RequestAllAccessesInput { trustCenterId: ID! - email: EmailAddr - name: String + email: EmailAddr! + fullName: String! } type RequestAccessesPayload { @@ -1677,22 +1773,22 @@ input AcceptNonDisclosureAgreementInput { input RequestDocumentAccessInput { trustCenterId: ID! documentId: ID! - email: EmailAddr - name: String + email: EmailAddr! + fullName: String! } input RequestReportAccessInput { trustCenterId: ID! reportId: ID! - email: EmailAddr - name: String + email: EmailAddr! + fullName: String! } input RequestTrustCenterFileAccessInput { trustCenterId: ID! trustCenterFileId: ID! - email: EmailAddr - name: String + email: EmailAddr! + fullName: String! } input ExportTrustCenterFileInput { @@ -1716,12 +1812,15 @@ type AcceptNonDisclosureAgreementPayload { } type Query { + viewer: Identity node(id: ID!): Node! trustCenterBySlug(slug: String!): TrustCenter @mustBeAuthenticated(role: NONE) currentTrustCenter: TrustCenter @mustBeAuthenticated(role: NONE) } type Mutation { + signInWithToken(input: SignInWithTokenInput!): SignInWithTokenPayload! + requestAllAccesses(input: RequestAllAccessesInput!): RequestAccessesPayload! @mustBeAuthenticated(role: NONE) @@ -1858,6 +1957,17 @@ func (ec *executionContext) field_Mutation_requestTrustCenterFileAccess_args(ctx return args, nil } +func (ec *executionContext) field_Mutation_signInWithToken_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.unmarshalNSignInWithTokenInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSignInWithTokenInput) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -2841,6 +2951,225 @@ func (ec *executionContext) fieldContext_Framework_darkLogoURL(_ context.Context return fc, nil } +func (ec *executionContext) _Identity_id(ctx context.Context, field graphql.CollectedField, obj *types.Identity) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Identity_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_Identity_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Identity", + 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) _Identity_email(ctx context.Context, field graphql.CollectedField, obj *types.Identity) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Identity_email, + func(ctx context.Context) (any, error) { + return obj.Email, nil + }, + nil, + ec.marshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Identity_email(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Identity", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type EmailAddr does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Identity_fullName(ctx context.Context, field graphql.CollectedField, obj *types.Identity) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Identity_fullName, + func(ctx context.Context) (any, error) { + return obj.FullName, nil + }, + nil, + ec.marshalNString2string, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Identity_fullName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Identity", + 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) _Identity_emailVerified(ctx context.Context, field graphql.CollectedField, obj *types.Identity) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Identity_emailVerified, + func(ctx context.Context) (any, error) { + return obj.EmailVerified, nil + }, + nil, + ec.marshalNBoolean2bool, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Identity_emailVerified(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Identity", + 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) _Identity_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Identity) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Identity_createdAt, + func(ctx context.Context) (any, error) { + return obj.CreatedAt, nil + }, + nil, + ec.marshalNDatetime2timeᚐTime, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Identity_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Identity", + 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) _Identity_updatedAt(ctx context.Context, field graphql.CollectedField, obj *types.Identity) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Identity_updatedAt, + func(ctx context.Context) (any, error) { + return obj.UpdatedAt, nil + }, + nil, + ec.marshalNDatetime2timeᚐTime, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Identity_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Identity", + 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) _Mutation_signInWithToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Mutation_signInWithToken, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.Mutation().SignInWithToken(ctx, fc.Args["input"].(types.SignInWithTokenInput)) + }, + nil, + ec.marshalNSignInWithTokenPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSignInWithTokenPayload, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_Mutation_signInWithToken(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_SignInWithTokenPayload_success(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type SignInWithTokenPayload", 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_signInWithToken_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_requestAllAccesses(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -3664,6 +3993,49 @@ func (ec *executionContext) fieldContext_PageInfo_endCursor(_ context.Context, f return fc, nil } +func (ec *executionContext) _Query_viewer(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Query_viewer, + func(ctx context.Context) (any, error) { + return ec.resolvers.Query().Viewer(ctx) + }, + nil, + ec.marshalOIdentity2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐIdentity, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_Query_viewer(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + 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_Identity_id(ctx, field) + case "email": + return ec.fieldContext_Identity_email(ctx, field) + case "fullName": + return ec.fieldContext_Identity_fullName(ctx, field) + case "emailVerified": + return ec.fieldContext_Identity_emailVerified(ctx, field) + case "createdAt": + return ec.fieldContext_Identity_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_Identity_updatedAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Identity", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _Query_node(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -4132,6 +4504,35 @@ func (ec *executionContext) fieldContext_RequestAccessesPayload_trustCenterAcces return fc, nil } +func (ec *executionContext) _SignInWithTokenPayload_success(ctx context.Context, field graphql.CollectedField, obj *types.SignInWithTokenPayload) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_SignInWithTokenPayload_success, + func(ctx context.Context) (any, error) { + return obj.Success, nil + }, + nil, + ec.marshalNBoolean2bool, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_SignInWithTokenPayload_success(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "SignInWithTokenPayload", + 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) _TrustCenter_id(ctx context.Context, field graphql.CollectedField, obj *types.TrustCenter) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -7219,7 +7620,7 @@ func (ec *executionContext) unmarshalInputRequestAllAccessesInput(ctx context.Co asMap[k] = v } - fieldsInOrder := [...]string{"trustCenterId", "email", "name"} + fieldsInOrder := [...]string{"trustCenterId", "email", "fullName"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -7235,18 +7636,18 @@ func (ec *executionContext) unmarshalInputRequestAllAccessesInput(ctx context.Co it.TrustCenterID = data case "email": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) - data, err := ec.unmarshalOEmailAddr2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) + data, err := ec.unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) if err != nil { return it, err } it.Email = data - case "name": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) + case "fullName": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName")) + data, err := ec.unmarshalNString2string(ctx, v) if err != nil { return it, err } - it.Name = data + it.FullName = data } } @@ -7260,7 +7661,7 @@ func (ec *executionContext) unmarshalInputRequestDocumentAccessInput(ctx context asMap[k] = v } - fieldsInOrder := [...]string{"trustCenterId", "documentId", "email", "name"} + fieldsInOrder := [...]string{"trustCenterId", "documentId", "email", "fullName"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -7283,18 +7684,18 @@ func (ec *executionContext) unmarshalInputRequestDocumentAccessInput(ctx context it.DocumentID = data case "email": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) - data, err := ec.unmarshalOEmailAddr2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) + data, err := ec.unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) if err != nil { return it, err } it.Email = data - case "name": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) + case "fullName": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName")) + data, err := ec.unmarshalNString2string(ctx, v) if err != nil { return it, err } - it.Name = data + it.FullName = data } } @@ -7308,7 +7709,7 @@ func (ec *executionContext) unmarshalInputRequestReportAccessInput(ctx context.C asMap[k] = v } - fieldsInOrder := [...]string{"trustCenterId", "reportId", "email", "name"} + fieldsInOrder := [...]string{"trustCenterId", "reportId", "email", "fullName"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -7331,18 +7732,18 @@ func (ec *executionContext) unmarshalInputRequestReportAccessInput(ctx context.C it.ReportID = data case "email": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) - data, err := ec.unmarshalOEmailAddr2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) + data, err := ec.unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) if err != nil { return it, err } it.Email = data - case "name": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) + case "fullName": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName")) + data, err := ec.unmarshalNString2string(ctx, v) if err != nil { return it, err } - it.Name = data + it.FullName = data } } @@ -7356,7 +7757,7 @@ func (ec *executionContext) unmarshalInputRequestTrustCenterFileAccessInput(ctx asMap[k] = v } - fieldsInOrder := [...]string{"trustCenterId", "trustCenterFileId", "email", "name"} + fieldsInOrder := [...]string{"trustCenterId", "trustCenterFileId", "email", "fullName"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -7379,18 +7780,45 @@ func (ec *executionContext) unmarshalInputRequestTrustCenterFileAccessInput(ctx it.TrustCenterFileID = data case "email": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) - data, err := ec.unmarshalOEmailAddr2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) + data, err := ec.unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) if err != nil { return it, err } it.Email = data - case "name": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) - data, err := ec.unmarshalOString2ᚖstring(ctx, v) + case "fullName": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName")) + data, err := ec.unmarshalNString2string(ctx, v) if err != nil { return it, err } - it.Name = data + it.FullName = data + } + } + + return it, nil +} + +func (ec *executionContext) unmarshalInputSignInWithTokenInput(ctx context.Context, obj any) (types.SignInWithTokenInput, error) { + var it types.SignInWithTokenInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"token"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "token": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("token")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Token = data } } @@ -7454,6 +7882,13 @@ func (ec *executionContext) _Node(ctx context.Context, sel ast.SelectionSet, obj return graphql.Null } return ec._Organization(ctx, sel, obj) + case types.Identity: + return ec._Identity(ctx, sel, &obj) + case *types.Identity: + if obj == nil { + return graphql.Null + } + return ec._Identity(ctx, sel, obj) case types.Framework: return ec._Framework(ctx, sel, &obj) case *types.Framework: @@ -8155,6 +8590,70 @@ func (ec *executionContext) _Framework(ctx context.Context, sel ast.SelectionSet return out } +var identityImplementors = []string{"Identity", "Node"} + +func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, obj *types.Identity) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, identityImplementors) + + 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("Identity") + case "id": + out.Values[i] = ec._Identity_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "email": + out.Values[i] = ec._Identity_email(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "fullName": + out.Values[i] = ec._Identity_fullName(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "emailVerified": + out.Values[i] = ec._Identity_emailVerified(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "createdAt": + out.Values[i] = ec._Identity_createdAt(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "updatedAt": + out.Values[i] = ec._Identity_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 mutationImplementors = []string{"Mutation"} func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { @@ -8174,6 +8673,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Mutation") + case "signInWithToken": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_signInWithToken(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "requestAllAccesses": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_requestAllAccesses(ctx, field) @@ -8405,6 +8911,25 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Query") + case "viewer": + 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._Query_viewer(ctx, field) + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "node": field := field @@ -8651,6 +9176,45 @@ func (ec *executionContext) _RequestAccessesPayload(ctx context.Context, sel ast return out } +var signInWithTokenPayloadImplementors = []string{"SignInWithTokenPayload"} + +func (ec *executionContext) _SignInWithTokenPayload(ctx context.Context, sel ast.SelectionSet, obj *types.SignInWithTokenPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, signInWithTokenPayloadImplementors) + + 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("SignInWithTokenPayload") + case "success": + out.Values[i] = ec._SignInWithTokenPayload_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 trustCenterImplementors = []string{"TrustCenter", "Node"} func (ec *executionContext) _TrustCenter(ctx context.Context, sel ast.SelectionSet, obj *types.TrustCenter) graphql.Marshaler { @@ -10954,6 +11518,25 @@ func (ec *executionContext) unmarshalNRequestTrustCenterFileAccessInput2goᚗpro return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNSignInWithTokenInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSignInWithTokenInput(ctx context.Context, v any) (types.SignInWithTokenInput, error) { + res, err := ec.unmarshalInputSignInWithTokenInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNSignInWithTokenPayload2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSignInWithTokenPayload(ctx context.Context, sel ast.SelectionSet, v types.SignInWithTokenPayload) graphql.Marshaler { + return ec._SignInWithTokenPayload(ctx, sel, &v) +} + +func (ec *executionContext) marshalNSignInWithTokenPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSignInWithTokenPayload(ctx context.Context, sel ast.SelectionSet, v *types.SignInWithTokenPayload) 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._SignInWithTokenPayload(ctx, sel, v) +} + func (ec *executionContext) unmarshalNString2string(ctx context.Context, v any) (string, error) { res, err := graphql.UnmarshalString(v) return res, graphql.ErrorOnPath(ctx, err) @@ -11583,22 +12166,11 @@ func (ec *executionContext) marshalOCursorKey2ᚖgoᚗproboᚗincᚋproboᚋpkg return res } -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 - } - res, err := mail1.UnmarshalAddrScalar(v) - return &res, graphql.ErrorOnPath(ctx, err) -} - -func (ec *executionContext) marshalOEmailAddr2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx context.Context, sel ast.SelectionSet, v *mail.Addr) graphql.Marshaler { +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 } - _ = sel - _ = ctx - res := mail1.MarshalAddrScalar(*v) - return res + return ec._Identity(ctx, sel, v) } func (ec *executionContext) unmarshalOInt2ᚖint(ctx context.Context, v any) (*int, error) { diff --git a/pkg/server/api/trust/v1/trust_center_access_handler.go b/pkg/server/api/trust/v1/trust_center_access_handler.go deleted file mode 100644 index 5a23668dc..000000000 --- a/pkg/server/api/trust/v1/trust_center_access_handler.go +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package trust_v1 - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "go.gearno.de/kit/httpserver" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/probo" - "go.probo.inc/probo/pkg/statelesstoken" - "go.probo.inc/probo/pkg/trust" -) - -type ctxKey struct { - name string -} - -var ( - CustomDomainOrganizationIDKey = &ctxKey{name: "custom_domain_organization_id"} -) - -func GetCustomDomainOrganizationID(ctx context.Context) (gid.GID, bool) { - organizationID, ok := ctx.Value(CustomDomainOrganizationIDKey).(gid.GID) - return organizationID, ok -} - -type ( - AuthTokenRequest struct { - Token string `json:"token"` - } - - AuthTokenResponse struct { - Success bool `json:"success"` - TrustCenterID string `json:"trust_center_id,omitempty"` - Message string `json:"message,omitempty"` - } -) - -func authTokenHandler(trustSvc *trust.Service, trustAuthCfg TrustAuthConfig) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var req AuthTokenRequest - // Limit request body size to 1KB to prevent DoS attacks - limitedReader := http.MaxBytesReader(w, r.Body, 1024) - if err := json.NewDecoder(limitedReader).Decode(&req); err != nil { - httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err)) - return - } - - if req.Token == "" { - httpserver.RenderJSON(w, http.StatusBadRequest, AuthTokenResponse{ - Success: false, - Message: "Token is required", - }) - return - } - - accessData, err := validateTrustCenterAccessToken(r.Context(), trustSvc, trustAuthCfg, req.Token) - if err != nil { - httpserver.RenderJSON(w, http.StatusUnauthorized, AuthTokenResponse{ - Success: false, - Message: "Invalid or expired token", - }) - return - } - - tokenString, err := statelesstoken.NewToken( - trustAuthCfg.TokenSecret, - trustAuthCfg.TokenType, - trustAuthCfg.TokenDuration, - *accessData, - ) - if err != nil { - httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("cannot create token: %w", err)) - return - } - - // Determine cookie domain: use custom domain if present, otherwise use configured domain - cookieDomain := trustAuthCfg.CookieDomain - if _, ok := GetCustomDomainOrganizationID(r.Context()); ok { - // On custom domain, use the request host - if r.TLS != nil && r.TLS.ServerName != "" { - cookieDomain = r.TLS.ServerName - } - } - - cookie := &http.Cookie{ - Name: trustAuthCfg.CookieName, - Value: tokenString, - Domain: cookieDomain, - Path: "/", - MaxAge: int(trustAuthCfg.CookieDuration / time.Second), - Secure: trustAuthCfg.CookieSecure, - HttpOnly: true, - SameSite: http.SameSiteStrictMode, - } - http.SetCookie(w, cookie) - - httpserver.RenderJSON(w, http.StatusOK, AuthTokenResponse{ - Success: true, - TrustCenterID: accessData.TrustCenterID.String(), - Message: "Authentication successful", - }) - } -} - -func validateTrustCenterAccessToken(ctx context.Context, trustSvc *trust.Service, trustAuthCfg TrustAuthConfig, tokenString string) (*probo.TrustCenterAccessData, error) { - token, err := statelesstoken.ValidateToken[probo.TrustCenterAccessData]( - trustSvc.GetTokenSecret(), - trustAuthCfg.TokenType, - tokenString, - ) - if err != nil { - return nil, fmt.Errorf("cannot validate trust center access token: %w", err) - } - - tenantSvc := trustSvc.WithTenant(token.Data.TrustCenterID.TenantID()) - if err := tenantSvc.TrustCenterAccesses.ValidateToken(ctx, token.Data.TrustCenterID, token.Data.Email); err != nil { - return nil, fmt.Errorf("cannot validate trust center access token: %w", err) - } - - return &token.Data, nil -} diff --git a/pkg/server/api/trust/v1/types/types.go b/pkg/server/api/trust/v1/types/types.go index f63e07d83..543eeac3d 100644 --- a/pkg/server/api/trust/v1/types/types.go +++ b/pkg/server/api/trust/v1/types/types.go @@ -102,6 +102,18 @@ type Framework struct { func (Framework) IsNode() {} func (this Framework) GetID() gid.GID { return this.ID } +type Identity struct { + ID gid.GID `json:"id"` + Email mail.Addr `json:"email"` + FullName string `json:"fullName"` + EmailVerified bool `json:"emailVerified"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func (Identity) IsNode() {} +func (this Identity) GetID() gid.GID { return this.ID } + type Mutation struct { } @@ -143,30 +155,38 @@ type RequestAccessesPayload struct { } type RequestAllAccessesInput struct { - TrustCenterID gid.GID `json:"trustCenterId"` - Email *mail.Addr `json:"email,omitempty"` - Name *string `json:"name,omitempty"` + TrustCenterID gid.GID `json:"trustCenterId"` + Email mail.Addr `json:"email"` + FullName string `json:"fullName"` } type RequestDocumentAccessInput struct { - TrustCenterID gid.GID `json:"trustCenterId"` - DocumentID gid.GID `json:"documentId"` - Email *mail.Addr `json:"email,omitempty"` - Name *string `json:"name,omitempty"` + TrustCenterID gid.GID `json:"trustCenterId"` + DocumentID gid.GID `json:"documentId"` + Email mail.Addr `json:"email"` + FullName string `json:"fullName"` } type RequestReportAccessInput struct { - TrustCenterID gid.GID `json:"trustCenterId"` - ReportID gid.GID `json:"reportId"` - Email *mail.Addr `json:"email,omitempty"` - Name *string `json:"name,omitempty"` + TrustCenterID gid.GID `json:"trustCenterId"` + ReportID gid.GID `json:"reportId"` + Email mail.Addr `json:"email"` + FullName string `json:"fullName"` } type RequestTrustCenterFileAccessInput struct { - TrustCenterID gid.GID `json:"trustCenterId"` - TrustCenterFileID gid.GID `json:"trustCenterFileId"` - Email *mail.Addr `json:"email,omitempty"` - Name *string `json:"name,omitempty"` + TrustCenterID gid.GID `json:"trustCenterId"` + TrustCenterFileID gid.GID `json:"trustCenterFileId"` + Email mail.Addr `json:"email"` + FullName string `json:"fullName"` +} + +type SignInWithTokenInput struct { + Token string `json:"token"` +} + +type SignInWithTokenPayload struct { + Success bool `json:"success"` } type TrustCenter struct { diff --git a/pkg/server/api/trust/v1/v1_resolver.go b/pkg/server/api/trust/v1/v1_resolver.go index c3f7b721a..2e570ef5e 100644 --- a/pkg/server/api/trust/v1/v1_resolver.go +++ b/pkg/server/api/trust/v1/v1_resolver.go @@ -7,29 +7,37 @@ package trust_v1 import ( "context" + "encoding/base64" + "errors" "fmt" "time" - "github.com/vektah/gqlparser/v2/gqlerror" + "go.gearno.de/kit/log" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/page" + "go.probo.inc/probo/pkg/server/api/authn" "go.probo.inc/probo/pkg/server/api/trust/v1/schema" "go.probo.inc/probo/pkg/server/api/trust/v1/types" + "go.probo.inc/probo/pkg/server/gqlutils" + "go.probo.inc/probo/pkg/trust" ) // Framework is the resolver for the framework field. func (r *auditResolver) Framework(ctx context.Context, obj *types.Audit) (*types.Framework, error) { - publicTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) + trustService := r.TrustService(ctx, obj.ID.TenantID()) - audit, err := publicTrustService.Audits.Get(ctx, obj.ID) + audit, err := trustService.Audits.Get(ctx, obj.ID) if err != nil { - panic(fmt.Errorf("cannot load audit: %w", err)) + r.logger.ErrorCtx(ctx, "cannot load audit", log.Error(err)) + return nil, gqlutils.Internal(ctx) } - framework, err := publicTrustService.Frameworks.Get(ctx, audit.FrameworkID) + framework, err := trustService.Frameworks.Get(ctx, audit.FrameworkID) if err != nil { - panic(fmt.Errorf("cannot load framework: %w", err)) + r.logger.ErrorCtx(ctx, "cannot load framework", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return types.NewFramework(framework), nil @@ -37,20 +45,22 @@ func (r *auditResolver) Framework(ctx context.Context, obj *types.Audit) (*types // Report is the resolver for the report field. func (r *auditResolver) Report(ctx context.Context, obj *types.Audit) (*types.Report, error) { - publicTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) + trustService := r.TrustService(ctx, obj.ID.TenantID()) - audit, err := publicTrustService.Audits.Get(ctx, obj.ID) + audit, err := trustService.Audits.Get(ctx, obj.ID) if err != nil { - panic(fmt.Errorf("cannot load audit: %w", err)) + r.logger.ErrorCtx(ctx, "cannot load audit", log.Error(err)) + return nil, gqlutils.Internal(ctx) } if audit.ReportID == nil { return nil, nil } - report, err := publicTrustService.Reports.Get(ctx, *audit.ReportID) + report, err := trustService.Reports.Get(ctx, *audit.ReportID) if err != nil { - panic(fmt.Errorf("cannot load report: %w", err)) + r.logger.ErrorCtx(ctx, "cannot load report", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return types.NewReport(report), nil @@ -58,610 +68,696 @@ func (r *auditResolver) Report(ctx context.Context, obj *types.Audit) (*types.Re // IsUserAuthorized is the resolver for the isUserAuthorized field. func (r *documentResolver) IsUserAuthorized(ctx context.Context, obj *types.Document) (bool, error) { - // publicTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) + trustService := r.TrustService(ctx, obj.ID.TenantID()) - // document, err := publicTrustService.Documents.Get(ctx, obj.ID) - // if err != nil { - // panic(fmt.Errorf("cannot load document: %w", err)) - // } + document, err := trustService.Documents.Get(ctx, obj.ID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot load document", log.Error(err)) + return false, gqlutils.Internal(ctx) + } - // if document.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { - // return true, nil - // } + if document.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { + return true, nil + } - // privateTrustService, err := r.PrivateTrustService(ctx, obj.ID.TenantID()) - // if err != nil { - // return false, nil - // } + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return false, gqlutils.Unauthenticatedf(ctx, "unauthenticated") + } - // userData := connect_v1.IdentityFromContext(ctx) - // if userData != nil { - // return true, nil - // } + trustCenter := TrustCenterFromContext(ctx) + documentAccess, err := trustService.TrustCenterAccesses.LoadDocumentAccess( + ctx, + trustCenter.ID, + identity.EmailAddress, + obj.ID, + ) + if err != nil { + // FIXME check for not found and return without error in this case + // r.logger.ErrorCtx(ctx, "cannot check document access", log.Error(err)) + // return false, gqlutils.Internal(ctx) + return false, nil + } - // tokenData := TokenAccessFromContext(ctx) - // if tokenData != nil { - // documentAccess, err := privateTrustService.TrustCenterAccesses.LoadDocumentAccess(ctx, tokenData.TrustCenterID, tokenData.Email, obj.ID) - // if err != nil { - // return false, nil - // } - - // return documentAccess.Status == coredata.TrustCenterDocumentAccessStatusGranted, nil - // } - - panic(fmt.Errorf("no user or token data found")) + return documentAccess.Status == coredata.TrustCenterDocumentAccessStatusGranted, nil } // HasUserRequestedAccess is the resolver for the hasUserRequestedAccess field. func (r *documentResolver) HasUserRequestedAccess(ctx context.Context, obj *types.Document) (bool, error) { - // privateTrustService, err := r.PrivateTrustService(ctx, obj.ID.TenantID()) - // if err != nil { - // return false, nil - // } + trustService := r.TrustService(ctx, obj.ID.TenantID()) - // userData := r.IdentityFromContext(ctx) - // if userData != nil { - // return false, nil - // } + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return false, nil // User is not authenticated, so no access requested + } - // tokenData := TokenAccessFromContext(ctx) - // if tokenData != nil { - // // Try to load document access - if it exists (regardless of active status), user has requested it - // _, err := privateTrustService.TrustCenterAccesses.LoadDocumentAccess(ctx, tokenData.TrustCenterID, tokenData.Email, obj.ID) - // if err != nil { - // return false, nil // No access requested or error - // } - // return true, nil // Access exists (requested) - // } - - // return false, nil - - panic(fmt.Errorf("no user or token data found")) + trustCenter := TrustCenterFromContext(ctx) + // Try to load document access - if it exists (regardless of active status), user has requested it + _, err := trustService.TrustCenterAccesses.LoadDocumentAccess( + ctx, + trustCenter.ID, + identity.EmailAddress, + obj.ID, + ) + if err != nil { + // FIXME check for not found and return without error in this case + // r.logger.ErrorCtx(ctx, "cannot check trust center file access", log.Error(err)) + // return false, gqlutils.Internal(ctx) + return false, nil + } + return true, nil // Access exists (requested) } // LightLogoURL is the resolver for the lightLogoURL field. func (r *frameworkResolver) LightLogoURL(ctx context.Context, obj *types.Framework) (*string, error) { - publicTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) + trustService := r.TrustService(ctx, obj.ID.TenantID()) - return publicTrustService.Frameworks.GenerateLightLogoURL(ctx, obj.ID, 1*time.Hour) + return trustService.Frameworks.GenerateLightLogoURL(ctx, obj.ID, 1*time.Hour) } // DarkLogoURL is the resolver for the darkLogoURL field. func (r *frameworkResolver) DarkLogoURL(ctx context.Context, obj *types.Framework) (*string, error) { - privateTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) + trustService := r.TrustService(ctx, obj.ID.TenantID()) - return privateTrustService.Frameworks.GenerateDarkLogoURL(ctx, obj.ID, 1*time.Hour) + return trustService.Frameworks.GenerateDarkLogoURL(ctx, obj.ID, 1*time.Hour) +} + +// SignInWithToken is the resolver for the signInWithToken field. +func (r *mutationResolver) SignInWithToken(ctx context.Context, input types.SignInWithTokenInput) (*types.SignInWithTokenPayload, error) { + _, session, err := r.iam.AuthService.OpenSessionWithMagicLink(ctx, input.Token) + if err != nil { + var errInvalidToken *iam.ErrInvalidToken + if errors.As(err, &errInvalidToken) { + return nil, gqlutils.Invalid(ctx, err) + } + + r.logger.ErrorCtx(ctx, "cannot open session with magic link", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + // TODO cookie domain + w := gqlutils.HTTPResponseWriterFromContext(ctx) + r.sessionCookie.Set(w, session) + + return &types.SignInWithTokenPayload{ + Success: true, + }, nil } // RequestAllAccesses is the resolver for the requestAllAccesses field. func (r *mutationResolver) RequestAllAccesses(ctx context.Context, input types.RequestAllAccessesInput) (*types.RequestAccessesPayload, error) { - // publicTrustService := r.PublicTrustService(ctx, input.TrustCenterID.TenantID()) + trustService := r.TrustService(ctx, input.TrustCenterID.TenantID()) - // userData := r.IdentityFromContext(ctx) - // if userData != nil { - // return nil, fmt.Errorf("session users cannot request trust center access") - // } + identity := authn.IdentityFromContext(ctx) + if identity == nil { + var err error + identity, err = r.iam.AuthService.LoadOrCreateIdentity( + ctx, + &iam.LoadOrCreateIdentityRequest{ + Email: input.Email, + }, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot load or create identity", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + } - // email := input.Email - // tokenData := TokenAccessFromContext(ctx) - // if tokenData != nil { - // if email != nil || input.Name != nil { - // return nil, fmt.Errorf("email and name are not allowed for authenticated users") - // } + access, err := trustService.TrustCenterAccesses.Request( + ctx, + &trust.TrustCenterAccessRequest{ + TrustCenterID: input.TrustCenterID, + Email: identity.EmailAddress, + FullName: identity.FullName, + DocumentIDs: nil, + ReportIDs: nil, + }, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot create trust center access", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } - // email = &tokenData.Email - // } - // if email == nil { - // return nil, fmt.Errorf("email is required for unauthenticated users") - // } - - // access, err := publicTrustService.TrustCenterAccesses.Request(ctx, &trust.TrustCenterAccessRequest{ - // TrustCenterID: input.TrustCenterID, - // Email: *email, - // Name: input.Name, - // DocumentIDs: nil, - // ReportIDs: nil, - // }) - // if err != nil { - // panic(fmt.Errorf("cannot create trust center access: %w", err)) - // } - - // return &types.RequestAccessesPayload{ - // TrustCenterAccess: &types.TrustCenterAccess{ - // ID: access.ID, - // Email: access.Email, - // Name: access.Name, - // CreatedAt: access.CreatedAt, - // UpdatedAt: access.UpdatedAt, - // }, - // }, nil - - panic(fmt.Errorf("no user or token data found")) + return &types.RequestAccessesPayload{ + TrustCenterAccess: &types.TrustCenterAccess{ + ID: access.ID, + Email: access.Email, + Name: access.Name, + CreatedAt: access.CreatedAt, + UpdatedAt: access.UpdatedAt, + }, + }, nil } // ExportDocumentPDF is the resolver for the exportDocumentPDF field. func (r *mutationResolver) ExportDocumentPDF(ctx context.Context, input types.ExportDocumentPDFInput) (*types.ExportDocumentPDFPayload, error) { - // publicTrustService := r.PublicTrustService(ctx, input.DocumentID.TenantID()) + trustService := r.TrustService(ctx, input.DocumentID.TenantID()) - // document, err := publicTrustService.Documents.Get(ctx, input.DocumentID) - // if err != nil { - // panic(fmt.Errorf("cannot load document: %w", err)) - // } + trustCenterInfo := TrustCenterFromContext(ctx) - // if document.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { - // pdf, err := publicTrustService.Documents.ExportPDFWithoutWatermark(ctx, input.DocumentID) - // if err != nil { - // panic(fmt.Errorf("cannot export document PDF: %w", err)) - // } + document, err := trustService.Documents.Get(ctx, input.DocumentID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot load document", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } - // return &types.ExportDocumentPDFPayload{ - // Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(pdf)), - // }, nil - // } + if document.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { + pdf, err := trustService.Documents.ExportPDFWithoutWatermark(ctx, input.DocumentID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot export document PDF", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } - // privateTrustService, err := r.PrivateTrustService(ctx, input.DocumentID.TenantID()) - // if err != nil { - // panic(fmt.Errorf("cannot export document PDF: %w", err)) - // } + return &types.ExportDocumentPDFPayload{ + Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(pdf)), + }, nil + } - // tokenData := TokenAccessFromContext(ctx) - // if tokenData != nil { - // ndaExists := true - // hasAcceptedNDA := false + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return nil, gqlutils.Unauthenticated(ctx, errors.New("unauthenticated")) + } - // trustCenter, _, err := privateTrustService.TrustCenters.Get(ctx, tokenData.TrustCenterID) - // if err != nil { - // panic(fmt.Errorf("cannot get trust center: %w", err)) - // } - // if trustCenter.NonDisclosureAgreementFileID == nil { - // ndaExists = false - // } + ndaExists := true + hasAcceptedNDA := false - // if ndaExists { - // tokenData := TokenAccessFromContext(ctx) - // hasAcceptedNDA, err = privateTrustService.TrustCenterAccesses.HasAcceptedNonDisclosureAgreement(ctx, tokenData.TrustCenterID, tokenData.Email) - // if err != nil { - // panic(fmt.Errorf("cannot check if user has accepted NDA: %w", err)) - // } - // } + trustCenter, _, err := trustService.TrustCenters.Get( + ctx, + trustCenterInfo.ID, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot get trust center", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + if trustCenter.NonDisclosureAgreementFileID == nil { + ndaExists = false + } - // documentAccess, err := privateTrustService.TrustCenterAccesses.LoadDocumentAccess(ctx, tokenData.TrustCenterID, tokenData.Email, input.DocumentID) - // if err != nil { - // panic(fmt.Errorf("cannot check document access: %w", err)) - // } + if ndaExists { + hasAcceptedNDA, err = trustService.TrustCenterAccesses.HasAcceptedNonDisclosureAgreement( + ctx, + trustCenterInfo.ID, + identity.EmailAddress, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot check if user has accepted NDA", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + } - // if documentAccess.Status != coredata.TrustCenterDocumentAccessStatusGranted { - // return nil, fmt.Errorf("access denied: no permission to access this document") - // } + documentAccess, err := trustService.TrustCenterAccesses.LoadDocumentAccess( + ctx, + trustCenterInfo.ID, + identity.EmailAddress, + input.DocumentID, + ) + if err != nil { + // FIXME check for not found and return without error in this case + // r.logger.ErrorCtx(ctx, "cannot check document access", log.Error(err)) + // return false, gqlutils.Internal(ctx) + return nil, nil + } - // if ndaExists && !hasAcceptedNDA { - // return nil, fmt.Errorf("user has not accepted NDA") - // } - // } + if documentAccess.Status != coredata.TrustCenterDocumentAccessStatusGranted { + return nil, gqlutils.Forbiddenf(ctx, "access denied: no permission to access this document") + } - // userData := IdentityFromContext(ctx) - // var userEmail mail.Addr - // if userData != nil { - // userEmail = userData.EmailAddress - // } - // if tokenData != nil { - // userEmail = tokenData.Email - // } + if ndaExists && !hasAcceptedNDA { + return nil, gqlutils.Forbiddenf(ctx, "user has not accepted NDA") + } - // pdf, err := privateTrustService.Documents.ExportPDF(ctx, input.DocumentID, userEmail) - // if err != nil { - // panic(fmt.Errorf("cannot export document PDF: %w", err)) - // } + pdf, err := trustService.Documents.ExportPDF(ctx, input.DocumentID, identity.EmailAddress) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot export document PDF", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } - // return &types.ExportDocumentPDFPayload{ - // Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(pdf)), - // }, nil - - panic(fmt.Errorf("no user or token data found")) + return &types.ExportDocumentPDFPayload{ + Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(pdf)), + }, nil } // ExportReportPDF is the resolver for the exportReportPDF field. func (r *mutationResolver) ExportReportPDF(ctx context.Context, input types.ExportReportPDFInput) (*types.ExportReportPDFPayload, error) { - // publicTrustService := r.PublicTrustService(ctx, input.ReportID.TenantID()) + trustService := r.TrustService(ctx, input.ReportID.TenantID()) - // audit, err := publicTrustService.Audits.GetByReportID(ctx, input.ReportID) - // if err != nil { - // panic(fmt.Errorf("cannot load audit: %w", err)) - // } + trustCenterInfo := TrustCenterFromContext(ctx) - // if audit.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { - // pdf, err := publicTrustService.Reports.ExportPDFWithoutWatermark(ctx, input.ReportID) - // if err != nil { - // panic(fmt.Errorf("cannot export report PDF: %w", err)) - // } + audit, err := trustService.Audits.GetByReportID(ctx, input.ReportID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot load audit", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } - // return &types.ExportReportPDFPayload{ - // Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(pdf)), - // }, nil - // } + if audit.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { + pdf, err := trustService.Reports.ExportPDFWithoutWatermark(ctx, input.ReportID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot export report PDF", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } - // privateTrustService, err := r.PrivateTrustService(ctx, input.ReportID.TenantID()) - // if err != nil { - // return nil, fmt.Errorf("cannot export report PDF: %w", err) - // } + return &types.ExportReportPDFPayload{ + Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(pdf)), + }, nil + } - // tokenData := TokenAccessFromContext(ctx) - // if tokenData != nil { - // ndaExists := true - // hasAcceptedNDA := false + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated") + } - // trustCenter, _, err := privateTrustService.TrustCenters.Get(ctx, tokenData.TrustCenterID) - // if err != nil { - // panic(fmt.Errorf("cannot get trust center: %w", err)) - // } - // if trustCenter.NonDisclosureAgreementFileID == nil { - // ndaExists = false - // } + ndaExists := true + hasAcceptedNDA := false - // if ndaExists { - // hasAcceptedNDA, err = privateTrustService.TrustCenterAccesses.HasAcceptedNonDisclosureAgreement(ctx, tokenData.TrustCenterID, tokenData.Email) - // if err != nil { - // panic(fmt.Errorf("cannot check if user has accepted NDA: %w", err)) - // } - // } + trustCenter, _, err := trustService.TrustCenters.Get( + ctx, + trustCenterInfo.ID, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot get trust center", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + if trustCenter.NonDisclosureAgreementFileID == nil { + ndaExists = false + } - // reportAccess, err := privateTrustService.TrustCenterAccesses.LoadReportAccess(ctx, tokenData.TrustCenterID, tokenData.Email, input.ReportID) - // if err != nil { - // panic(fmt.Errorf("cannot check report access: %w", err)) - // } + if ndaExists { + hasAcceptedNDA, err = trustService.TrustCenterAccesses.HasAcceptedNonDisclosureAgreement( + ctx, + trustCenterInfo.ID, + identity.EmailAddress, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot check if user has accepted NDA", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + } - // if reportAccess.Status != coredata.TrustCenterDocumentAccessStatusGranted { - // return nil, fmt.Errorf("access denied: no permission to access this report") - // } + reportAccess, err := trustService.TrustCenterAccesses.LoadReportAccess( + ctx, + trustCenterInfo.ID, + identity.EmailAddress, + input.ReportID, + ) + if err != nil { + // FIXME check for not found and return without error in this case + // r.logger.ErrorCtx(ctx, "cannot check report access", log.Error(err)) + // return false, gqlutils.Internal(ctx) + return nil, nil + } - // if ndaExists && !hasAcceptedNDA { - // return nil, fmt.Errorf("user has not accepted NDA") - // } - // } + if reportAccess.Status != coredata.TrustCenterDocumentAccessStatusGranted { + return nil, gqlutils.Forbiddenf(ctx, "access denied: no permission to access this report") + } - // userData := IdentityFromContext(ctx) - // var userEmail mail.Addr - // if userData != nil { - // userEmail = userData.EmailAddress - // } - // if tokenData != nil { - // userEmail = tokenData.Email - // } + if ndaExists && !hasAcceptedNDA { + return nil, gqlutils.Forbiddenf(ctx, "user has not accepted NDA") + } - // pdf, err := privateTrustService.Reports.ExportPDF(ctx, input.ReportID, userEmail) - // if err != nil { - // panic(fmt.Errorf("cannot export report PDF: %w", err)) - // } + pdf, err := trustService.Reports.ExportPDF(ctx, input.ReportID, identity.EmailAddress) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot export report PDF", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } - // return &types.ExportReportPDFPayload{ - // Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(pdf)), - // }, nil - - panic(fmt.Errorf("no user or token data found")) + return &types.ExportReportPDFPayload{ + Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(pdf)), + }, nil } // AcceptNonDisclosureAgreement is the resolver for the acceptNonDisclosureAgreement field. func (r *mutationResolver) AcceptNonDisclosureAgreement(ctx context.Context, input types.AcceptNonDisclosureAgreementInput) (*types.AcceptNonDisclosureAgreementPayload, error) { - // privateTrustService, err := r.PrivateTrustService(ctx, input.TrustCenterID.TenantID()) - // if err != nil { - // return nil, fmt.Errorf("cannot accept NDA: %w", err) - // } + trustService := r.TrustService(ctx, input.TrustCenterID.TenantID()) - // tokenData := TokenAccessFromContext(ctx) - // if tokenData == nil { - // return nil, fmt.Errorf("token not found") - // } + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated") + } - // err = privateTrustService.TrustCenterAccesses.AcceptNonDisclosureAgreement(ctx, input.TrustCenterID, tokenData.Email) - // if err != nil { - // return nil, fmt.Errorf("cannot accept NDA: %w", err) - // } + if err := trustService.TrustCenterAccesses.AcceptNonDisclosureAgreement(ctx, input.TrustCenterID, identity.EmailAddress); err != nil { + r.logger.ErrorCtx(ctx, "cannot accept NDA", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } - // return &types.AcceptNonDisclosureAgreementPayload{Success: true}, nil - - panic(fmt.Errorf("no user or token data found")) + return &types.AcceptNonDisclosureAgreementPayload{Success: true}, nil } // RequestDocumentAccess is the resolver for the requestDocumentAccess field. func (r *mutationResolver) RequestDocumentAccess(ctx context.Context, input types.RequestDocumentAccessInput) (*types.RequestAccessesPayload, error) { - // publicTrustService := r.PublicTrustService(ctx, input.TrustCenterID.TenantID()) + trustService := r.TrustService(ctx, input.TrustCenterID.TenantID()) - // var ( - // email mail.Addr - // fullname string - // ) + document, err := trustService.Documents.Get(ctx, input.DocumentID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot load document", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + if document.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { + return nil, gqlutils.Invalidf( + ctx, + "document is publicly available and does not require access request", + ) + } - // identity := connect_v1.IdentityFromContext(ctx) - // if identity != nil { - // email = identity.EmailAddress - // fullname = identity.FullName - // } else if input.Email != nil && input.Name != nil { - // email = *input.Email - // fullname = *input.Name - // } else { - // return nil, gqlutils.Invalid(fmt.Errorf("email and name are required"), nil) - // } + identity := authn.IdentityFromContext(ctx) + if identity == nil { + var err error + identity, err = r.iam.AuthService.LoadOrCreateIdentity( + ctx, + &iam.LoadOrCreateIdentityRequest{ + Email: input.Email, + }, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot load or create identity", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + } - // access, err := publicTrustService.TrustCenterAccesses.Request( - // ctx, - // &trust.TrustCenterAccessRequest{ - // TrustCenterID: input.TrustCenterID, - // Email: email, - // Name: fullname, - // DocumentIDs: []gid.GID{input.DocumentID}, - // ReportIDs: []gid.GID{}, - // }, - // ) - // if err != nil { - // panic(fmt.Errorf("cannot request document access: %w", err)) - // } + access, err := trustService.TrustCenterAccesses.Request( + ctx, + &trust.TrustCenterAccessRequest{ + TrustCenterID: input.TrustCenterID, + Email: identity.EmailAddress, + FullName: identity.FullName, + DocumentIDs: []gid.GID{input.DocumentID}, + ReportIDs: []gid.GID{}, + }, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot request document access", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } - // return &types.RequestAccessesPayload{ - // TrustCenterAccess: &types.TrustCenterAccess{ - // ID: access.ID, - // Email: access.Email, - // Name: access.Name, - // CreatedAt: access.CreatedAt, - // UpdatedAt: access.UpdatedAt, - // }, - // }, nil - - panic(fmt.Errorf("no user or token data found")) + return &types.RequestAccessesPayload{ + TrustCenterAccess: &types.TrustCenterAccess{ + ID: access.ID, + Email: access.Email, + Name: access.Name, + CreatedAt: access.CreatedAt, + UpdatedAt: access.UpdatedAt, + }, + }, nil } // RequestReportAccess is the resolver for the requestReportAccess field. func (r *mutationResolver) RequestReportAccess(ctx context.Context, input types.RequestReportAccessInput) (*types.RequestAccessesPayload, error) { - // publicTrustService := r.PublicTrustService(ctx, input.TrustCenterID.TenantID()) + trustService := r.TrustService(ctx, input.TrustCenterID.TenantID()) - // audit, err := publicTrustService.Audits.GetByReportID(ctx, input.ReportID) - // if err != nil { - // panic(fmt.Errorf("cannot load audit: %w", err)) - // } + audit, err := trustService.Audits.GetByReportID(ctx, input.ReportID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot load audit", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } - // if audit.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { - // return nil, fmt.Errorf("report is publicly available and does not require access request") - // } + if audit.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { + return nil, gqlutils.Invalidf( + ctx, + "report is publicly available and does not require access request", + ) + } - // userData := r.IdentityFromContext(ctx) - // if userData != nil { - // return nil, fmt.Errorf("session users cannot request trust center access") - // } + identity := authn.IdentityFromContext(ctx) + if identity == nil { + var err error + identity, err = r.iam.AuthService.LoadOrCreateIdentity( + ctx, + &iam.LoadOrCreateIdentityRequest{ + Email: input.Email, + }, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot load or create identity", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + } - // email := input.Email - // tokenData := TokenAccessFromContext(ctx) - // if tokenData != nil { - // email = &tokenData.Email - // } - // if email == nil { - // return nil, fmt.Errorf("email is required for unauthenticated users") - // } + access, err := trustService.TrustCenterAccesses.Request( + ctx, + &trust.TrustCenterAccessRequest{ + TrustCenterID: input.TrustCenterID, + Email: identity.EmailAddress, + FullName: identity.FullName, + DocumentIDs: []gid.GID{}, + ReportIDs: []gid.GID{input.ReportID}, + }, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot request report access", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } - // access, err := publicTrustService.TrustCenterAccesses.Request(ctx, &trust.TrustCenterAccessRequest{ - // TrustCenterID: input.TrustCenterID, - // Email: *email, - // Name: input.Name, - // DocumentIDs: []gid.GID{}, - // ReportIDs: []gid.GID{input.ReportID}, - // }) - // if err != nil { - // panic(fmt.Errorf("cannot request report access: %w", err)) - // } - - // return &types.RequestAccessesPayload{ - // TrustCenterAccess: &types.TrustCenterAccess{ - // ID: access.ID, - // Email: access.Email, - // Name: access.Name, - // CreatedAt: access.CreatedAt, - // UpdatedAt: access.UpdatedAt, - // }, - // }, nil - - panic(fmt.Errorf("no user or token data found")) + return &types.RequestAccessesPayload{ + TrustCenterAccess: &types.TrustCenterAccess{ + ID: access.ID, + Email: access.Email, + Name: access.Name, + CreatedAt: access.CreatedAt, + UpdatedAt: access.UpdatedAt, + }, + }, nil } // RequestTrustCenterFileAccess is the resolver for the requestTrustCenterFileAccess field. func (r *mutationResolver) RequestTrustCenterFileAccess(ctx context.Context, input types.RequestTrustCenterFileAccessInput) (*types.RequestAccessesPayload, error) { - // publicTrustService := r.PublicTrustService(ctx, input.TrustCenterID.TenantID()) + trustService := r.TrustService(ctx, input.TrustCenterID.TenantID()) - // trustCenterFile, err := publicTrustService.TrustCenterFiles.Get(ctx, input.TrustCenterFileID) - // if err != nil { - // panic(fmt.Errorf("cannot load trust center file: %w", err)) - // } + trustCenterFile, err := trustService.TrustCenterFiles.Get(ctx, input.TrustCenterFileID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot load trust center file", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } - // if trustCenterFile.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { - // return nil, fmt.Errorf("trust center file is publicly available and does not require access request") - // } + if trustCenterFile.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { + return nil, gqlutils.Invalidf( + ctx, + "trust center file is publicly available and does not require access request", + ) + } - // userData := r.IdentityFromContext(ctx) - // if userData != nil { - // return nil, fmt.Errorf("session users cannot request trust center access") - // } + identity := authn.IdentityFromContext(ctx) + if identity == nil { + var err error + identity, err = r.iam.AuthService.LoadOrCreateIdentity( + ctx, + &iam.LoadOrCreateIdentityRequest{ + Email: input.Email, + }, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot load or create identity", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + } - // email := input.Email - // tokenData := TokenAccessFromContext(ctx) - // if tokenData != nil { - // email = &tokenData.Email - // } - // if email == nil { - // return nil, fmt.Errorf("email is required for unauthenticated users") - // } + access, err := trustService.TrustCenterAccesses.Request( + ctx, + &trust.TrustCenterAccessRequest{ + TrustCenterID: input.TrustCenterID, + Email: identity.EmailAddress, + FullName: identity.FullName, + DocumentIDs: []gid.GID{}, + ReportIDs: []gid.GID{}, + TrustCenterFileIDs: []gid.GID{input.TrustCenterFileID}, + }, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot request trust center file access", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } - // access, err := publicTrustService.TrustCenterAccesses.Request(ctx, &trust.TrustCenterAccessRequest{ - // TrustCenterID: input.TrustCenterID, - // Email: *email, - // Name: input.Name, - // DocumentIDs: []gid.GID{}, - // ReportIDs: []gid.GID{}, - // TrustCenterFileIDs: []gid.GID{input.TrustCenterFileID}, - // }) - // if err != nil { - // panic(fmt.Errorf("cannot request trust center file access: %w", err)) - // } - - // return &types.RequestAccessesPayload{ - // TrustCenterAccess: &types.TrustCenterAccess{ - // ID: access.ID, - // Email: access.Email, - // Name: access.Name, - // CreatedAt: access.CreatedAt, - // UpdatedAt: access.UpdatedAt, - // }, - // }, nil - - panic(fmt.Errorf("no user or token data found")) + return &types.RequestAccessesPayload{ + TrustCenterAccess: &types.TrustCenterAccess{ + ID: access.ID, + Email: access.Email, + Name: access.Name, + CreatedAt: access.CreatedAt, + UpdatedAt: access.UpdatedAt, + }, + }, nil } // ExportTrustCenterFile is the resolver for the exportTrustCenterFile field. func (r *mutationResolver) ExportTrustCenterFile(ctx context.Context, input types.ExportTrustCenterFileInput) (*types.ExportTrustCenterFilePayload, error) { - // publicTrustService := r.PublicTrustService(ctx, input.TrustCenterFileID.TenantID()) + trustService := r.TrustService(ctx, input.TrustCenterFileID.TenantID()) - // trustCenterFile, err := publicTrustService.TrustCenterFiles.Get(ctx, input.TrustCenterFileID) - // if err != nil { - // panic(fmt.Errorf("cannot load trust center file: %w", err)) - // } + trustCenterInfo := TrustCenterFromContext(ctx) - // if trustCenterFile.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { - // fileData, err := publicTrustService.TrustCenterFiles.ExportFileWithoutWatermark(ctx, input.TrustCenterFileID) - // if err != nil { - // panic(fmt.Errorf("cannot export trust center file: %w", err)) - // } + trustCenterFile, err := trustService.TrustCenterFiles.Get(ctx, input.TrustCenterFileID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot load trust center file", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } - // return &types.ExportTrustCenterFilePayload{ - // Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(fileData)), - // }, nil - // } + if trustCenterFile.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { + fileData, err := trustService.TrustCenterFiles.ExportFileWithoutWatermark(ctx, input.TrustCenterFileID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot export trust center file", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } - // privateTrustService, err := r.PrivateTrustService(ctx, input.TrustCenterFileID.TenantID()) - // if err != nil { - // return nil, fmt.Errorf("cannot export trust center file: %w", err) - // } + return &types.ExportTrustCenterFilePayload{ + Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(fileData)), + }, nil + } - // tokenData := TokenAccessFromContext(ctx) - // if tokenData != nil { - // ndaExists := true - // hasAcceptedNDA := false + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated") + } - // trustCenter, _, err := privateTrustService.TrustCenters.Get(ctx, tokenData.TrustCenterID) - // if err != nil { - // panic(fmt.Errorf("cannot get trust center: %w", err)) - // } - // if trustCenter.NonDisclosureAgreementFileID == nil { - // ndaExists = false - // } + ndaExists := true + hasAcceptedNDA := false - // if ndaExists { - // hasAcceptedNDA, err = privateTrustService.TrustCenterAccesses.HasAcceptedNonDisclosureAgreement(ctx, tokenData.TrustCenterID, tokenData.Email) - // if err != nil { - // panic(fmt.Errorf("cannot check if user has accepted NDA: %w", err)) - // } - // } + trustCenter, _, err := trustService.TrustCenters.Get( + ctx, + trustCenterInfo.ID, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot get trust center", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + if trustCenter.NonDisclosureAgreementFileID == nil { + ndaExists = false + } - // fileAccess, err := privateTrustService.TrustCenterAccesses.LoadTrustCenterFileAccess(ctx, tokenData.TrustCenterID, tokenData.Email, input.TrustCenterFileID) - // if err != nil { - // panic(fmt.Errorf("cannot check trust center file access: %w", err)) - // } + if ndaExists { + hasAcceptedNDA, err = trustService.TrustCenterAccesses.HasAcceptedNonDisclosureAgreement(ctx, + trustCenterInfo.ID, + identity.EmailAddress, + ) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot check if user has accepted NDA", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + } - // if fileAccess.Status != coredata.TrustCenterDocumentAccessStatusGranted { - // return nil, fmt.Errorf("access denied: no permission to access this file") - // } + fileAccess, err := trustService.TrustCenterAccesses.LoadTrustCenterFileAccess(ctx, + trustCenterInfo.ID, + identity.EmailAddress, + input.TrustCenterFileID, + ) + if err != nil { + // FIXME check for not found and return without error in this case + // r.logger.ErrorCtx(ctx, "cannot check trust center file access", log.Error(err)) + // return false, gqlutils.Internal(ctx) + return nil, nil + } - // if ndaExists && !hasAcceptedNDA { - // return nil, fmt.Errorf("user has not accepted NDA") - // } - // } + if fileAccess.Status != coredata.TrustCenterDocumentAccessStatusGranted { + return nil, gqlutils.Forbiddenf(ctx, "access denied: no permission to access this file") + } - // userData := IdentityFromContext(ctx) - // var userEmail mail.Addr - // if userData != nil { - // userEmail = userData.EmailAddress - // } - // if tokenData != nil { - // userEmail = tokenData.Email - // } + if ndaExists && !hasAcceptedNDA { + return nil, gqlutils.Forbiddenf(ctx, "user has not accepted NDA") + } - // fileData, err := privateTrustService.TrustCenterFiles.ExportFile(ctx, input.TrustCenterFileID, userEmail) - // if err != nil { - // panic(fmt.Errorf("cannot export trust center file: %w", err)) - // } + fileData, err := trustService.TrustCenterFiles.ExportFile(ctx, input.TrustCenterFileID, identity.EmailAddress) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot export trust center file", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } - // return &types.ExportTrustCenterFilePayload{ - // Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(fileData)), - // }, nil - - panic(fmt.Errorf("no user or token data found")) + return &types.ExportTrustCenterFilePayload{ + Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(fileData)), + }, nil } // LogoURL is the resolver for the logoUrl field. func (r *organizationResolver) LogoURL(ctx context.Context, obj *types.Organization) (*string, error) { - publicTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) + trustService := r.TrustService(ctx, obj.ID.TenantID()) - return publicTrustService.Organizations.GenerateLogoURL(ctx, obj.ID, 1*time.Hour) + return trustService.Organizations.GenerateLogoURL(ctx, obj.ID, 1*time.Hour) +} + +// Viewer is the resolver for the viewer field. +func (r *queryResolver) Viewer(ctx context.Context) (*types.Identity, error) { + identity := authn.IdentityFromContext(ctx) + + if identity == nil { + return nil, nil + } + + return &types.Identity{ + ID: identity.ID, + Email: identity.EmailAddress, + FullName: identity.FullName, + EmailVerified: identity.EmailAddressVerified, + CreatedAt: identity.CreatedAt, + UpdatedAt: identity.UpdatedAt, + }, nil } // Node is the resolver for the node field. func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error) { - publicTrustService := r.PublicTrustService(ctx, id.TenantID()) + trustService := r.TrustService(ctx, id.TenantID()) switch id.EntityType() { case coredata.OrganizationEntityType: - organization, err := publicTrustService.Organizations.Get(ctx, id) + organization, err := trustService.Organizations.Get(ctx, id) if err != nil { - panic(fmt.Errorf("cannot get organization: %w", err)) + r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return types.NewOrganization(organization), nil case coredata.DocumentEntityType: - document, err := publicTrustService.Documents.Get(ctx, id) + document, err := trustService.Documents.Get(ctx, id) if err != nil { - panic(fmt.Errorf("cannot get document: %w", err)) + r.logger.ErrorCtx(ctx, "cannot get document", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return types.NewDocument(document), nil case coredata.FrameworkEntityType: - framework, err := publicTrustService.Frameworks.Get(ctx, id) + framework, err := trustService.Frameworks.Get(ctx, id) if err != nil { - panic(fmt.Errorf("cannot get framework: %w", err)) + r.logger.ErrorCtx(ctx, "cannot get framework", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return types.NewFramework(framework), nil case coredata.ReportEntityType: - report, err := publicTrustService.Reports.Get(ctx, id) + report, err := trustService.Reports.Get(ctx, id) if err != nil { - panic(fmt.Errorf("cannot get report: %w", err)) + r.logger.ErrorCtx(ctx, "cannot get report", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return types.NewReport(report), nil case coredata.AuditEntityType: - audit, err := publicTrustService.Audits.Get(ctx, id) + audit, err := trustService.Audits.Get(ctx, id) if err != nil { - panic(fmt.Errorf("cannot get audit: %w", err)) + r.logger.ErrorCtx(ctx, "cannot get audit", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return types.NewAudit(audit), nil case coredata.VendorEntityType: - vendor, err := publicTrustService.Vendors.Get(ctx, id) + vendor, err := trustService.Vendors.Get(ctx, id) if err != nil { - panic(fmt.Errorf("cannot get vendor: %w", err)) + r.logger.ErrorCtx(ctx, "cannot get vendor", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return types.NewVendor(vendor), nil case coredata.TrustCenterEntityType: - trustCenter, file, err := publicTrustService.TrustCenters.Get(ctx, id) + trustCenter, file, err := trustService.TrustCenters.Get(ctx, id) if err != nil { - panic(fmt.Errorf("cannot get trust center: %w", err)) + r.logger.ErrorCtx(ctx, "cannot get trust center", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return types.NewTrustCenter(trustCenter, file), nil case coredata.TrustCenterReferenceEntityType: - reference, err := publicTrustService.TrustCenterReferences.Get(ctx, id) + reference, err := trustService.TrustCenterReferences.Get(ctx, id) if err != nil { - panic(fmt.Errorf("cannot get trust center reference: %w", err)) + r.logger.ErrorCtx(ctx, "cannot get trust center reference", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return types.NewTrustCenterReference(reference), nil default: - return nil, gqlerror.Errorf("node %q not found", id) + return nil, gqlutils.NotFoundf(ctx, "node %q not found", id) } } @@ -678,13 +774,13 @@ func (r *queryResolver) TrustCenterBySlug(ctx context.Context, slug string) (*ty return nil, nil } - publicTrustService := r.PublicTrustService(ctx, trustCenter.TenantID) - trustCenter, file, err := publicTrustService.TrustCenters.Get(ctx, trustCenter.ID) + trustService := r.TrustService(ctx, trustCenter.TenantID) + trustCenter, file, err := trustService.TrustCenters.Get(ctx, trustCenter.ID) if err != nil { panic(fmt.Errorf("cannot get trust center: %w", err)) } - org, err := publicTrustService.Organizations.Get(ctx, trustCenter.OrganizationID) + org, err := trustService.Organizations.Get(ctx, trustCenter.OrganizationID) if err != nil { panic(fmt.Errorf("cannot get organization: %w", err)) } @@ -696,118 +792,97 @@ func (r *queryResolver) TrustCenterBySlug(ctx context.Context, slug string) (*ty // CurrentTrustCenter is the resolver for the currentTrustCenter field. func (r *queryResolver) CurrentTrustCenter(ctx context.Context) (*types.TrustCenter, error) { - // // Get organization and tenant from custom domain context - // organizationID, ok := GetCustomDomainOrganizationID(ctx) - // if !ok { - // return nil, fmt.Errorf("organization not found for custom domain") - // } + trustCenterInfo := TrustCenterFromContext(ctx) - // tenantID, ok := GetCustomDomainTenantID(ctx) - // if !ok { - // return nil, fmt.Errorf("tenant not found for custom domain") - // } + trustService := r.TrustService(ctx, trustCenterInfo.ID.TenantID()) - // publicTrustService := r.PublicTrustService(ctx, tenantID) + org, err := trustService.Organizations.Get(ctx, trustCenterInfo.OrganizationID) + if err != nil { + panic(fmt.Errorf("cannot get organization: %w", err)) + } - // trustCenter, err := publicTrustService.TrustCenters.GetByOrganizationID(ctx, organizationID) - // if err != nil { - // return nil, fmt.Errorf("cannot load trust center: %w", err) - // } + trustCenter, file, err := trustService.TrustCenters.Get(ctx, trustCenterInfo.ID) + if err != nil { + panic(fmt.Errorf("cannot get trust center: %w", err)) + } - // if !trustCenter.Active { - // return nil, nil - // } + response := types.NewTrustCenter(trustCenter, file) + response.Organization = types.NewOrganization(org) - // trustCenter, file, err := publicTrustService.TrustCenters.Get(ctx, trustCenter.ID) - // if err != nil { - // panic(fmt.Errorf("cannot get trust center: %w", err)) - // } - - // org, err := publicTrustService.Organizations.Get(ctx, organizationID) - // if err != nil { - // panic(fmt.Errorf("cannot get organization: %w", err)) - // } - // response := types.NewTrustCenter(trustCenter, file) - // response.Organization = types.NewOrganization(org) - - // return response, nil - - panic(fmt.Errorf("no user or token data found")) + return response, nil } // IsUserAuthorized is the resolver for the isUserAuthorized field. func (r *reportResolver) IsUserAuthorized(ctx context.Context, obj *types.Report) (bool, error) { - // publicTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) + trustService := r.TrustService(ctx, obj.ID.TenantID()) - // audit, err := publicTrustService.Audits.GetByReportID(ctx, obj.ID) - // if err != nil { - // panic(fmt.Errorf("cannot load document: %w", err)) - // } + trustCenterInfo := TrustCenterFromContext(ctx) - // if audit.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { - // return true, nil - // } + audit, err := trustService.Audits.GetByReportID(ctx, obj.ID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot load document", log.Error(err)) + return false, gqlutils.Internal(ctx) + } - // privateTrustService, err := r.PrivateTrustService(ctx, obj.ID.TenantID()) - // if err != nil { - // return false, nil - // } + if audit.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { + return true, nil + } - // userData := r.IdentityFromContext(ctx) - // if userData != nil { - // return true, nil - // } + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return false, gqlutils.Unauthenticatedf(ctx, "unauthenticated") + } - // tokenData := TokenAccessFromContext(ctx) - // if tokenData != nil { - // reportAccess, err := privateTrustService.TrustCenterAccesses.LoadReportAccess(ctx, tokenData.TrustCenterID, tokenData.Email, obj.ID) - // if err != nil { - // return false, nil - // } + reportAccess, err := trustService.TrustCenterAccesses.LoadReportAccess(ctx, + trustCenterInfo.ID, + identity.EmailAddress, + obj.ID, + ) + if err != nil { + // FIXME check for not found and return without error in this case + // r.logger.ErrorCtx(ctx, "cannot check report access", log.Error(err)) + // return false, gqlutils.Internal(ctx) + return false, nil + } - // return reportAccess.Status == coredata.TrustCenterDocumentAccessStatusGranted, nil - // } - - // panic(fmt.Errorf("no user or token data found")) - - panic(fmt.Errorf("no user or token data found")) + return reportAccess.Status == coredata.TrustCenterDocumentAccessStatusGranted, nil } // HasUserRequestedAccess is the resolver for the hasUserRequestedAccess field. func (r *reportResolver) HasUserRequestedAccess(ctx context.Context, obj *types.Report) (bool, error) { - // privateTrustService, err := r.PrivateTrustService(ctx, obj.ID.TenantID()) - // if err != nil { - // return false, nil - // } + trustService := r.TrustService(ctx, obj.ID.TenantID()) - // userData := r.IdentityFromContext(ctx) - // if userData != nil { - // return false, nil - // } + trustCenterInfo := TrustCenterFromContext(ctx) - // tokenData := TokenAccessFromContext(ctx) - // if tokenData != nil { - // _, err := privateTrustService.TrustCenterAccesses.LoadReportAccess(ctx, tokenData.TrustCenterID, tokenData.Email, obj.ID) - // if err != nil { - // return false, nil - // } - // return true, nil - // } + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return false, nil // User is not authenticated, so no access requested + } - // return false, nil + _, err := trustService.TrustCenterAccesses.LoadReportAccess(ctx, + trustCenterInfo.ID, + identity.EmailAddress, + obj.ID, + ) + if err != nil { + // FIXME check for not found and return without error in this case + // r.logger.ErrorCtx(ctx, "cannot check report access", log.Error(err)) + // return false, gqlutils.Internal(ctx) + return false, nil + } - panic(fmt.Errorf("no user or token data found")) + return true, nil } // NdaFileURL is the resolver for the ndaFileUrl field. func (r *trustCenterResolver) NdaFileURL(ctx context.Context, obj *types.TrustCenter) (*string, error) { - privateTrustService, err := r.PrivateTrustService(ctx, obj.ID.TenantID()) - if err != nil { - return nil, nil - } + trustService := r.TrustService(ctx, obj.ID.TenantID()) - fileURL, err := privateTrustService.TrustCenters.GenerateNDAFileURL(ctx, obj.ID, 15*time.Minute) + fileURL, err := trustService.TrustCenters.GenerateNDAFileURL(ctx, obj.ID, 15*time.Minute) if err != nil { + // FIXME: add error not found check etc + // r.logger.ErrorCtx(ctx, "cannot generate NDA file URL", log.Error(err)) + // return nil, gqlutils.Internal(ctx) return nil, nil } @@ -821,43 +896,32 @@ func (r *trustCenterResolver) Organization(ctx context.Context, obj *types.Trust // IsUserAuthenticated is the resolver for the isUserAuthenticated field. func (r *trustCenterResolver) IsUserAuthenticated(ctx context.Context, obj *types.TrustCenter) (bool, error) { - _, err := r.PrivateTrustService(ctx, obj.ID.TenantID()) - if err != nil { - return false, nil - } + identity := authn.IdentityFromContext(ctx) - return true, nil + return identity != nil, nil } // HasAcceptedNonDisclosureAgreement is the resolver for the hasAcceptedNonDisclosureAgreement field. func (r *trustCenterResolver) HasAcceptedNonDisclosureAgreement(ctx context.Context, obj *types.TrustCenter) (bool, error) { - // privateTrustService, err := r.PrivateTrustService(ctx, obj.ID.TenantID()) - // if err != nil { - // return false, nil - // } + trustService := r.TrustService(ctx, obj.ID.TenantID()) - // userData := IdentityFromContext(ctx) - // if userData != nil { - // return true, nil - // } + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return false, nil // User is not authenticated, so no NDA accepted + } - // tokenData := TokenAccessFromContext(ctx) - // if tokenData != nil { - // hasAcceptedNDA, err := privateTrustService.TrustCenterAccesses.HasAcceptedNonDisclosureAgreement(ctx, obj.ID, tokenData.Email) - // if err != nil { - // panic(fmt.Errorf("cannot check if user has accepted NDA: %w", err)) - // } - // return hasAcceptedNDA, nil - // } + hasAcceptedNDA, err := trustService.TrustCenterAccesses.HasAcceptedNonDisclosureAgreement(ctx, obj.ID, identity.EmailAddress) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot check if user has accepted NDA", log.Error(err)) + return false, gqlutils.Internal(ctx) + } - // panic(fmt.Errorf("no user or token data found")) - - panic(fmt.Errorf("no user or token data found")) + return hasAcceptedNDA, nil } // Documents is the resolver for the documents field. func (r *trustCenterResolver) Documents(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.DocumentConnection, error) { - publicTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) + trustService := r.TrustService(ctx, obj.ID.TenantID()) pageOrderBy := page.OrderBy[coredata.DocumentOrderField]{ Field: coredata.DocumentOrderFieldTitle, @@ -865,9 +929,10 @@ func (r *trustCenterResolver) Documents(ctx context.Context, obj *types.TrustCen } cursor := types.NewCursor(first, after, last, before, pageOrderBy) - documentPage, err := publicTrustService.Documents.ListForOrganizationId(ctx, obj.Organization.ID, cursor) + documentPage, err := trustService.Documents.ListForOrganizationId(ctx, obj.Organization.ID, cursor) if err != nil { - panic(fmt.Errorf("cannot list public documents: %w", err)) + r.logger.ErrorCtx(ctx, "cannot list public documents", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return types.NewDocumentConnection(documentPage), nil @@ -875,7 +940,7 @@ func (r *trustCenterResolver) Documents(ctx context.Context, obj *types.TrustCen // Audits is the resolver for the audits field. func (r *trustCenterResolver) Audits(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.AuditConnection, error) { - publicTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) + trustService := r.TrustService(ctx, obj.ID.TenantID()) pageOrderBy := page.OrderBy[coredata.AuditOrderField]{ Field: coredata.AuditOrderFieldValidFrom, @@ -883,9 +948,10 @@ func (r *trustCenterResolver) Audits(ctx context.Context, obj *types.TrustCenter } cursor := types.NewCursor(first, after, last, before, pageOrderBy) - auditPage, err := publicTrustService.Audits.ListForOrganizationId(ctx, obj.Organization.ID, cursor) + auditPage, err := trustService.Audits.ListForOrganizationId(ctx, obj.Organization.ID, cursor) if err != nil { - panic(fmt.Errorf("cannot list public audits: %w", err)) + r.logger.ErrorCtx(ctx, "cannot list public audits", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return types.NewAuditConnection(auditPage), nil @@ -893,7 +959,7 @@ func (r *trustCenterResolver) Audits(ctx context.Context, obj *types.TrustCenter // Vendors is the resolver for the vendors field. func (r *trustCenterResolver) Vendors(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.VendorConnection, error) { - publicTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) + trustService := r.TrustService(ctx, obj.ID.TenantID()) pageOrderBy := page.OrderBy[coredata.VendorOrderField]{ Field: coredata.VendorOrderFieldName, @@ -901,9 +967,10 @@ func (r *trustCenterResolver) Vendors(ctx context.Context, obj *types.TrustCente } cursor := types.NewCursor(first, after, last, before, pageOrderBy) - vendorPage, err := publicTrustService.Vendors.ListForOrganizationId(ctx, obj.Organization.ID, cursor) + vendorPage, err := trustService.Vendors.ListForOrganizationId(ctx, obj.Organization.ID, cursor) if err != nil { - panic(fmt.Errorf("cannot list public vendors: %w", err)) + r.logger.ErrorCtx(ctx, "cannot list public vendors", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return types.NewVendorConnection(vendorPage), nil @@ -911,7 +978,7 @@ func (r *trustCenterResolver) Vendors(ctx context.Context, obj *types.TrustCente // References is the resolver for the references field. func (r *trustCenterResolver) References(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TrustCenterReferenceConnection, error) { - publicTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) + trustService := r.TrustService(ctx, obj.ID.TenantID()) pageOrderBy := page.OrderBy[coredata.TrustCenterReferenceOrderField]{ Field: coredata.TrustCenterReferenceOrderFieldRank, @@ -919,9 +986,10 @@ func (r *trustCenterResolver) References(ctx context.Context, obj *types.TrustCe } cursor := types.NewCursor(first, after, last, before, pageOrderBy) - referencePage, err := publicTrustService.TrustCenterReferences.ListForTrustCenterID(ctx, obj.ID, cursor) + referencePage, err := trustService.TrustCenterReferences.ListForTrustCenterID(ctx, obj.ID, cursor) if err != nil { - panic(fmt.Errorf("cannot list public trust center references: %w", err)) + r.logger.ErrorCtx(ctx, "cannot list public trust center references", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return types.NewTrustCenterReferenceConnection(referencePage), nil @@ -929,7 +997,7 @@ func (r *trustCenterResolver) References(ctx context.Context, obj *types.TrustCe // TrustCenterFiles is the resolver for the trustCenterFiles field. func (r *trustCenterResolver) TrustCenterFiles(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TrustCenterFileConnection, error) { - publicTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) + trustService := r.TrustService(ctx, obj.ID.TenantID()) pageOrderBy := page.OrderBy[coredata.TrustCenterFileOrderField]{ Field: coredata.TrustCenterFileOrderFieldName, @@ -937,9 +1005,10 @@ func (r *trustCenterResolver) TrustCenterFiles(ctx context.Context, obj *types.T } cursor := types.NewCursor(first, after, last, before, pageOrderBy) - trustCenterFilePage, err := publicTrustService.TrustCenterFiles.ListForOrganizationId(ctx, obj.Organization.ID, cursor) + trustCenterFilePage, err := trustService.TrustCenterFiles.ListForOrganizationId(ctx, obj.Organization.ID, cursor) if err != nil { - panic(fmt.Errorf("cannot list public trust center files: %w", err)) + r.logger.ErrorCtx(ctx, "cannot list public trust center files", log.Error(err)) + return nil, gqlutils.Internal(ctx) } return types.NewTrustCenterFileConnection(trustCenterFilePage), nil @@ -947,73 +1016,71 @@ func (r *trustCenterResolver) TrustCenterFiles(ctx context.Context, obj *types.T // IsUserAuthorized is the resolver for the isUserAuthorized field. func (r *trustCenterFileResolver) IsUserAuthorized(ctx context.Context, obj *types.TrustCenterFile) (bool, error) { - // publicTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) + trustService := r.TrustService(ctx, obj.ID.TenantID()) - // trustCenterFile, err := publicTrustService.TrustCenterFiles.Get(ctx, obj.ID) - // if err != nil { - // panic(fmt.Errorf("cannot load trust center file: %w", err)) - // } + trustCenterInfo := TrustCenterFromContext(ctx) - // if trustCenterFile.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { - // return true, nil - // } + trustCenterFile, err := trustService.TrustCenterFiles.Get(ctx, obj.ID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot load trust center file", log.Error(err)) + return false, gqlutils.Internal(ctx) + } - // privateTrustService, err := r.PrivateTrustService(ctx, obj.ID.TenantID()) - // if err != nil { - // return false, nil - // } + if trustCenterFile.TrustCenterVisibility == coredata.TrustCenterVisibilityPublic { + return true, nil + } - // userData := r.IdentityFromContext(ctx) - // if userData != nil { - // return true, nil - // } + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return false, gqlutils.Unauthenticatedf(ctx, "unauthenticated") + } - // tokenData := TokenAccessFromContext(ctx) - // if tokenData != nil { - // fileAccess, err := privateTrustService.TrustCenterAccesses.LoadTrustCenterFileAccess(ctx, tokenData.TrustCenterID, tokenData.Email, obj.ID) - // if err != nil { - // return false, nil - // } + fileAccess, err := trustService.TrustCenterAccesses.LoadTrustCenterFileAccess(ctx, + trustCenterInfo.ID, + identity.EmailAddress, + obj.ID, + ) + if err != nil { + // FIXME check for not found and return without error in this case + // r.logger.ErrorCtx(ctx, "cannot check trust center file access", log.Error(err)) + // return false, gqlutils.Internal(ctx) + return false, nil + } - // return fileAccess.Status == coredata.TrustCenterDocumentAccessStatusGranted, nil - // } - - // panic(fmt.Errorf("no user or token data found")) - - panic(fmt.Errorf("no user or token data found")) + return fileAccess.Status == coredata.TrustCenterDocumentAccessStatusGranted, nil } // HasUserRequestedAccess is the resolver for the hasUserRequestedAccess field. func (r *trustCenterFileResolver) HasUserRequestedAccess(ctx context.Context, obj *types.TrustCenterFile) (bool, error) { - // privateTrustService, err := r.PrivateTrustService(ctx, obj.ID.TenantID()) - // if err != nil { - // return false, nil - // } + trustService := r.TrustService(ctx, obj.ID.TenantID()) - // userData := r.IdentityFromContext(ctx) - // if userData != nil { - // return false, nil - // } + trustCenterInfo := TrustCenterFromContext(ctx) - // tokenData := TokenAccessFromContext(ctx) - // if tokenData != nil { - // _, err := privateTrustService.TrustCenterAccesses.LoadTrustCenterFileAccess(ctx, tokenData.TrustCenterID, tokenData.Email, obj.ID) - // if err != nil { - // return false, nil - // } - // return true, nil - // } + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return false, nil // User is not authenticated, so no access requested + } - // return false, nil + _, err := trustService.TrustCenterAccesses.LoadTrustCenterFileAccess(ctx, + trustCenterInfo.ID, + identity.EmailAddress, + obj.ID, + ) + if err != nil { + // FIXME check for not found and return without error in this case + // r.logger.ErrorCtx(ctx, "cannot check trust center file access", log.Error(err)) + // return false, gqlutils.Internal(ctx) + return false, nil + } - panic(fmt.Errorf("no user or token data found")) + return true, nil } // LogoURL is the resolver for the logoUrl field. func (r *trustCenterReferenceResolver) LogoURL(ctx context.Context, obj *types.TrustCenterReference) (string, error) { - publicTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) + trustService := r.TrustService(ctx, obj.ID.TenantID()) - logoURL, err := publicTrustService.TrustCenterReferences.GenerateLogoURL(ctx, obj.ID, 1*time.Hour) + logoURL, err := trustService.TrustCenterReferences.GenerateLogoURL(ctx, obj.ID, 1*time.Hour) if err != nil { panic(fmt.Errorf("cannot generate logo URL: %w", err)) } diff --git a/pkg/server/gqlutils/errors.go b/pkg/server/gqlutils/errors.go index 071e017cf..42cdb82ab 100644 --- a/pkg/server/gqlutils/errors.go +++ b/pkg/server/gqlutils/errors.go @@ -58,6 +58,10 @@ func Forbidden(ctx context.Context, err error) *gqlerror.Error { } } +func Forbiddenf(ctx context.Context, format string, a ...any) *gqlerror.Error { + return Forbidden(ctx, fmt.Errorf(format, a...)) +} + func NotFound(ctx context.Context, err error) *gqlerror.Error { return &gqlerror.Error{ Message: err.Error(), @@ -68,6 +72,10 @@ func NotFound(ctx context.Context, err error) *gqlerror.Error { } } +func NotFoundf(ctx context.Context, format string, a ...any) *gqlerror.Error { + return NotFound(ctx, fmt.Errorf(format, a...)) +} + func Conflict(ctx context.Context, err error) *gqlerror.Error { return &gqlerror.Error{ Message: err.Error(), diff --git a/pkg/server/server.go b/pkg/server/server.go index 7abb1a7e4..55f1866aa 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -15,7 +15,6 @@ package server import ( - "context" "errors" "net/http" "strings" @@ -193,7 +192,7 @@ func (s *Server) loadTrustCenterBySlugOrID(next http.Handler) http.Handler { ) } - ctx = s.addTrustCenterToContext(ctx, trustCenter.ID.TenantID(), trustCenter.OrganizationID) + ctx = trust_v1.ContextWithTrustCenter(ctx, *trustCenter) next.ServeHTTP(w, r.WithContext(ctx)) }) } @@ -235,16 +234,20 @@ func (s *Server) loadTrustCenterByDomain(next http.Handler) http.Handler { log.String("organization_id", organizationID.String()), ) - ctx = s.addTrustCenterToContext(ctx, organizationID.TenantID(), organizationID) + trustCenter, err := s.proboService.LoadTrustCenterByOrganizationID(ctx, organizationID) + if err != nil { + s.logger.WarnCtx(ctx, "trust center not found", + log.Error(err), + ) + http.Error(w, "Trust center not found", http.StatusNotFound) + return + } + + ctx = trust_v1.ContextWithTrustCenter(ctx, *trustCenter) next.ServeHTTP(w, r.WithContext(ctx)) }) } -func (s *Server) addTrustCenterToContext(ctx context.Context, tenantID, organizationID interface{}) context.Context { - ctx = context.WithValue(ctx, trust_v1.CustomDomainOrganizationIDKey, organizationID) - return ctx -} - func (s *Server) stripTrustPrefix(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { slugOrId := chi.URLParam(r, "slugOrId") diff --git a/pkg/trust/trust_center_access_service.go b/pkg/trust/trust_center_access_service.go index df268403d..a25c6a348 100644 --- a/pkg/trust/trust_center_access_service.go +++ b/pkg/trust/trust_center_access_service.go @@ -44,7 +44,7 @@ type ( TrustCenterAccessRequest struct { TrustCenterID gid.GID Email mail.Addr - Name string + FullName string DocumentIDs []gid.GID ReportIDs []gid.GID TrustCenterFileIDs []gid.GID @@ -63,26 +63,6 @@ func (tcar *TrustCenterAccessRequest) Validate() error { return v.Error() } -func (s TrustCenterAccessService) ValidateToken( - ctx context.Context, - trustCenterID gid.GID, - email mail.Addr, -) error { - return s.svc.pg.WithConn(ctx, func(conn pg.Conn) error { - access := &coredata.TrustCenterAccess{} - err := access.LoadByTrustCenterIDAndEmail(ctx, conn, s.svc.scope, trustCenterID, email) - if err != nil { - return fmt.Errorf("cannot load trust center access: %w", err) - } - - if !access.Active { - return fmt.Errorf("trust center access is not active") - } - - return nil - }) -} - func (s TrustCenterAccessService) Request( ctx context.Context, req *TrustCenterAccessRequest, @@ -170,7 +150,7 @@ func (s TrustCenterAccessService) Request( TenantID: s.svc.scope.GetTenantID(), TrustCenterID: req.TrustCenterID, Email: req.Email, - Name: req.Name, + Name: req.FullName, Active: false, HasAcceptedNonDisclosureAgreement: false, CreatedAt: now,