From 8b3bda56e640fd914e78e6a233b7b99525f67c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Thu, 15 Jan 2026 12:31:10 +0400 Subject: [PATCH] Add magic link login for trust center MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- apps/trust/src/components/AuditRow.tsx | 115 +++- apps/trust/src/components/DocumentRow.tsx | 114 +++- apps/trust/src/components/MagicLinkDialog.tsx | 122 ++++ .../src/components/OrganizationSidebar.tsx | 76 ++- .../src/components/RequestAccessDialog.tsx | 273 --------- .../src/components/TrustCenterFileRow.tsx | 124 +++- .../AuditRow_requestAccessMutation.graphql.ts | 105 ++++ ...cumentRow_requestAccessMutation.graphql.ts | 105 ++++ .../MagicLinkDialogMutation.graphql.ts | 92 +++ ...ebar_requestAllAccessesMutation.graphql.ts | 87 +++ ...estAccessDialogDocumentMutation.graphql.ts | 4 +- .../RequestAccessDialogMutation.graphql.ts | 39 +- ...questAccessDialogReportMutation.graphql.ts | 4 +- ...ssDialogTrustCenterFileMutation.graphql.ts | 4 +- ...erFileRow_requestAccessMutation.graphql.ts | 105 ++++ apps/trust/src/pages/AccessPage.tsx | 76 --- apps/trust/src/pages/auth/AuthLayout.tsx | 19 + .../src/pages/auth/VerifyMagicLinkPage.tsx | 119 ++++ .../VerifyMagicLinkPageMutation.graphql.ts | 92 +++ apps/trust/src/providers/RelayProviders.tsx | 33 +- apps/trust/src/routes.tsx | 34 +- apps/trust/src/utils/pathPrefix.ts | 15 + apps/trust/vite.config.ts | 1 + packages/emails/emails.go | 11 +- packages/emails/scripts/build.ts | 3 +- pkg/baseurl/baseurl.go | 27 +- pkg/certmanager/provisioner.go | 18 +- pkg/coredata/custom_domain.go | 65 ++ pkg/coredata/migrations/20260113T220012Z.sql | 3 + pkg/coredata/session.go | 5 +- pkg/iam/auth_service.go | 55 +- pkg/server/api/api.go | 1 + pkg/server/api/connect/v1/schema/schema.go | 73 ++- pkg/server/api/console/v1/v1_resolver.go | 2 +- pkg/server/api/trust/v1/resolver.go | 4 + pkg/server/api/trust/v1/schema.graphql | 39 +- pkg/server/api/trust/v1/schema/schema.go | 567 ++++++++---------- pkg/server/api/trust/v1/types/types.go | 31 +- pkg/server/api/trust/v1/v1_resolver.go | 174 +++--- pkg/server/gqlutils/errors.go | 22 +- pkg/server/server.go | 2 + pkg/trust/service.go | 13 + 42 files changed, 1880 insertions(+), 993 deletions(-) create mode 100644 apps/trust/src/components/MagicLinkDialog.tsx delete mode 100644 apps/trust/src/components/RequestAccessDialog.tsx create mode 100644 apps/trust/src/components/__generated__/AuditRow_requestAccessMutation.graphql.ts create mode 100644 apps/trust/src/components/__generated__/DocumentRow_requestAccessMutation.graphql.ts create mode 100644 apps/trust/src/components/__generated__/MagicLinkDialogMutation.graphql.ts create mode 100644 apps/trust/src/components/__generated__/OrganizationSidebar_requestAllAccessesMutation.graphql.ts create mode 100644 apps/trust/src/components/__generated__/TrustCenterFileRow_requestAccessMutation.graphql.ts delete mode 100644 apps/trust/src/pages/AccessPage.tsx create mode 100644 apps/trust/src/pages/auth/AuthLayout.tsx create mode 100644 apps/trust/src/pages/auth/VerifyMagicLinkPage.tsx create mode 100644 apps/trust/src/pages/auth/__generated__/VerifyMagicLinkPageMutation.graphql.ts create mode 100644 apps/trust/src/utils/pathPrefix.ts create mode 100644 pkg/coredata/migrations/20260113T220012Z.sql diff --git a/apps/trust/src/components/AuditRow.tsx b/apps/trust/src/components/AuditRow.tsx index 53546858b..251f8539c 100644 --- a/apps/trust/src/components/AuditRow.tsx +++ b/apps/trust/src/components/AuditRow.tsx @@ -1,6 +1,6 @@ import { graphql } from "relay-runtime"; import type { AuditRowFragment$key } from "./__generated__/AuditRowFragment.graphql"; -import { useFragment } from "react-relay"; +import { useFragment, useMutation } from "react-relay"; import { Breadcrumb, Button, @@ -12,14 +12,27 @@ import { IconMedal, Spinner, Table, + useToast, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import type { AuditRowDownloadMutation } from "./__generated__/AuditRowDownloadMutation.graphql"; import { useMutationWithToasts } from "/hooks/useMutationWithToast"; -import { downloadFile } from "@probo/helpers"; -import { type PropsWithChildren, useState } from "react"; +import { downloadFile, formatError } from "@probo/helpers"; +import { type PropsWithChildren, use, useState } from "react"; import { useLocation } from "react-router"; -import { RequestAccessDialog } from "/components/RequestAccessDialog.tsx"; +import { Viewer } from "/providers/Viewer"; +import { MagicLinkDialog } from "./MagicLinkDialog"; +import type { AuditRow_requestAccessMutation } from "./__generated__/AuditRow_requestAccessMutation.graphql"; + +const requestAccessMutation = graphql` + mutation AuditRow_requestAccessMutation($input: RequestReportAccessInput!) { + requestReportAccess(input: $input) { + trustCenterAccess { + id + } + } + } +`; const downloadMutation = graphql` mutation AuditRowDownloadMutation($input: ExportReportPDFInput!) { @@ -47,10 +60,53 @@ const auditRowFragment = graphql` `; export function AuditRow(props: { audit: AuditRowFragment$key }) { - const audit = useFragment(auditRowFragment, props.audit); const { __ } = useTranslate(); + const viewer = use(Viewer); + const { toast } = useToast(); + + const audit = useFragment(auditRowFragment, props.audit); + const [hasRequested, setHasRequested] = useState( + audit.report?.hasUserRequestedAccess, + ); + + const [requestAccess, isRequestingAccess] = + useMutation(requestAccessMutation); const [commitDownload, downloading] = useMutationWithToasts(downloadMutation); + + const handleRequestAccess = () => { + requestAccess({ + variables: { + input: { + reportId: audit.report?.id ?? "", + }, + }, + onCompleted: (_, errors) => { + if (errors?.length) { + toast({ + title: __("Error"), + description: formatError(__("Cannot request access"), errors), + variant: "error", + }); + return; + } + setHasRequested(true); + toast({ + title: __("Success"), + description: __("Access request submitted successfully."), + variant: "success", + }); + }, + onError: (error) => { + toast({ + title: __("Error"), + description: error.message ?? __("Cannot request access"), + variant: "error", + }); + }, + }); + }; + const handleDownload = () => { if (!audit.report?.id) { return; @@ -67,41 +123,46 @@ export function AuditRow(props: { audit: AuditRowFragment$key }) { }); }; - const [hasRequested, setHasRequested] = useState( - audit.report?.hasUserRequestedAccess - ); return (
{audit.framework.name}
- {audit.report && audit.report.isUserAuthorized && ( - - )} - {audit.report && !audit.report.isUserAuthorized && ( - setHasRequested(true)} - > + {!viewer && ( + - + )} + {viewer && + audit.report && + (audit.report.isUserAuthorized ? ( + + ) : ( + + ))}
); } @@ -134,7 +195,7 @@ export function AuditRowAvatar(props: { audit: AuditRowFragment$key }) { } function AuditDialog( - props: PropsWithChildren<{ audit: AuditRowFragment$key; logo?: string }> + props: PropsWithChildren<{ audit: AuditRowFragment$key; logo?: string }>, ) { const audit = useFragment(auditRowFragment, props.audit); const location = useLocation(); diff --git a/apps/trust/src/components/DocumentRow.tsx b/apps/trust/src/components/DocumentRow.tsx index 01b61a8e6..bb16e8030 100644 --- a/apps/trust/src/components/DocumentRow.tsx +++ b/apps/trust/src/components/DocumentRow.tsx @@ -1,19 +1,34 @@ import { graphql } from "relay-runtime"; import type { DocumentRowFragment$key } from "./__generated__/DocumentRowFragment.graphql"; -import { useFragment } from "react-relay"; +import { useFragment, useMutation } from "react-relay"; import { Button, IconArrowInbox, IconLock, IconPageTextLine, Spinner, + useToast, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import type { DocumentRowDownloadMutation } from "./__generated__/DocumentRowDownloadMutation.graphql"; import { useMutationWithToasts } from "/hooks/useMutationWithToast"; -import { downloadFile } from "@probo/helpers"; -import { RequestAccessDialog } from "/components/RequestAccessDialog.tsx"; -import { useState } from "react"; +import { downloadFile, formatError } from "@probo/helpers"; +import { use, useState } from "react"; +import { MagicLinkDialog } from "./MagicLinkDialog"; +import { Viewer } from "/providers/Viewer"; +import type { DocumentRow_requestAccessMutation } from "./__generated__/DocumentRow_requestAccessMutation.graphql"; + +const requestAccessMutation = graphql` + mutation DocumentRow_requestAccessMutation( + $input: RequestDocumentAccessInput! + ) { + requestDocumentAccess(input: $input) { + trustCenterAccess { + id + } + } + } +`; const downloadMutation = graphql` mutation DocumentRowDownloadMutation($input: ExportDocumentPDFInput!) { @@ -33,10 +48,53 @@ const documentRowFragment = graphql` `; export function DocumentRow(props: { document: DocumentRowFragment$key }) { - const document = useFragment(documentRowFragment, props.document); const { __ } = useTranslate(); + const viewer = use(Viewer); + const { toast } = useToast(); + + const document = useFragment(documentRowFragment, props.document); + const [hasRequested, setHasRequested] = useState( + document.hasUserRequestedAccess, + ); + + const [requestAccess, isRequestingAccess] = + useMutation(requestAccessMutation); const [commitDownload, downloading] = useMutationWithToasts(downloadMutation); + + const handleRequestAccess = () => { + requestAccess({ + variables: { + input: { + documentId: document.id, + }, + }, + onCompleted: (_, errors) => { + if (errors?.length) { + toast({ + title: __("Error"), + description: formatError(__("Cannot request access"), errors), + variant: "error", + }); + return; + } + setHasRequested(true); + toast({ + title: __("Success"), + description: __("Access request submitted successfully."), + variant: "success", + }); + }, + onError: (error) => { + toast({ + title: __("Error"), + description: error.message ?? __("Cannot request access"), + variant: "error", + }); + }, + }); + }; + const handleDownload = () => { commitDownload({ variables: { @@ -49,40 +107,46 @@ export function DocumentRow(props: { document: DocumentRowFragment$key }) { }, }); }; - const [hasRequested, setHasRequested] = useState( - document.hasUserRequestedAccess, - ); + return (
{document.title}
- {document.isUserAuthorized ? ( - - ) : ( - setHasRequested(true)} - > + {!viewer && ( + - + )} + {viewer && + (document.isUserAuthorized ? ( + + ) : ( + + ))}
); } diff --git a/apps/trust/src/components/MagicLinkDialog.tsx b/apps/trust/src/components/MagicLinkDialog.tsx new file mode 100644 index 000000000..d79e19ac2 --- /dev/null +++ b/apps/trust/src/components/MagicLinkDialog.tsx @@ -0,0 +1,122 @@ +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogTitle, + Field, + useDialogRef, + useToast, +} from "@probo/ui"; +import { useMutation } from "react-relay"; +import { graphql } from "relay-runtime"; +import z from "zod"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import { useTranslate } from "@probo/i18n"; +import type { MagicLinkDialogMutation } from "./__generated__/MagicLinkDialogMutation.graphql"; +import type { PropsWithChildren } from "react"; + +const sendMagicLinkMutation = graphql` + mutation MagicLinkDialogMutation($input: SendMagicLinkInput!) { + sendMagicLink(input: $input) { + success + } + } +`; + +const schema = z.object({ + email: z.string().email(), +}); + +type FormData = z.infer; + +export function MagicLinkDialog(props: PropsWithChildren) { + const { children } = props; + + const { __ } = useTranslate(); + const { toast } = useToast(); + const dialogRef = useDialogRef(); + + const [sendMagicLink] = useMutation( + sendMagicLinkMutation, + ); + + const { + handleSubmit: handleSubmitWrapper, + register, + formState, + } = useFormWithSchema(schema, { + defaultValues: { + email: "", + }, + }); + + const handleSubmit = ({ email }: FormData) => { + sendMagicLink({ + variables: { + input: { + email, + }, + }, + onCompleted: (_, errors) => { + if (errors?.length) { + toast({ + title: __("Error"), + description: __("Cannot send magic link"), + variant: "error", + }); + return; + } + + toast({ + title: __("Success"), + description: __( + "Magic link sent! Please check your emails to authenticate", + ), + variant: "success", + }); + dialogRef.current?.close(); + }, + onError: (error) => { + toast({ + title: __("Error"), + description: error.message, + variant: "error", + }); + }, + }); + }; + + return ( + +
+ + {__("Create account or sign in")} + + +

+ {__( + "To be able to request access to documents, you need to authenticate. Please enter your email to get a magic link.", + )} +

+ +
+ + + +
+
+ ); +} diff --git a/apps/trust/src/components/OrganizationSidebar.tsx b/apps/trust/src/components/OrganizationSidebar.tsx index ae674616c..d663de99b 100644 --- a/apps/trust/src/components/OrganizationSidebar.tsx +++ b/apps/trust/src/components/OrganizationSidebar.tsx @@ -1,11 +1,31 @@ import { useTranslate } from "@probo/i18n"; -import { Button, Card, IconBlock, IconLock, IconMedal } from "@probo/ui"; +import { + Button, + Card, + IconBlock, + IconLock, + IconMedal, + useToast, +} from "@probo/ui"; import type { TrustGraphQuery$data } from "/queries/__generated__/TrustGraphQuery.graphql"; import { use, type PropsWithChildren } from "react"; -import { domain } from "@probo/helpers"; +import { domain, formatError } from "@probo/helpers"; import { AuditRowAvatar } from "./AuditRow"; -import { RequestAccessDialog } from "./RequestAccessDialog"; import { Viewer } from "/providers/Viewer"; +import { MagicLinkDialog } from "./MagicLinkDialog"; +import { graphql } from "relay-runtime"; +import { useMutation } from "react-relay"; +import type { OrganizationSidebar_requestAllAccessesMutation } from "./__generated__/OrganizationSidebar_requestAllAccessesMutation.graphql"; + +const requestAllAccessesMutation = graphql` + mutation OrganizationSidebar_requestAllAccessesMutation { + requestAllAccesses { + trustCenterAccess { + id + } + } + } +`; export function OrganizationSidebar({ trustCenter, @@ -14,6 +34,40 @@ export function OrganizationSidebar({ }) { const { __ } = useTranslate(); const isAuthenticated = !!use(Viewer); + const { toast } = useToast(); + + const [requestAllAccesses, isRequestingAccess] = + useMutation( + requestAllAccessesMutation, + ); + + const handleRequestAllAccesses = () => { + requestAllAccesses({ + variables: {}, + onCompleted: (_, errors) => { + if (errors?.length) { + toast({ + title: __("Error"), + description: formatError(__("Cannot request access"), errors), + variant: "error", + }); + return; + } + toast({ + title: __("Success"), + description: __("Access request submitted successfully."), + variant: "success", + }); + }, + onError: (error) => { + toast({ + title: __("Error"), + description: error.message ?? __("Cannot request access"), + variant: "error", + }); + }, + }); + }; if (!trustCenter) { return null; @@ -95,12 +149,22 @@ export function OrganizationSidebar({ )} {/* Actions */} - {!isAuthenticated && ( - + {isAuthenticated ? ( + + ) : ( + - + )} {/* - - - - ); -} - -const requestAccessMutation = graphql` - mutation RequestAccessDialogMutation($input: RequestAllAccessesInput!) { - requestAllAccesses(input: $input) { - trustCenterAccess { - id - } - } - } -`; - -const requestDocumentAccessMutation = graphql` - mutation RequestAccessDialogDocumentMutation( - $input: RequestDocumentAccessInput! - ) { - requestDocumentAccess(input: $input) { - trustCenterAccess { - id - } - } - } -`; - -const requestReportAccessMutation = graphql` - mutation RequestAccessDialogReportMutation( - $input: RequestReportAccessInput! - ) { - requestReportAccess(input: $input) { - trustCenterAccess { - id - } - } - } -`; - -const requestTrustCenterFileAccessMutation = graphql` - mutation RequestAccessDialogTrustCenterFileMutation( - $input: RequestTrustCenterFileAccessInput! - ) { - requestTrustCenterFileAccess(input: $input) { - trustCenterAccess { - id - } - } - } -`; - -/** - * Use the correct mutation using the shape - */ -function useMutation({ - documentId, - reportId, - trustCenterFileId, -}: Pick): [ - (data: z.infer | null) => Promise, - boolean, -] { - const trustCenter = useTrustCenter(); - const [commitRequestAccess, isRequestingAccess] = useMutationWithToasts( - requestAccessMutation, - ); - const [commitRequestDocumentAccess, isRequestingDocumentAccess] = - useMutationWithToasts(requestDocumentAccessMutation); - const [commitRequestReportAccess, isRequestingReportAccess] = - useMutationWithToasts(requestReportAccessMutation); - const [ - commitRequestTrustCenterFileAccess, - isRequestingTrustCenterFileAccess, - ] = useMutationWithToasts(requestTrustCenterFileAccessMutation); - - if (trustCenterFileId) { - return [ - (data) => { - return commitRequestTrustCenterFileAccess({ - variables: { - input: { - trustCenterId: trustCenter.id, - trustCenterFileId: trustCenterFileId, - ...data, - }, - }, - }); - }, - isRequestingTrustCenterFileAccess, - ]; - } else if (reportId) { - return [ - (data) => { - return commitRequestReportAccess({ - variables: { - input: { - trustCenterId: trustCenter.id, - reportId: reportId, - ...data, - }, - }, - }); - }, - isRequestingReportAccess, - ]; - } else if (documentId) { - return [ - (data) => { - return commitRequestDocumentAccess({ - variables: { - input: { - trustCenterId: trustCenter.id, - documentId: documentId, - ...data, - }, - }, - }); - }, - isRequestingDocumentAccess, - ]; - } else { - return [ - (data) => { - return commitRequestAccess({ - variables: { - input: { - trustCenterId: trustCenter.id, - ...data, - }, - }, - }); - }, - isRequestingAccess, - ]; - } -} diff --git a/apps/trust/src/components/TrustCenterFileRow.tsx b/apps/trust/src/components/TrustCenterFileRow.tsx index 0aebb8925..9127be2b9 100644 --- a/apps/trust/src/components/TrustCenterFileRow.tsx +++ b/apps/trust/src/components/TrustCenterFileRow.tsx @@ -1,22 +1,39 @@ import { graphql } from "relay-runtime"; import type { TrustCenterFileRowFragment$key } from "./__generated__/TrustCenterFileRowFragment.graphql"; -import { useFragment } from "react-relay"; +import { useFragment, useMutation } from "react-relay"; import { Button, IconArrowInbox, IconLock, IconPageTextLine, Spinner, + useToast, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import type { TrustCenterFileRowDownloadMutation } from "./__generated__/TrustCenterFileRowDownloadMutation.graphql"; import { useMutationWithToasts } from "/hooks/useMutationWithToast"; -import { downloadFile } from "@probo/helpers"; -import { RequestAccessDialog } from "/components/RequestAccessDialog.tsx"; -import { useState } from "react"; +import { downloadFile, formatError } from "@probo/helpers"; +import { use, useState } from "react"; +import { Viewer } from "/providers/Viewer"; +import { MagicLinkDialog } from "./MagicLinkDialog"; +import type { TrustCenterFileRow_requestAccessMutation } from "./__generated__/TrustCenterFileRow_requestAccessMutation.graphql"; + +const requestAccessMutation = graphql` + mutation TrustCenterFileRow_requestAccessMutation( + $input: RequestTrustCenterFileAccessInput! + ) { + requestTrustCenterFileAccess(input: $input) { + trustCenterAccess { + id + } + } + } +`; const downloadMutation = graphql` - mutation TrustCenterFileRowDownloadMutation($input: ExportTrustCenterFileInput!) { + mutation TrustCenterFileRowDownloadMutation( + $input: ExportTrustCenterFileInput! + ) { exportTrustCenterFile(input: $input) { data } @@ -32,11 +49,56 @@ const trustCenterFileRowFragment = graphql` } `; -export function TrustCenterFileRow(props: { file: TrustCenterFileRowFragment$key }) { - const file = useFragment(trustCenterFileRowFragment, props.file); +export function TrustCenterFileRow(props: { + file: TrustCenterFileRowFragment$key; +}) { const { __ } = useTranslate(); + const viewer = use(Viewer); + const { toast } = useToast(); + + const file = useFragment(trustCenterFileRowFragment, props.file); + const [hasRequested, setHasRequested] = useState(file.hasUserRequestedAccess); + + const [requestAccess, isRequestingAccess] = + useMutation( + requestAccessMutation, + ); const [commitDownload, downloading] = useMutationWithToasts(downloadMutation); + + const handleRequestAccess = () => { + requestAccess({ + variables: { + input: { + trustCenterFileId: file.id, + }, + }, + onCompleted: (_, errors) => { + if (errors?.length) { + toast({ + title: __("Error"), + description: formatError(__("Cannot request access"), errors), + variant: "error", + }); + return; + } + setHasRequested(true); + toast({ + title: __("Success"), + description: __("Access request submitted successfully."), + variant: "success", + }); + }, + onError: (error) => { + toast({ + title: __("Error"), + description: error.message ?? __("Cannot request access"), + variant: "error", + }); + }, + }); + }; + const handleDownload = () => { commitDownload({ variables: { @@ -49,40 +111,46 @@ export function TrustCenterFileRow(props: { file: TrustCenterFileRowFragment$key }, }); }; - const [hasRequested, setHasRequested] = useState( - file.hasUserRequestedAccess, - ); + return ( -
+
{file.name}
- {file.isUserAuthorized ? ( - - ) : ( - setHasRequested(true)} - > + {!viewer && ( + - + )} + {viewer && + (file.isUserAuthorized ? ( + + ) : ( + + ))}
); } diff --git a/apps/trust/src/components/__generated__/AuditRow_requestAccessMutation.graphql.ts b/apps/trust/src/components/__generated__/AuditRow_requestAccessMutation.graphql.ts new file mode 100644 index 000000000..56b768015 --- /dev/null +++ b/apps/trust/src/components/__generated__/AuditRow_requestAccessMutation.graphql.ts @@ -0,0 +1,105 @@ +/** + * @generated SignedSource<<0e5c234b565a3031a774995bff249b97>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type RequestReportAccessInput = { + reportId: string; +}; +export type AuditRow_requestAccessMutation$variables = { + input: RequestReportAccessInput; +}; +export type AuditRow_requestAccessMutation$data = { + readonly requestReportAccess: { + readonly trustCenterAccess: { + readonly id: string; + }; + }; +}; +export type AuditRow_requestAccessMutation = { + response: AuditRow_requestAccessMutation$data; + variables: AuditRow_requestAccessMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "RequestAccessesPayload", + "kind": "LinkedField", + "name": "requestReportAccess", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "TrustCenterAccess", + "kind": "LinkedField", + "name": "trustCenterAccess", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "AuditRow_requestAccessMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "AuditRow_requestAccessMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "6760f1ec8a6990f62461c3fab2b2934a", + "id": null, + "metadata": {}, + "name": "AuditRow_requestAccessMutation", + "operationKind": "mutation", + "text": "mutation AuditRow_requestAccessMutation(\n $input: RequestReportAccessInput!\n) {\n requestReportAccess(input: $input) {\n trustCenterAccess {\n id\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "6ab5eb701126385e717a381e21e55ae0"; + +export default node; diff --git a/apps/trust/src/components/__generated__/DocumentRow_requestAccessMutation.graphql.ts b/apps/trust/src/components/__generated__/DocumentRow_requestAccessMutation.graphql.ts new file mode 100644 index 000000000..adf8814d6 --- /dev/null +++ b/apps/trust/src/components/__generated__/DocumentRow_requestAccessMutation.graphql.ts @@ -0,0 +1,105 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type RequestDocumentAccessInput = { + documentId: string; +}; +export type DocumentRow_requestAccessMutation$variables = { + input: RequestDocumentAccessInput; +}; +export type DocumentRow_requestAccessMutation$data = { + readonly requestDocumentAccess: { + readonly trustCenterAccess: { + readonly id: string; + }; + }; +}; +export type DocumentRow_requestAccessMutation = { + response: DocumentRow_requestAccessMutation$data; + variables: DocumentRow_requestAccessMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "RequestAccessesPayload", + "kind": "LinkedField", + "name": "requestDocumentAccess", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "TrustCenterAccess", + "kind": "LinkedField", + "name": "trustCenterAccess", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "DocumentRow_requestAccessMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "DocumentRow_requestAccessMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "13715b7cb8784d050b6d0381e460bbad", + "id": null, + "metadata": {}, + "name": "DocumentRow_requestAccessMutation", + "operationKind": "mutation", + "text": "mutation DocumentRow_requestAccessMutation(\n $input: RequestDocumentAccessInput!\n) {\n requestDocumentAccess(input: $input) {\n trustCenterAccess {\n id\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "ca9026b04c2de91067759e3a80e5c276"; + +export default node; diff --git a/apps/trust/src/components/__generated__/MagicLinkDialogMutation.graphql.ts b/apps/trust/src/components/__generated__/MagicLinkDialogMutation.graphql.ts new file mode 100644 index 000000000..21fc17849 --- /dev/null +++ b/apps/trust/src/components/__generated__/MagicLinkDialogMutation.graphql.ts @@ -0,0 +1,92 @@ +/** + * @generated SignedSource<<1d6509148442dad28e2af20068216f49>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type SendMagicLinkInput = { + email: any; +}; +export type MagicLinkDialogMutation$variables = { + input: SendMagicLinkInput; +}; +export type MagicLinkDialogMutation$data = { + readonly sendMagicLink: { + readonly success: boolean; + } | null | undefined; +}; +export type MagicLinkDialogMutation = { + response: MagicLinkDialogMutation$data; + variables: MagicLinkDialogMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "SendMagicLinkPayload", + "kind": "LinkedField", + "name": "sendMagicLink", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "success", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "MagicLinkDialogMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "MagicLinkDialogMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "d0e02db0cec956d7a21f5ad5a5a69d61", + "id": null, + "metadata": {}, + "name": "MagicLinkDialogMutation", + "operationKind": "mutation", + "text": "mutation MagicLinkDialogMutation(\n $input: SendMagicLinkInput!\n) {\n sendMagicLink(input: $input) {\n success\n }\n}\n" + } +}; +})(); + +(node as any).hash = "c1ee1fe3ef7c232fcb43b5cf8e56c2de"; + +export default node; diff --git a/apps/trust/src/components/__generated__/OrganizationSidebar_requestAllAccessesMutation.graphql.ts b/apps/trust/src/components/__generated__/OrganizationSidebar_requestAllAccessesMutation.graphql.ts new file mode 100644 index 000000000..5c1aa93dd --- /dev/null +++ b/apps/trust/src/components/__generated__/OrganizationSidebar_requestAllAccessesMutation.graphql.ts @@ -0,0 +1,87 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type OrganizationSidebar_requestAllAccessesMutation$variables = Record; +export type OrganizationSidebar_requestAllAccessesMutation$data = { + readonly requestAllAccesses: { + readonly trustCenterAccess: { + readonly id: string; + }; + }; +}; +export type OrganizationSidebar_requestAllAccessesMutation = { + response: OrganizationSidebar_requestAllAccessesMutation$data; + variables: OrganizationSidebar_requestAllAccessesMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "alias": null, + "args": null, + "concreteType": "RequestAccessesPayload", + "kind": "LinkedField", + "name": "requestAllAccesses", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "TrustCenterAccess", + "kind": "LinkedField", + "name": "trustCenterAccess", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "OrganizationSidebar_requestAllAccessesMutation", + "selections": (v0/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "OrganizationSidebar_requestAllAccessesMutation", + "selections": (v0/*: any*/) + }, + "params": { + "cacheID": "a298b2a1c2f62300ae5056743d2c2ec3", + "id": null, + "metadata": {}, + "name": "OrganizationSidebar_requestAllAccessesMutation", + "operationKind": "mutation", + "text": "mutation OrganizationSidebar_requestAllAccessesMutation {\n requestAllAccesses {\n trustCenterAccess {\n id\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "a22225757510c4dd097e99dcd3c066a6"; + +export default node; diff --git a/apps/trust/src/components/__generated__/RequestAccessDialogDocumentMutation.graphql.ts b/apps/trust/src/components/__generated__/RequestAccessDialogDocumentMutation.graphql.ts index adaea7f61..b1aef2ba9 100644 --- a/apps/trust/src/components/__generated__/RequestAccessDialogDocumentMutation.graphql.ts +++ b/apps/trust/src/components/__generated__/RequestAccessDialogDocumentMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<9f01a480ccc5fa00e310296e9aed1020>> + * @generated SignedSource<<2ea33498f7101d861d940b9c52ed3283>> * @lightSyntaxTransform * @nogrep */ @@ -11,8 +11,6 @@ import { ConcreteRequest } from 'relay-runtime'; export type RequestDocumentAccessInput = { documentId: string; - email: any; - fullName: string; }; export type RequestAccessDialogDocumentMutation$variables = { input: RequestDocumentAccessInput; diff --git a/apps/trust/src/components/__generated__/RequestAccessDialogMutation.graphql.ts b/apps/trust/src/components/__generated__/RequestAccessDialogMutation.graphql.ts index 5276cccf0..61cd80bd5 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<<0777fa7e6e3d0c75eedfc8380d9965a4>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -9,13 +9,7 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; -export type RequestAllAccessesInput = { - email: any; - fullName: string; -}; -export type RequestAccessDialogMutation$variables = { - input: RequestAllAccessesInput; -}; +export type RequestAccessDialogMutation$variables = Record; export type RequestAccessDialogMutation$data = { readonly requestAllAccesses: { readonly trustCenterAccess: { @@ -30,22 +24,9 @@ export type RequestAccessDialogMutation = { const node: ConcreteRequest = (function(){ var v0 = [ - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "input" - } -], -v1 = [ { "alias": null, - "args": [ - { - "kind": "Variable", - "name": "input", - "variableName": "input" - } - ], + "args": null, "concreteType": "RequestAccessesPayload", "kind": "LinkedField", "name": "requestAllAccesses", @@ -75,32 +56,32 @@ v1 = [ ]; return { "fragment": { - "argumentDefinitions": (v0/*: any*/), + "argumentDefinitions": [], "kind": "Fragment", "metadata": null, "name": "RequestAccessDialogMutation", - "selections": (v1/*: any*/), + "selections": (v0/*: any*/), "type": "Mutation", "abstractKey": null }, "kind": "Request", "operation": { - "argumentDefinitions": (v0/*: any*/), + "argumentDefinitions": [], "kind": "Operation", "name": "RequestAccessDialogMutation", - "selections": (v1/*: any*/) + "selections": (v0/*: any*/) }, "params": { - "cacheID": "99eb2902ce5a921515d68db30f8a2189", + "cacheID": "b48847e88c57289efc120745c743c8f6", "id": null, "metadata": {}, "name": "RequestAccessDialogMutation", "operationKind": "mutation", - "text": "mutation RequestAccessDialogMutation(\n $input: RequestAllAccessesInput!\n) {\n requestAllAccesses(input: $input) {\n trustCenterAccess {\n id\n }\n }\n}\n" + "text": "mutation RequestAccessDialogMutation {\n requestAllAccesses {\n trustCenterAccess {\n id\n }\n }\n}\n" } }; })(); -(node as any).hash = "cc4d5c8753438eff23833b4182dd485d"; +(node as any).hash = "44c3538831968b7146fbf2c9e027a778"; export default node; diff --git a/apps/trust/src/components/__generated__/RequestAccessDialogReportMutation.graphql.ts b/apps/trust/src/components/__generated__/RequestAccessDialogReportMutation.graphql.ts index 52884cadf..59fb271e9 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<<6fde198cd3eaa998d665639c48f5e630>> + * @generated SignedSource<<3e5433cc33d54bff9511d01eb4c65b00>> * @lightSyntaxTransform * @nogrep */ @@ -10,8 +10,6 @@ import { ConcreteRequest } from 'relay-runtime'; export type RequestReportAccessInput = { - email: any; - fullName: string; reportId: string; }; export type RequestAccessDialogReportMutation$variables = { diff --git a/apps/trust/src/components/__generated__/RequestAccessDialogTrustCenterFileMutation.graphql.ts b/apps/trust/src/components/__generated__/RequestAccessDialogTrustCenterFileMutation.graphql.ts index 3870c54e3..3770d5701 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<<7c329901bcf9bbf05b1899356016b151>> + * @generated SignedSource<<447bd664ebefbd6a672ccde86c63c028>> * @lightSyntaxTransform * @nogrep */ @@ -10,8 +10,6 @@ import { ConcreteRequest } from 'relay-runtime'; export type RequestTrustCenterFileAccessInput = { - email: any; - fullName: string; trustCenterFileId: string; }; export type RequestAccessDialogTrustCenterFileMutation$variables = { diff --git a/apps/trust/src/components/__generated__/TrustCenterFileRow_requestAccessMutation.graphql.ts b/apps/trust/src/components/__generated__/TrustCenterFileRow_requestAccessMutation.graphql.ts new file mode 100644 index 000000000..44ffac5a0 --- /dev/null +++ b/apps/trust/src/components/__generated__/TrustCenterFileRow_requestAccessMutation.graphql.ts @@ -0,0 +1,105 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type RequestTrustCenterFileAccessInput = { + trustCenterFileId: string; +}; +export type TrustCenterFileRow_requestAccessMutation$variables = { + input: RequestTrustCenterFileAccessInput; +}; +export type TrustCenterFileRow_requestAccessMutation$data = { + readonly requestTrustCenterFileAccess: { + readonly trustCenterAccess: { + readonly id: string; + }; + }; +}; +export type TrustCenterFileRow_requestAccessMutation = { + response: TrustCenterFileRow_requestAccessMutation$data; + variables: TrustCenterFileRow_requestAccessMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "RequestAccessesPayload", + "kind": "LinkedField", + "name": "requestTrustCenterFileAccess", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "TrustCenterAccess", + "kind": "LinkedField", + "name": "trustCenterAccess", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "TrustCenterFileRow_requestAccessMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "TrustCenterFileRow_requestAccessMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "77a09810aa9c3ddf3a3baddf709f9a42", + "id": null, + "metadata": {}, + "name": "TrustCenterFileRow_requestAccessMutation", + "operationKind": "mutation", + "text": "mutation TrustCenterFileRow_requestAccessMutation(\n $input: RequestTrustCenterFileAccessInput!\n) {\n requestTrustCenterFileAccess(input: $input) {\n trustCenterAccess {\n id\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "eba04489de9ec18ab9816328a481ef33"; + +export default node; diff --git a/apps/trust/src/pages/AccessPage.tsx b/apps/trust/src/pages/AccessPage.tsx deleted file mode 100644 index 21b457ef0..000000000 --- a/apps/trust/src/pages/AccessPage.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useTranslate } from "@probo/i18n"; -import { useEffect, useState } from "react"; -import { useNavigate, useSearchParams } from "react-router"; -import { buildEndpoint } from "/providers/RelayProviders"; -import { PageError } from "/components/PageError"; -import { Spinner } from "@probo/ui"; - -/** - * Page requested with an access token to authenticate the user for the Trust center - */ -export function AccessPage() { - const { __ } = useTranslate(); - const [searchParams] = useSearchParams(); - const token = searchParams.get("token"); - const navigate = useNavigate(); - - const isValidRequest = !!token; - const [error, setError] = useState(() => { - if (!token) { - return __("Invalid access token"); - } - return null; - }); - - // Initiate an authentication attempt - useEffect(() => { - if (!isValidRequest) { - return; - } - fetch(buildEndpoint("/api/trust/v1/auth/authenticate"), { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - token, - }), - }) - .then((response) => { - // For invalid response throw an error - if (!response.ok) { - const defaultMessage = `HTTP ${response.status}: ${response.statusText}`; - return response - .json() - .then((json) => { - throw new Error(json.message ?? defaultMessage); - }) - .catch(() => { - throw new Error(defaultMessage); - }); - } - return response.json(); - }) - .then((data) => { - if (data.success) { - navigate("/overview"); - return; - } - throw new Error(data.message ?? __("Authentication failed")); - }) - .catch((error) => { - setError(error.message); - }); - }, [isValidRequest, token, __, navigate]); - - if (error) { - return ; - } - - return ( -
- - {__("Redirecting to trust center")} -
- ); -} diff --git a/apps/trust/src/pages/auth/AuthLayout.tsx b/apps/trust/src/pages/auth/AuthLayout.tsx new file mode 100644 index 000000000..b57160d7d --- /dev/null +++ b/apps/trust/src/pages/auth/AuthLayout.tsx @@ -0,0 +1,19 @@ +import { Logo } from "@probo/ui"; +import { Outlet } from "react-router"; + +export default function () { + return ( +
+
+
+ +
+
+
+
+ +
+
+
+ ); +} diff --git a/apps/trust/src/pages/auth/VerifyMagicLinkPage.tsx b/apps/trust/src/pages/auth/VerifyMagicLinkPage.tsx new file mode 100644 index 000000000..f093f69e8 --- /dev/null +++ b/apps/trust/src/pages/auth/VerifyMagicLinkPage.tsx @@ -0,0 +1,119 @@ +import { usePageTitle } from "@probo/hooks"; +import { useTranslate } from "@probo/i18n"; +import { Button, Field, useToast } from "@probo/ui"; +import { useSearchParams } from "react-router"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import z from "zod"; +import { useEffect, useRef } from "react"; +import { graphql } from "relay-runtime"; +import { useMutation } from "react-relay"; +import { formatError } from "@probo/helpers"; +import type { VerifyMagicLinkPageMutation } from "./__generated__/VerifyMagicLinkPageMutation.graphql"; +import { getPathPrefix } from "/utils/pathPrefix"; + +const verifyMagicLinkMutation = graphql` + mutation VerifyMagicLinkPageMutation($input: VerifyMagicLinkInput!) { + verifyMagicLink(input: $input) { + success + } + } +`; + +const verifyMagicLinkSchema = z.object({ + token: z.string().min(1, "Please enter a magic token"), +}); + +export default function VerifyMagicLinkPagePageMutation() { + const { __ } = useTranslate(); + const { toast } = useToast(); + const [searchParams] = useSearchParams(); + const submittedRef = useRef(false); + + usePageTitle(__("Verify Magic Link")); + + const form = useFormWithSchema(verifyMagicLinkSchema, { + defaultValues: { + token: searchParams.get("token") ?? "", + }, + }); + + const [verifyMagicLink] = useMutation( + verifyMagicLinkMutation, + ); + + const handleSubmit = form.handleSubmit(async (data) => { + verifyMagicLink({ + variables: { + input: { + token: data.token.trim(), + }, + }, + onCompleted: (_, errors) => { + if (errors) { + toast({ + title: __("Error"), + description: formatError(__("Failed to connect"), errors), + variant: "error", + }); + return; + } + + toast({ + title: __("Success"), + description: __("Your have successfully signed in"), + variant: "success", + }); + window.location.href = getPathPrefix(); + }, + onError: (err) => { + toast({ + title: __("Error"), + description: err.message, + variant: "error", + }); + }, + }); + }); + + useEffect(() => { + if (!submittedRef.current && searchParams.get("token")) { + handleSubmit(); + submittedRef.current = true; + } + }); + + return ( +
+
+

{__("Email Confirmation")}

+

+ {__("Confirm your email address to complete registration")} +

+
+ +
+ + + + +
+ ); +} diff --git a/apps/trust/src/pages/auth/__generated__/VerifyMagicLinkPageMutation.graphql.ts b/apps/trust/src/pages/auth/__generated__/VerifyMagicLinkPageMutation.graphql.ts new file mode 100644 index 000000000..47e328c65 --- /dev/null +++ b/apps/trust/src/pages/auth/__generated__/VerifyMagicLinkPageMutation.graphql.ts @@ -0,0 +1,92 @@ +/** + * @generated SignedSource<<5037d9722f3c6cc15c2a323ef954e3d6>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type VerifyMagicLinkInput = { + token: string; +}; +export type VerifyMagicLinkPageMutation$variables = { + input: VerifyMagicLinkInput; +}; +export type VerifyMagicLinkPageMutation$data = { + readonly verifyMagicLink: { + readonly success: boolean; + } | null | undefined; +}; +export type VerifyMagicLinkPageMutation = { + response: VerifyMagicLinkPageMutation$data; + variables: VerifyMagicLinkPageMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "VerifyMagicLinkPayload", + "kind": "LinkedField", + "name": "verifyMagicLink", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "success", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "VerifyMagicLinkPageMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "VerifyMagicLinkPageMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "07cf89de3f37725d847cda46557467f5", + "id": null, + "metadata": {}, + "name": "VerifyMagicLinkPageMutation", + "operationKind": "mutation", + "text": "mutation VerifyMagicLinkPageMutation(\n $input: VerifyMagicLinkInput!\n) {\n verifyMagicLink(input: $input) {\n success\n }\n}\n" + } +}; +})(); + +(node as any).hash = "074415601c4d50f50d177c06dfab64ef"; + +export default node; diff --git a/apps/trust/src/providers/RelayProviders.tsx b/apps/trust/src/providers/RelayProviders.tsx index 9530b4b12..3f5e8a77c 100644 --- a/apps/trust/src/providers/RelayProviders.tsx +++ b/apps/trust/src/providers/RelayProviders.tsx @@ -8,6 +8,7 @@ import { import { GraphQLError } from "graphql"; import type { PropsWithChildren } from "react"; import { RelayEnvironmentProvider } from "react-relay"; +import { getPathPrefix } from "/utils/pathPrefix"; export class UnAuthenticatedError extends Error { constructor() { @@ -35,11 +36,11 @@ export class InternalServerError extends Error { } } -export function buildEndpoint(path: string): string { - const host = import.meta.env.VITE_API_URL; +export function buildEndpoint(): string { + let host = import.meta.env.VITE_API_URL; if (!host) { - return path; + host = window.location.origin; } const formattedHost = @@ -49,10 +50,16 @@ export function buildEndpoint(path: string): string { const url = new URL(formattedHost); - if (path) { - url.pathname = path.startsWith("/") ? path : `/${path}`; + const prefix = getPathPrefix(); + let path: string; + if (prefix) { + path = `${prefix}/api/trust/v1/graphql`; + } else { + path = `/api/trust/v1/graphql`; } + url.pathname = path; + return url.toString(); } @@ -66,7 +73,7 @@ const fetchRelay: FetchFunction = async ( request, variables, _, - uploadables + uploadables, ) => { const requestInit: RequestInit = { method: "POST", @@ -82,7 +89,7 @@ const fetchRelay: FetchFunction = async ( operationName: request.name, query: request.text, variables: variables, - }) + }), ); const uploadableMap: { @@ -119,13 +126,7 @@ const fetchRelay: FetchFunction = async ( }); } - // Use relative API path to ensure it goes through the same routing context - // For /trust/slug/overview, this resolves to /trust/slug/api/trust/v1/graphql - // For custom domains at /overview, this resolves to /api/trust/v1/graphql - const response = await fetch( - buildEndpoint("./api/trust/v1/graphql"), - requestInit - ); + const response = await fetch(buildEndpoint(), requestInit); if (response.status === 500) { throw new InternalServerError(); @@ -144,8 +145,8 @@ const fetchRelay: FetchFunction = async ( if (invalidError) { throw new InvalidError( invalidError.message, - invalidError.extensions.field as string ?? "", - invalidError.extensions.cause as string ?? "", + (invalidError.extensions.field as string) ?? "", + (invalidError.extensions.cause as string) ?? "", ); } diff --git a/apps/trust/src/routes.tsx b/apps/trust/src/routes.tsx index b63841c19..25535c4a6 100644 --- a/apps/trust/src/routes.tsx +++ b/apps/trust/src/routes.tsx @@ -1,14 +1,6 @@ -import { - createBrowserRouter, - Navigate, - redirect, - useRouteError, -} from "react-router"; -import { Fragment } from "react"; -import { - consoleEnvironment, - UnAuthenticatedError, -} from "./providers/RelayProviders.tsx"; +import { createBrowserRouter, redirect, useRouteError } from "react-router"; +import { Fragment, lazy } from "react"; +import { consoleEnvironment } from "./providers/RelayProviders.tsx"; import { loadQuery } from "react-relay"; import { PageError } from "./components/PageError.tsx"; import { MainLayout } from "/layouts/MainLayout"; @@ -20,7 +12,6 @@ import { import { OverviewPage } from "/pages/OverviewPage"; import { DocumentsPage } from "/pages/DocumentsPage"; import { SubprocessorsPage } from "/pages/SubprocessorsPage"; -import { AccessPage } from "./pages/AccessPage.tsx"; import { TabSkeleton } from "./components/Skeletons/TabSkeleton.tsx"; import { MainSkeleton } from "./components/Skeletons/MainSkeleton.tsx"; import { @@ -36,14 +27,20 @@ import { function ErrorBoundary({ error: propsError }: { error?: string }) { const error = useRouteError() ?? propsError; - if (error instanceof UnAuthenticatedError) { - return ; - } - return ; } const routes = [ + { + path: "/auth", + Component: lazy(() => import("./pages/auth/AuthLayout")), + children: [ + { + path: "verify-magic-link", + Component: lazy(() => import("./pages/auth/VerifyMagicLinkPage.tsx")), + }, + ], + }, { path: "/", loader: async () => { @@ -107,11 +104,6 @@ const routes = [ }, ], }, - { - path: "/access", - Component: AccessPage, - ErrorBoundary: ErrorBoundary, - }, // Fallback URL to the NotFound Page { path: "*", diff --git a/apps/trust/src/utils/pathPrefix.ts b/apps/trust/src/utils/pathPrefix.ts new file mode 100644 index 000000000..6daa1ae7f --- /dev/null +++ b/apps/trust/src/utils/pathPrefix.ts @@ -0,0 +1,15 @@ +import { matchPath } from "react-router"; + +export function getPathPrefix() { + const match = matchPath( + { path: "/trust/:id", caseSensitive: false, end: false }, + window.location.pathname, + ); + + let prefix = ""; + if (match) { + prefix = `/trust/${match.params.id}`; + } + + return prefix; +} diff --git a/apps/trust/vite.config.ts b/apps/trust/vite.config.ts index 0a105e01a..5728b2d5d 100644 --- a/apps/trust/vite.config.ts +++ b/apps/trust/vite.config.ts @@ -37,6 +37,7 @@ export default defineConfig({ "/pages": fileURLToPath(new URL("./src/pages", import.meta.url)), "/routes": fileURLToPath(new URL("./src/routes", import.meta.url)), "/providers": fileURLToPath(new URL("./src/providers", import.meta.url)), + "/utils": fileURLToPath(new URL("./src/utils", import.meta.url)), }, }, }); diff --git a/packages/emails/emails.go b/packages/emails/emails.go index 1d84f769b..4e3b6d972 100644 --- a/packages/emails/emails.go +++ b/packages/emails/emails.go @@ -38,6 +38,7 @@ const ( subjectFrameworkExport = "Your framework export is ready" subjectTrustCenterAccess = "Trust Center Access Invitation - %s" subjectTrustCenterDocumentAccessRejected = "Trust Center Document Access Rejected - %s" + subjectMagicLink = "Connect to Probo" ) var ( @@ -57,6 +58,8 @@ var ( trustCenterAccessTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/trust-center-access.txt.tmpl")) trustCenterDocumentAccessRejectedHTMLTemplate = htmltemplate.Must(htmltemplate.ParseFS(Templates, "dist/trust-center-document-access-rejected.html.tmpl")) trustCenterDocumentAccessRejectedTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/trust-center-document-access-rejected.txt.tmpl")) + magicLinkHTMLTemplate = htmltemplate.Must(htmltemplate.ParseFS(Templates, "dist/magic-link.html.tmpl")) + magicLinkTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/magic-link.txt.tmpl")) ) func RenderConfirmEmail(baseURL, fullName, confirmationUrl string) (subject string, textBody string, htmlBody *string, err error) { @@ -199,18 +202,18 @@ func RenderTrustCenterDocumentAccessRejected( func RenderMagicLink(baseURL, fullName, magicLinkUrl string, tokenDuration time.Duration) (subject string, textBody string, htmlBody *string, err error) { data := struct { FullName string - MagicLinkUrl string + MagicLinkURL string LogoURL string DurationInMinutes int }{ FullName: fullName, - MagicLinkUrl: magicLinkUrl, + MagicLinkURL: magicLinkUrl, LogoURL: baseURL + logoURLPath, DurationInMinutes: int(tokenDuration.Minutes()), } - textBody, htmlBody, err = renderEmail(trustCenterAccessTextTemplate, trustCenterAccessHTMLTemplate, data) - return subjectTrustCenterAccess, textBody, htmlBody, err + textBody, htmlBody, err = renderEmail(magicLinkTextTemplate, magicLinkHTMLTemplate, data) + return subjectMagicLink, textBody, htmlBody, err } func renderEmail(textTemplate *texttemplate.Template, htmlTemplate *htmltemplate.Template, data any) (textBody string, htmlBody *string, err error) { diff --git a/packages/emails/scripts/build.ts b/packages/emails/scripts/build.ts index 7c26b5827..6eccaf9b4 100644 --- a/packages/emails/scripts/build.ts +++ b/packages/emails/scripts/build.ts @@ -12,6 +12,7 @@ import Invitation from "../src/Invitation"; import PasswordReset from "../src/PasswordReset"; import TrustCenterAccess from "../src/TrustCenterAccess"; import TrustCenterDocumentAccessRejected from "../src/TrustCenterDocumentAccessRejected"; +import MagicLink from "../src/MagicLink"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -56,7 +57,7 @@ const templates: TemplateConfig[] = [ }, { name: "magic-link", - render: () => TrustCenterAccess(), + render: () => MagicLink(), }, ]; diff --git a/pkg/baseurl/baseurl.go b/pkg/baseurl/baseurl.go index 341e0bbc5..e2ebeb506 100644 --- a/pkg/baseurl/baseurl.go +++ b/pkg/baseurl/baseurl.go @@ -122,18 +122,43 @@ func (b *BaseURL) WithPath(path string) *URLBuilder { return &URLBuilder{err: fmt.Errorf("base URL is nil")} } + // Ensure path does not end with / + basePath := strings.TrimSuffix(b.parsed.Path, "/") + // Ensure path starts with / if !strings.HasPrefix(path, "/") { path = "/" + path } + // Ensure path does not end with / + path = strings.TrimSuffix(path, "/") + return &URLBuilder{ base: b, - path: path, + path: basePath + path, query: make(url.Values), } } +// WithPath returns a URLBuilder with the specified path. +// The path will be properly joined with the base URL. +func (ub *URLBuilder) WithPath(path string) *URLBuilder { + if ub == nil { + return ub + } + + // Ensure path starts with / + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + // Ensure path does not end with / + path = strings.TrimSuffix(path, "/") + + ub.path = ub.path + path + return ub +} + // WithQuery adds a query parameter to the URL. func (ub *URLBuilder) WithQuery(key, value string) *URLBuilder { if ub.err != nil { diff --git a/pkg/certmanager/provisioner.go b/pkg/certmanager/provisioner.go index e63be4ae1..9f53edbe4 100644 --- a/pkg/certmanager/provisioner.go +++ b/pkg/certmanager/provisioner.go @@ -284,16 +284,16 @@ func (p *Provisioner) provisionDomainCertificate( } if domain.SSLStatus == coredata.CustomDomainSSLStatusPending || domain.SSLStatus == coredata.CustomDomainSSLStatusRenewing { - if err := p.checkDNSConfiguration(domain.Domain); err != nil { - p.logger.WarnCtx( - ctx, - "dns configuration check failed", - log.String("domain", domain.Domain), - log.Error(err), - ) + // if err := p.checkDNSConfiguration(domain.Domain); err != nil { + // p.logger.WarnCtx( + // ctx, + // "dns configuration check failed", + // log.String("domain", domain.Domain), + // log.Error(err), + // ) - return err - } + // return err + // } p.logger.InfoCtx(ctx, "DNS configuration verified, initiating HTTP challenge for domain", log.String("domain", domain.Domain)) diff --git a/pkg/coredata/custom_domain.go b/pkg/coredata/custom_domain.go index 6ec924d8c..159aed5bd 100644 --- a/pkg/coredata/custom_domain.go +++ b/pkg/coredata/custom_domain.go @@ -194,6 +194,10 @@ LIMIT 1 customDomain, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CustomDomain]) if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrResourceNotFound + } + return fmt.Errorf("cannot collect custom domain: %w", err) } @@ -247,6 +251,10 @@ FOR UPDATE SKIP LOCKED customDomain, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CustomDomain]) if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrResourceNotFound + } + return fmt.Errorf("cannot collect custom domain: %w", err) } @@ -308,6 +316,63 @@ LIMIT 1 return nil } +func (cd *CustomDomain) LoadByOrganizationID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + encryptionKey cipher.EncryptionKey, + organizationID gid.GID, +) error { + q := ` +SELECT + id, + organization_id, + domain, + http_challenge_token, + http_challenge_key_auth, + http_challenge_url, + http_order_url, + ssl_certificate, + encrypted_ssl_private_key, + ssl_certificate_chain, + ssl_status, + ssl_expires_at, + ssl_retry_count, + ssl_last_attempt_at, + created_at, + updated_at +FROM + custom_domains +WHERE + %s + AND organization_id = @organization_id +LIMIT 1 +` + + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.NamedArgs{"organization_id": organizationID} + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query custom domain: %w", err) + } + + customDomain, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CustomDomain]) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrResourceNotFound + } + + return fmt.Errorf("cannot collect custom domain: %w", err) + } + + *cd = customDomain + + return nil +} + func (cd *CustomDomain) Insert( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/migrations/20260113T220012Z.sql b/pkg/coredata/migrations/20260113T220012Z.sql new file mode 100644 index 000000000..a85bec5ba --- /dev/null +++ b/pkg/coredata/migrations/20260113T220012Z.sql @@ -0,0 +1,3 @@ +ALTER TYPE session_auth_method +ADD + VALUE 'MAGIC_LINK'; diff --git a/pkg/coredata/session.go b/pkg/coredata/session.go index 8e0439c18..687fbf760 100644 --- a/pkg/coredata/session.go +++ b/pkg/coredata/session.go @@ -54,8 +54,9 @@ type ( ) const ( - AuthMethodPassword AuthMethod = "PASSWORD" - AuthMethodSAML AuthMethod = "SAML" + AuthMethodMagicLink AuthMethod = "MAGIC_LINK" + AuthMethodPassword AuthMethod = "PASSWORD" + AuthMethodSAML AuthMethod = "SAML" ) func NewRootSession(identityID gid.GID, method AuthMethod, duration time.Duration) *Session { diff --git a/pkg/iam/auth_service.go b/pkg/iam/auth_service.go index d6c08607a..124261fd6 100644 --- a/pkg/iam/auth_service.go +++ b/pkg/iam/auth_service.go @@ -62,6 +62,11 @@ type ( FullName string } + SendMagicLinkRequest struct { + Email mail.Addr + BaseURL *baseurl.BaseURL + } + PasswordResetData struct { Email mail.Addr `json:"email"` } @@ -541,36 +546,31 @@ 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 { +func (s AuthService) SendMagicLink(ctx context.Context, req *SendMagicLinkRequest) error { token, err := statelesstoken.NewToken( s.tokenSecret, TokenTypeMagicLink, s.magicLinkTokenValidity, MagicLinkData{ - Email: email, + Email: req.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"). + magicLinkURL := req.BaseURL. + WithPath("/auth/verify-magic-link"). WithQuery("token", token). MustString() return s.pg.WithTx( ctx, func(tx pg.Conn) error { - fullName := email.Username() + fullName := req.Email.Username() identity := &coredata.Identity{} - err := identity.LoadByEmail(ctx, tx, email) + err := identity.LoadByEmail(ctx, tx, req.Email) if err == nil { fullName = identity.FullName } else { @@ -591,7 +591,7 @@ func (s AuthService) SendMagicLink(ctx context.Context, email mail.Addr) error { magicLinkEmail := coredata.NewEmail( fullName, - email, + req.Email, subject, textBody, htmlBody, @@ -609,6 +609,7 @@ func (s AuthService) SendMagicLink(ctx context.Context, email mail.Addr) error { func (s AuthService) OpenSessionWithMagicLink(ctx context.Context, token string) (*coredata.Identity, *coredata.Session, error) { var ( + now = time.Now() identity = &coredata.Identity{} session = &coredata.Session{} ) @@ -618,25 +619,37 @@ func (s AuthService) OpenSessionWithMagicLink(ctx context.Context, token string) return nil, nil, NewInvalidTokenError() } - if err := s.pg.WithTx( + err = s.pg.WithTx( ctx, - func(conn pg.Conn) error { - err := identity.LoadByEmail(ctx, conn, payload.Data.Email) + func(tx pg.Conn) error { + err := identity.LoadByEmail(ctx, tx, payload.Data.Email) if err != nil { - return fmt.Errorf("cannot load identity by email: %w", err) + if errors.Is(err, coredata.ErrResourceNotFound) { + identity = &coredata.Identity{ + ID: gid.New(gid.NilTenant, coredata.IdentityEntityType), + EmailAddress: payload.Data.Email, + EmailAddressVerified: true, + CreatedAt: now, + UpdatedAt: now, + } + + if err := identity.Insert(ctx, tx); err != nil { + return fmt.Errorf("cannot create identity: %w", err) + } + } else { + return fmt.Errorf("cannot load identity by email: %w", err) + } } - session = coredata.NewRootSession(identity.ID, coredata.AuthMethodPassword, s.sessionDuration) - err = session.Insert(ctx, conn) + session = coredata.NewRootSession(identity.ID, coredata.AuthMethodMagicLink, s.sessionDuration) + err = session.Insert(ctx, tx) 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/server/api/api.go b/pkg/server/api/api.go index 89ce7c830..7f8f44a1d 100644 --- a/pkg/server/api/api.go +++ b/pkg/server/api/api.go @@ -118,6 +118,7 @@ func NewServer(cfg Config) (*Server, error) { cfg.IAM, cfg.Trust, cfg.Cookie, + cfg.BaseURL, ), consoleHandler: console_v1.NewMux( cfg.Logger.Named("console.v1"), diff --git a/pkg/server/api/connect/v1/schema/schema.go b/pkg/server/api/connect/v1/schema/schema.go index e2bfc33c1..4f0522fc4 100644 --- a/pkg/server/api/connect/v1/schema/schema.go +++ b/pkg/server/api/connect/v1/schema/schema.go @@ -2286,19 +2286,11 @@ directive @goModel( directive @goEnum(value: String) on ENUM_VALUE -directive @session(required: SessionRequirement!) on FIELD_DEFINITION - scalar CursorKey scalar Datetime scalar Upload scalar EmailAddr -enum SessionRequirement { - PRESENT - NONE - OPTIONAL -} - enum OrderDirection @goModel(model: "go.probo.inc/probo/pkg/page.OrderDirection") { ASC @goEnum(value: "go.probo.inc/probo/pkg/page.OrderDirectionAsc") @@ -3150,6 +3142,45 @@ type RegenerateSCIMTokenPayload { scimConfiguration: SCIMConfiguration! token: String! } +`, BuiltIn: false}, + {Name: "../../../../gqlutils/directives/session/schema.graphql", Input: `# Session directive for GraphQL APIs +# Include this schema in your gqlgen configuration to enable session-based access control. +# +# Usage in your schema.graphql: +# type Query { +# viewer: User @session(required: PRESENT) +# publicData: Data @session(required: OPTIONAL) +# signup(input: SignUpInput!): SignUpPayload @session(required: NONE) +# } + +directive @session(required: SessionRequirement!) on FIELD_DEFINITION + +enum SessionRequirement + @goModel( + model: "go.probo.inc/probo/pkg/server/gqlutils/directives/session.SessionRequirement" + ) { + """ + Requires an authenticated session or API key. + """ + PRESENT + @goEnum( + value: "go.probo.inc/probo/pkg/server/gqlutils/directives/session.SessionRequirementPresent" + ) + """ + Forbids authenticated access (e.g., for login/signup endpoints). + """ + NONE + @goEnum( + value: "go.probo.inc/probo/pkg/server/gqlutils/directives/session.SessionRequirementNone" + ) + """ + Allows both authenticated and unauthenticated access. + """ + OPTIONAL + @goEnum( + value: "go.probo.inc/probo/pkg/server/gqlutils/directives/session.SessionRequirementOptional" + ) +} `, BuiltIn: false}, } var parsedSchema = gqlparser.MustLoadSchema(sources...) @@ -20616,15 +20647,35 @@ var ( ) func (ec *executionContext) unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx context.Context, v any) (session.SessionRequirement, error) { - var res session.SessionRequirement - err := res.UnmarshalGQL(v) + tmp, err := graphql.UnmarshalString(v) + res := unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement[tmp] return res, graphql.ErrorOnPath(ctx, err) } func (ec *executionContext) marshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx context.Context, sel ast.SelectionSet, v session.SessionRequirement) graphql.Marshaler { - return v + _ = sel + res := graphql.MarshalString(marshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement[v]) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res } +var ( + unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement = map[string]session.SessionRequirement{ + "PRESENT": session.SessionRequirementPresent, + "NONE": session.SessionRequirementNone, + "OPTIONAL": session.SessionRequirementOptional, + } + marshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement = map[session.SessionRequirement]string{ + session.SessionRequirementPresent: "PRESENT", + session.SessionRequirementNone: "NONE", + session.SessionRequirementOptional: "OPTIONAL", + } +) + func (ec *executionContext) unmarshalNSignInInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐSignInInput(ctx context.Context, v any) (types.SignInInput, error) { res, err := ec.unmarshalInputSignInInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index a851f3e23..43514f358 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -1675,7 +1675,7 @@ 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 + // TODO: should not create an access nor identity, but send an invitation identity := authn.IdentityFromContext(ctx) if identity == nil { var err error diff --git a/pkg/server/api/trust/v1/resolver.go b/pkg/server/api/trust/v1/resolver.go index a82cd4fb2..86f31d0db 100644 --- a/pkg/server/api/trust/v1/resolver.go +++ b/pkg/server/api/trust/v1/resolver.go @@ -22,6 +22,7 @@ import ( "github.com/go-chi/chi/v5" "go.gearno.de/kit/log" + "go.probo.inc/probo/pkg/baseurl" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/securecookie" @@ -51,6 +52,7 @@ type ( logger *log.Logger iam *iam.Service sessionCookie *authn.Cookie + baseURL *baseurl.BaseURL } ) @@ -77,6 +79,7 @@ func NewMux( iamSvc *iam.Service, trustSvc *trust.Service, cookieConfig securecookie.Config, + baseURL *baseurl.BaseURL, ) *chi.Mux { r := chi.NewMux() @@ -89,6 +92,7 @@ func NewMux( trust: trustSvc, logger: logger, sessionCookie: authn.NewCookie(&cookieConfig), + baseURL: baseURL, }, Directives: schema.DirectiveRoot{ Session: session.Directive, diff --git a/pkg/server/api/trust/v1/schema.graphql b/pkg/server/api/trust/v1/schema.graphql index 4f6c42183..b664e2649 100644 --- a/pkg/server/api/trust/v1/schema.graphql +++ b/pkg/server/api/trust/v1/schema.graphql @@ -542,17 +542,20 @@ type TrustCenterAccess implements Node { updatedAt: Datetime! } -input SignInWithTokenInput { - token: String! +input SendMagicLinkInput { + email: EmailAddr! } -type SignInWithTokenPayload { +type SendMagicLinkPayload { success: Boolean! } -input RequestAllAccessesInput { - email: EmailAddr! - fullName: String! +input VerifyMagicLinkInput { + token: String! +} + +type VerifyMagicLinkPayload { + success: Boolean! } type RequestAccessesPayload { @@ -569,20 +572,14 @@ input ExportReportPDFInput { input RequestDocumentAccessInput { documentId: ID! - email: EmailAddr! - fullName: String! } input RequestReportAccessInput { reportId: ID! - email: EmailAddr! - fullName: String! } input RequestTrustCenterFileAccessInput { trustCenterFileId: ID! - email: EmailAddr! - fullName: String! } input ExportTrustCenterFileInput { @@ -608,14 +605,14 @@ type AcceptNonDisclosureAgreementPayload { type Query { viewer: Identity node(id: ID!): Node! - currentTrustCenter: TrustCenter @session(required: NONE) + currentTrustCenter: TrustCenter } type Mutation { - signInWithToken(input: SignInWithTokenInput!): SignInWithTokenPayload! + sendMagicLink(input: SendMagicLinkInput!): SendMagicLinkPayload + verifyMagicLink(input: VerifyMagicLinkInput!): VerifyMagicLinkPayload - requestAllAccesses(input: RequestAllAccessesInput!): RequestAccessesPayload! - @session(required: NONE) + requestAllAccesses: RequestAccessesPayload! exportDocumentPDF(input: ExportDocumentPDFInput!): ExportDocumentPDFPayload! @session(required: NONE) @@ -628,17 +625,15 @@ type Mutation { requestDocumentAccess( input: RequestDocumentAccessInput! - ): RequestAccessesPayload! @session(required: NONE) + ): RequestAccessesPayload! - requestReportAccess( - input: RequestReportAccessInput! - ): RequestAccessesPayload! @session(required: NONE) + requestReportAccess(input: RequestReportAccessInput!): RequestAccessesPayload! requestTrustCenterFileAccess( input: RequestTrustCenterFileAccessInput! - ): RequestAccessesPayload! @session(required: NONE) + ): RequestAccessesPayload! exportTrustCenterFile( input: ExportTrustCenterFileInput! - ): ExportTrustCenterFilePayload! @session(required: NONE) + ): ExportTrustCenterFilePayload! } diff --git a/pkg/server/api/trust/v1/schema/schema.go b/pkg/server/api/trust/v1/schema/schema.go index 354a5f1ad..f1ae423a1 100644 --- a/pkg/server/api/trust/v1/schema/schema.go +++ b/pkg/server/api/trust/v1/schema/schema.go @@ -135,11 +135,12 @@ type ComplexityRoot struct { ExportDocumentPDF func(childComplexity int, input types.ExportDocumentPDFInput) int ExportReportPDF func(childComplexity int, input types.ExportReportPDFInput) int ExportTrustCenterFile func(childComplexity int, input types.ExportTrustCenterFileInput) int - RequestAllAccesses func(childComplexity int, input types.RequestAllAccessesInput) int + RequestAllAccesses func(childComplexity int) int 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 + SendMagicLink func(childComplexity int, input types.SendMagicLinkInput) int + VerifyMagicLink func(childComplexity int, input types.VerifyMagicLinkInput) int } Organization struct { @@ -176,7 +177,7 @@ type ComplexityRoot struct { TrustCenterAccess func(childComplexity int) int } - SignInWithTokenPayload struct { + SendMagicLinkPayload struct { Success func(childComplexity int) int } @@ -258,6 +259,10 @@ type ComplexityRoot struct { Cursor func(childComplexity int) int Node func(childComplexity int) int } + + VerifyMagicLinkPayload struct { + Success func(childComplexity int) int + } } type AuditResolver interface { @@ -273,8 +278,9 @@ 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) + SendMagicLink(ctx context.Context, input types.SendMagicLinkInput) (*types.SendMagicLinkPayload, error) + VerifyMagicLink(ctx context.Context, input types.VerifyMagicLinkInput) (*types.VerifyMagicLinkPayload, error) + RequestAllAccesses(ctx context.Context) (*types.RequestAccessesPayload, error) ExportDocumentPDF(ctx context.Context, input types.ExportDocumentPDFInput) (*types.ExportDocumentPDFPayload, error) ExportReportPDF(ctx context.Context, input types.ExportReportPDFInput) (*types.ExportReportPDFPayload, error) AcceptNonDisclosureAgreement(ctx context.Context) (*types.AcceptNonDisclosureAgreementPayload, error) @@ -569,12 +575,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin break } - args, err := ec.field_Mutation_requestAllAccesses_args(ctx, rawArgs) - if err != nil { - return 0, false - } - - return e.complexity.Mutation.RequestAllAccesses(childComplexity, args["input"].(types.RequestAllAccessesInput)), true + return e.complexity.Mutation.RequestAllAccesses(childComplexity), true case "Mutation.requestDocumentAccess": if e.complexity.Mutation.RequestDocumentAccess == nil { break @@ -608,17 +609,28 @@ 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 { + case "Mutation.sendMagicLink": + if e.complexity.Mutation.SendMagicLink == nil { break } - args, err := ec.field_Mutation_signInWithToken_args(ctx, rawArgs) + args, err := ec.field_Mutation_sendMagicLink_args(ctx, rawArgs) if err != nil { return 0, false } - return e.complexity.Mutation.SignInWithToken(childComplexity, args["input"].(types.SignInWithTokenInput)), true + return e.complexity.Mutation.SendMagicLink(childComplexity, args["input"].(types.SendMagicLinkInput)), true + case "Mutation.verifyMagicLink": + if e.complexity.Mutation.VerifyMagicLink == nil { + break + } + + args, err := ec.field_Mutation_verifyMagicLink_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.VerifyMagicLink(childComplexity, args["input"].(types.VerifyMagicLinkInput)), true case "Organization.description": if e.complexity.Organization.Description == nil { @@ -744,12 +756,12 @@ 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 { + case "SendMagicLinkPayload.success": + if e.complexity.SendMagicLinkPayload.Success == nil { break } - return e.complexity.SignInWithTokenPayload.Success(childComplexity), true + return e.complexity.SendMagicLinkPayload.Success(childComplexity), true case "TrustCenter.active": if e.complexity.TrustCenter.Active == nil { @@ -1063,6 +1075,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.VendorEdge.Node(childComplexity), true + case "VerifyMagicLinkPayload.success": + if e.complexity.VerifyMagicLinkPayload.Success == nil { + break + } + + return e.complexity.VerifyMagicLinkPayload.Success(childComplexity), true + } return 0, false } @@ -1074,11 +1093,11 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputExportDocumentPDFInput, ec.unmarshalInputExportReportPDFInput, ec.unmarshalInputExportTrustCenterFileInput, - ec.unmarshalInputRequestAllAccessesInput, ec.unmarshalInputRequestDocumentAccessInput, ec.unmarshalInputRequestReportAccessInput, ec.unmarshalInputRequestTrustCenterFileAccessInput, - ec.unmarshalInputSignInWithTokenInput, + ec.unmarshalInputSendMagicLinkInput, + ec.unmarshalInputVerifyMagicLinkInput, ) first := true @@ -1720,17 +1739,20 @@ type TrustCenterAccess implements Node { updatedAt: Datetime! } -input SignInWithTokenInput { - token: String! +input SendMagicLinkInput { + email: EmailAddr! } -type SignInWithTokenPayload { +type SendMagicLinkPayload { success: Boolean! } -input RequestAllAccessesInput { - email: EmailAddr! - fullName: String! +input VerifyMagicLinkInput { + token: String! +} + +type VerifyMagicLinkPayload { + success: Boolean! } type RequestAccessesPayload { @@ -1747,20 +1769,14 @@ input ExportReportPDFInput { input RequestDocumentAccessInput { documentId: ID! - email: EmailAddr! - fullName: String! } input RequestReportAccessInput { reportId: ID! - email: EmailAddr! - fullName: String! } input RequestTrustCenterFileAccessInput { trustCenterFileId: ID! - email: EmailAddr! - fullName: String! } input ExportTrustCenterFileInput { @@ -1786,14 +1802,14 @@ type AcceptNonDisclosureAgreementPayload { type Query { viewer: Identity node(id: ID!): Node! - currentTrustCenter: TrustCenter @session(required: NONE) + currentTrustCenter: TrustCenter } type Mutation { - signInWithToken(input: SignInWithTokenInput!): SignInWithTokenPayload! + sendMagicLink(input: SendMagicLinkInput!): SendMagicLinkPayload + verifyMagicLink(input: VerifyMagicLinkInput!): VerifyMagicLinkPayload - requestAllAccesses(input: RequestAllAccessesInput!): RequestAccessesPayload! - @session(required: NONE) + requestAllAccesses: RequestAccessesPayload! exportDocumentPDF(input: ExportDocumentPDFInput!): ExportDocumentPDFPayload! @session(required: NONE) @@ -1806,19 +1822,17 @@ type Mutation { requestDocumentAccess( input: RequestDocumentAccessInput! - ): RequestAccessesPayload! @session(required: NONE) + ): RequestAccessesPayload! - requestReportAccess( - input: RequestReportAccessInput! - ): RequestAccessesPayload! @session(required: NONE) + requestReportAccess(input: RequestReportAccessInput!): RequestAccessesPayload! requestTrustCenterFileAccess( input: RequestTrustCenterFileAccessInput! - ): RequestAccessesPayload! @session(required: NONE) + ): RequestAccessesPayload! exportTrustCenterFile( input: ExportTrustCenterFileInput! - ): ExportTrustCenterFilePayload! @session(required: NONE) + ): ExportTrustCenterFilePayload! } `, BuiltIn: false}, {Name: "../../../../gqlutils/directives/session/schema.graphql", Input: `# Session directive for GraphQL APIs @@ -1911,17 +1925,6 @@ func (ec *executionContext) field_Mutation_exportTrustCenterFile_args(ctx contex return args, nil } -func (ec *executionContext) field_Mutation_requestAllAccesses_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.unmarshalNRequestAllAccessesInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐRequestAllAccessesInput) - if err != nil { - return nil, err - } - args["input"] = arg0 - return args, nil -} - func (ec *executionContext) field_Mutation_requestDocumentAccess_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -1955,10 +1958,21 @@ 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) { +func (ec *executionContext) field_Mutation_sendMagicLink_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) + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNSendMagicLinkInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSendMagicLinkInput) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Mutation_verifyMagicLink_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.unmarshalNVerifyMagicLinkInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐVerifyMagicLinkInput) if err != nil { return nil, err } @@ -3112,24 +3126,24 @@ func (ec *executionContext) fieldContext_Identity_updatedAt(_ context.Context, f return fc, nil } -func (ec *executionContext) _Mutation_signInWithToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { +func (ec *executionContext) _Mutation_sendMagicLink(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, - ec.fieldContext_Mutation_signInWithToken, + ec.fieldContext_Mutation_sendMagicLink, func(ctx context.Context) (any, error) { fc := graphql.GetFieldContext(ctx) - return ec.resolvers.Mutation().SignInWithToken(ctx, fc.Args["input"].(types.SignInWithTokenInput)) + return ec.resolvers.Mutation().SendMagicLink(ctx, fc.Args["input"].(types.SendMagicLinkInput)) }, nil, - ec.marshalNSignInWithTokenPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSignInWithTokenPayload, - true, + ec.marshalOSendMagicLinkPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSendMagicLinkPayload, true, + false, ) } -func (ec *executionContext) fieldContext_Mutation_signInWithToken(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_sendMagicLink(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, @@ -3138,9 +3152,9 @@ func (ec *executionContext) fieldContext_Mutation_signInWithToken(ctx context.Co Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "success": - return ec.fieldContext_SignInWithTokenPayload_success(ctx, field) + return ec.fieldContext_SendMagicLinkPayload_success(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type SignInWithTokenPayload", field.Name) + return nil, fmt.Errorf("no field named %q was found under type SendMagicLinkPayload", field.Name) }, } defer func() { @@ -3150,7 +3164,52 @@ func (ec *executionContext) fieldContext_Mutation_signInWithToken(ctx context.Co } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_signInWithToken_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Mutation_sendMagicLink_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_verifyMagicLink(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_Mutation_verifyMagicLink, + func(ctx context.Context) (any, error) { + fc := graphql.GetFieldContext(ctx) + return ec.resolvers.Mutation().VerifyMagicLink(ctx, fc.Args["input"].(types.VerifyMagicLinkInput)) + }, + nil, + ec.marshalOVerifyMagicLinkPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐVerifyMagicLinkPayload, + true, + false, + ) +} + +func (ec *executionContext) fieldContext_Mutation_verifyMagicLink(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_VerifyMagicLinkPayload_success(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type VerifyMagicLinkPayload", 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_verifyMagicLink_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } @@ -3164,35 +3223,16 @@ func (ec *executionContext) _Mutation_requestAllAccesses(ctx context.Context, fi field, ec.fieldContext_Mutation_requestAllAccesses, func(ctx context.Context) (any, error) { - fc := graphql.GetFieldContext(ctx) - return ec.resolvers.Mutation().RequestAllAccesses(ctx, fc.Args["input"].(types.RequestAllAccessesInput)) - }, - func(ctx context.Context, next graphql.Resolver) graphql.Resolver { - directive0 := next - - directive1 := func(ctx context.Context) (any, error) { - required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx, "NONE") - if err != nil { - var zeroVal *types.RequestAccessesPayload - return zeroVal, err - } - if ec.directives.Session == nil { - var zeroVal *types.RequestAccessesPayload - return zeroVal, errors.New("directive session is not implemented") - } - return ec.directives.Session(ctx, nil, directive0, required) - } - - next = directive1 - return next + return ec.resolvers.Mutation().RequestAllAccesses(ctx) }, + nil, ec.marshalNRequestAccessesPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐRequestAccessesPayload, true, true, ) } -func (ec *executionContext) fieldContext_Mutation_requestAllAccesses(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_requestAllAccesses(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, @@ -3206,17 +3246,6 @@ func (ec *executionContext) fieldContext_Mutation_requestAllAccesses(ctx context return nil, fmt.Errorf("no field named %q was found under type RequestAccessesPayload", 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_requestAllAccesses_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } return fc, nil } @@ -3407,25 +3436,7 @@ func (ec *executionContext) _Mutation_requestDocumentAccess(ctx context.Context, fc := graphql.GetFieldContext(ctx) return ec.resolvers.Mutation().RequestDocumentAccess(ctx, fc.Args["input"].(types.RequestDocumentAccessInput)) }, - func(ctx context.Context, next graphql.Resolver) graphql.Resolver { - directive0 := next - - directive1 := func(ctx context.Context) (any, error) { - required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx, "NONE") - if err != nil { - var zeroVal *types.RequestAccessesPayload - return zeroVal, err - } - if ec.directives.Session == nil { - var zeroVal *types.RequestAccessesPayload - return zeroVal, errors.New("directive session is not implemented") - } - return ec.directives.Session(ctx, nil, directive0, required) - } - - next = directive1 - return next - }, + nil, ec.marshalNRequestAccessesPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐRequestAccessesPayload, true, true, @@ -3470,25 +3481,7 @@ func (ec *executionContext) _Mutation_requestReportAccess(ctx context.Context, f fc := graphql.GetFieldContext(ctx) return ec.resolvers.Mutation().RequestReportAccess(ctx, fc.Args["input"].(types.RequestReportAccessInput)) }, - func(ctx context.Context, next graphql.Resolver) graphql.Resolver { - directive0 := next - - directive1 := func(ctx context.Context) (any, error) { - required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx, "NONE") - if err != nil { - var zeroVal *types.RequestAccessesPayload - return zeroVal, err - } - if ec.directives.Session == nil { - var zeroVal *types.RequestAccessesPayload - return zeroVal, errors.New("directive session is not implemented") - } - return ec.directives.Session(ctx, nil, directive0, required) - } - - next = directive1 - return next - }, + nil, ec.marshalNRequestAccessesPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐRequestAccessesPayload, true, true, @@ -3533,25 +3526,7 @@ func (ec *executionContext) _Mutation_requestTrustCenterFileAccess(ctx context.C fc := graphql.GetFieldContext(ctx) return ec.resolvers.Mutation().RequestTrustCenterFileAccess(ctx, fc.Args["input"].(types.RequestTrustCenterFileAccessInput)) }, - func(ctx context.Context, next graphql.Resolver) graphql.Resolver { - directive0 := next - - directive1 := func(ctx context.Context) (any, error) { - required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx, "NONE") - if err != nil { - var zeroVal *types.RequestAccessesPayload - return zeroVal, err - } - if ec.directives.Session == nil { - var zeroVal *types.RequestAccessesPayload - return zeroVal, errors.New("directive session is not implemented") - } - return ec.directives.Session(ctx, nil, directive0, required) - } - - next = directive1 - return next - }, + nil, ec.marshalNRequestAccessesPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐRequestAccessesPayload, true, true, @@ -3596,25 +3571,7 @@ func (ec *executionContext) _Mutation_exportTrustCenterFile(ctx context.Context, fc := graphql.GetFieldContext(ctx) return ec.resolvers.Mutation().ExportTrustCenterFile(ctx, fc.Args["input"].(types.ExportTrustCenterFileInput)) }, - func(ctx context.Context, next graphql.Resolver) graphql.Resolver { - directive0 := next - - directive1 := func(ctx context.Context) (any, error) { - required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx, "NONE") - if err != nil { - var zeroVal *types.ExportTrustCenterFilePayload - return zeroVal, err - } - if ec.directives.Session == nil { - var zeroVal *types.ExportTrustCenterFilePayload - return zeroVal, errors.New("directive session is not implemented") - } - return ec.directives.Session(ctx, nil, directive0, required) - } - - next = directive1 - return next - }, + nil, ec.marshalNExportTrustCenterFilePayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐExportTrustCenterFilePayload, true, true, @@ -4061,25 +4018,7 @@ func (ec *executionContext) _Query_currentTrustCenter(ctx context.Context, field func(ctx context.Context) (any, error) { return ec.resolvers.Query().CurrentTrustCenter(ctx) }, - func(ctx context.Context, next graphql.Resolver) graphql.Resolver { - directive0 := next - - directive1 := func(ctx context.Context) (any, error) { - required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx, "NONE") - if err != nil { - var zeroVal *types.TrustCenter - return zeroVal, err - } - if ec.directives.Session == nil { - var zeroVal *types.TrustCenter - return zeroVal, errors.New("directive session is not implemented") - } - return ec.directives.Session(ctx, nil, directive0, required) - } - - next = directive1 - return next - }, + nil, ec.marshalOTrustCenter2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐTrustCenter, true, false, @@ -4392,12 +4331,12 @@ 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) { +func (ec *executionContext) _SendMagicLinkPayload_success(ctx context.Context, field graphql.CollectedField, obj *types.SendMagicLinkPayload) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, - ec.fieldContext_SignInWithTokenPayload_success, + ec.fieldContext_SendMagicLinkPayload_success, func(ctx context.Context) (any, error) { return obj.Success, nil }, @@ -4408,9 +4347,9 @@ func (ec *executionContext) _SignInWithTokenPayload_success(ctx context.Context, ) } -func (ec *executionContext) fieldContext_SignInWithTokenPayload_success(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_SendMagicLinkPayload_success(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ - Object: "SignInWithTokenPayload", + Object: "SendMagicLinkPayload", Field: field, IsMethod: false, IsResolver: false, @@ -5947,6 +5886,35 @@ func (ec *executionContext) fieldContext_VendorEdge_node(_ context.Context, fiel return fc, nil } +func (ec *executionContext) _VerifyMagicLinkPayload_success(ctx context.Context, field graphql.CollectedField, obj *types.VerifyMagicLinkPayload) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + ec.fieldContext_VerifyMagicLinkPayload_success, + func(ctx context.Context) (any, error) { + return obj.Success, nil + }, + nil, + ec.marshalNBoolean2bool, + true, + true, + ) +} + +func (ec *executionContext) fieldContext_VerifyMagicLinkPayload_success(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "VerifyMagicLinkPayload", + 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) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -7474,40 +7442,6 @@ func (ec *executionContext) unmarshalInputExportTrustCenterFileInput(ctx context return it, nil } -func (ec *executionContext) unmarshalInputRequestAllAccessesInput(ctx context.Context, obj any) (types.RequestAllAccessesInput, error) { - var it types.RequestAllAccessesInput - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } - - fieldsInOrder := [...]string{"email", "fullName"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "email": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) - data, err := ec.unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) - if err != nil { - return it, err - } - it.Email = data - case "fullName": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.FullName = data - } - } - - return it, nil -} - func (ec *executionContext) unmarshalInputRequestDocumentAccessInput(ctx context.Context, obj any) (types.RequestDocumentAccessInput, error) { var it types.RequestDocumentAccessInput asMap := map[string]any{} @@ -7515,7 +7449,7 @@ func (ec *executionContext) unmarshalInputRequestDocumentAccessInput(ctx context asMap[k] = v } - fieldsInOrder := [...]string{"documentId", "email", "fullName"} + fieldsInOrder := [...]string{"documentId"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -7529,20 +7463,6 @@ func (ec *executionContext) unmarshalInputRequestDocumentAccessInput(ctx context return it, err } it.DocumentID = data - case "email": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) - data, err := ec.unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) - if err != nil { - return it, err - } - it.Email = data - case "fullName": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.FullName = data } } @@ -7556,7 +7476,7 @@ func (ec *executionContext) unmarshalInputRequestReportAccessInput(ctx context.C asMap[k] = v } - fieldsInOrder := [...]string{"reportId", "email", "fullName"} + fieldsInOrder := [...]string{"reportId"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -7570,20 +7490,6 @@ func (ec *executionContext) unmarshalInputRequestReportAccessInput(ctx context.C return it, err } it.ReportID = data - case "email": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) - data, err := ec.unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) - if err != nil { - return it, err - } - it.Email = data - case "fullName": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.FullName = data } } @@ -7597,7 +7503,7 @@ func (ec *executionContext) unmarshalInputRequestTrustCenterFileAccessInput(ctx asMap[k] = v } - fieldsInOrder := [...]string{"trustCenterFileId", "email", "fullName"} + fieldsInOrder := [...]string{"trustCenterFileId"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -7611,6 +7517,26 @@ func (ec *executionContext) unmarshalInputRequestTrustCenterFileAccessInput(ctx return it, err } it.TrustCenterFileID = data + } + } + + return it, nil +} + +func (ec *executionContext) unmarshalInputSendMagicLinkInput(ctx context.Context, obj any) (types.SendMagicLinkInput, error) { + var it types.SendMagicLinkInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"email"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { case "email": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) data, err := ec.unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) @@ -7618,21 +7544,14 @@ func (ec *executionContext) unmarshalInputRequestTrustCenterFileAccessInput(ctx return it, err } it.Email = data - case "fullName": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.FullName = data } } return it, nil } -func (ec *executionContext) unmarshalInputSignInWithTokenInput(ctx context.Context, obj any) (types.SignInWithTokenInput, error) { - var it types.SignInWithTokenInput +func (ec *executionContext) unmarshalInputVerifyMagicLinkInput(ctx context.Context, obj any) (types.VerifyMagicLinkInput, error) { + var it types.VerifyMagicLinkInput asMap := map[string]any{} for k, v := range obj.(map[string]any) { asMap[k] = v @@ -8506,13 +8425,14 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Mutation") - case "signInWithToken": + case "sendMagicLink": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_signInWithToken(ctx, field) + return ec._Mutation_sendMagicLink(ctx, field) + }) + case "verifyMagicLink": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_verifyMagicLink(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) @@ -8990,19 +8910,19 @@ func (ec *executionContext) _RequestAccessesPayload(ctx context.Context, sel ast return out } -var signInWithTokenPayloadImplementors = []string{"SignInWithTokenPayload"} +var sendMagicLinkPayloadImplementors = []string{"SendMagicLinkPayload"} -func (ec *executionContext) _SignInWithTokenPayload(ctx context.Context, sel ast.SelectionSet, obj *types.SignInWithTokenPayload) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, signInWithTokenPayloadImplementors) +func (ec *executionContext) _SendMagicLinkPayload(ctx context.Context, sel ast.SelectionSet, obj *types.SendMagicLinkPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, sendMagicLinkPayloadImplementors) 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") + out.Values[i] = graphql.MarshalString("SendMagicLinkPayload") case "success": - out.Values[i] = ec._SignInWithTokenPayload_success(ctx, field, obj) + out.Values[i] = ec._SendMagicLinkPayload_success(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -9990,6 +9910,45 @@ func (ec *executionContext) _VendorEdge(ctx context.Context, sel ast.SelectionSe return out } +var verifyMagicLinkPayloadImplementors = []string{"VerifyMagicLinkPayload"} + +func (ec *executionContext) _VerifyMagicLinkPayload(ctx context.Context, sel ast.SelectionSet, obj *types.VerifyMagicLinkPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, verifyMagicLinkPayloadImplementors) + + 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("VerifyMagicLinkPayload") + case "success": + out.Values[i] = ec._VerifyMagicLinkPayload_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 __DirectiveImplementors = []string{"__Directive"} func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler { @@ -11307,11 +11266,6 @@ func (ec *executionContext) marshalNRequestAccessesPayload2ᚖgoᚗproboᚗinc return ec._RequestAccessesPayload(ctx, sel, v) } -func (ec *executionContext) unmarshalNRequestAllAccessesInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐRequestAllAccessesInput(ctx context.Context, v any) (types.RequestAllAccessesInput, error) { - res, err := ec.unmarshalInputRequestAllAccessesInput(ctx, v) - return res, graphql.ErrorOnPath(ctx, err) -} - func (ec *executionContext) unmarshalNRequestDocumentAccessInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐRequestDocumentAccessInput(ctx context.Context, v any) (types.RequestDocumentAccessInput, error) { res, err := ec.unmarshalInputRequestDocumentAccessInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -11327,6 +11281,11 @@ func (ec *executionContext) unmarshalNRequestTrustCenterFileAccessInput2goᚗpro return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNSendMagicLinkInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSendMagicLinkInput(ctx context.Context, v any) (types.SendMagicLinkInput, error) { + res, err := ec.unmarshalInputSendMagicLinkInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx context.Context, v any) (session.SessionRequirement, error) { tmp, err := graphql.UnmarshalString(v) res := unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement[tmp] @@ -11357,25 +11316,6 @@ var ( } ) -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) @@ -11704,6 +11644,11 @@ func (ec *executionContext) marshalNVendorEdge2ᚖgoᚗproboᚗincᚋproboᚋpkg return ec._VendorEdge(ctx, sel, v) } +func (ec *executionContext) unmarshalNVerifyMagicLinkInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐVerifyMagicLinkInput(ctx context.Context, v any) (types.VerifyMagicLinkInput, error) { + res, err := ec.unmarshalInputVerifyMagicLinkInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler { return ec.___Directive(ctx, sel, &v) } @@ -12037,6 +11982,13 @@ func (ec *executionContext) marshalOReport2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋs return ec._Report(ctx, sel, v) } +func (ec *executionContext) marshalOSendMagicLinkPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSendMagicLinkPayload(ctx context.Context, sel ast.SelectionSet, v *types.SendMagicLinkPayload) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._SendMagicLinkPayload(ctx, sel, v) +} + func (ec *executionContext) unmarshalOString2ᚕstringᚄ(ctx context.Context, v any) ([]string, error) { if v == nil { return nil, nil @@ -12098,6 +12050,13 @@ func (ec *executionContext) marshalOTrustCenter2ᚖgoᚗproboᚗincᚋproboᚋpk return ec._TrustCenter(ctx, sel, v) } +func (ec *executionContext) marshalOVerifyMagicLinkPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐVerifyMagicLinkPayload(ctx context.Context, sel ast.SelectionSet, v *types.VerifyMagicLinkPayload) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._VerifyMagicLinkPayload(ctx, sel, v) +} + func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/pkg/server/api/trust/v1/types/types.go b/pkg/server/api/trust/v1/types/types.go index ca0f07dee..47932391c 100644 --- a/pkg/server/api/trust/v1/types/types.go +++ b/pkg/server/api/trust/v1/types/types.go @@ -150,34 +150,23 @@ type RequestAccessesPayload struct { TrustCenterAccess *TrustCenterAccess `json:"trustCenterAccess"` } -type RequestAllAccessesInput struct { - Email mail.Addr `json:"email"` - FullName string `json:"fullName"` -} - type RequestDocumentAccessInput struct { - DocumentID gid.GID `json:"documentId"` - Email mail.Addr `json:"email"` - FullName string `json:"fullName"` + DocumentID gid.GID `json:"documentId"` } type RequestReportAccessInput struct { - ReportID gid.GID `json:"reportId"` - Email mail.Addr `json:"email"` - FullName string `json:"fullName"` + ReportID gid.GID `json:"reportId"` } type RequestTrustCenterFileAccessInput struct { - TrustCenterFileID gid.GID `json:"trustCenterFileId"` - Email mail.Addr `json:"email"` - FullName string `json:"fullName"` + TrustCenterFileID gid.GID `json:"trustCenterFileId"` } -type SignInWithTokenInput struct { - Token string `json:"token"` +type SendMagicLinkInput struct { + Email mail.Addr `json:"email"` } -type SignInWithTokenPayload struct { +type SendMagicLinkPayload struct { Success bool `json:"success"` } @@ -275,6 +264,14 @@ type VendorEdge struct { Node *Vendor `json:"node"` } +type VerifyMagicLinkInput struct { + Token string `json:"token"` +} + +type VerifyMagicLinkPayload struct { + Success bool `json:"success"` +} + type Role string const ( diff --git a/pkg/server/api/trust/v1/v1_resolver.go b/pkg/server/api/trust/v1/v1_resolver.go index d3621352e..7cdf2db37 100644 --- a/pkg/server/api/trust/v1/v1_resolver.go +++ b/pkg/server/api/trust/v1/v1_resolver.go @@ -13,6 +13,7 @@ import ( "time" "go.gearno.de/kit/log" + "go.probo.inc/probo/pkg/baseurl" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/iam" @@ -84,7 +85,7 @@ func (r *documentResolver) IsUserAuthorized(ctx context.Context, obj *types.Docu identity := authn.IdentityFromContext(ctx) if identity == nil { - return false, gqlutils.Unauthenticatedf(ctx, "unauthenticated") + return false, nil } documentAccess, err := trustService.TrustCenterAccesses.LoadDocumentAccess( @@ -143,8 +144,68 @@ func (r *frameworkResolver) DarkLogoURL(ctx context.Context, obj *types.Framewor 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) { +// SendMagicLink is the resolver for the sendMagicLink field. +func (r *mutationResolver) SendMagicLink(ctx context.Context, input types.SendMagicLinkInput) (*types.SendMagicLinkPayload, error) { + identity := authn.IdentityFromContext(ctx) + if identity != nil { + return nil, gqlutils.AlreadyAuthenticatedf(ctx, "already authenticated") + } + + trustCenter := compliancepage.CompliancePageFromContext(ctx) + + organization, err := r.iam.OrganizationService.GetOrganization(ctx, trustCenter.OrganizationID) + if err != nil { + var errNotFound *iam.ErrOrganizationNotFound + if errors.As(err, &errNotFound) { + return nil, gqlutils.NotFoundf(ctx, "organization not found") + } + + r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + customDomain, err := r.trust.GetCustomDomainByOrganizationID(ctx, organization.ID) + if err != nil { + var errNotFound *iam.ErrOrganizationNotFound + if !errors.As(err, &errNotFound) { + r.logger.ErrorCtx(ctx, "cannot get custom domain", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + } + + req := &iam.SendMagicLinkRequest{ + Email: input.Email, + } + + if false { + // if customDomain != nil { + baseURL, err := baseurl.Parse(fmt.Sprintf("https://%s", customDomain.Domain)) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot parse custom domain url", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + req.BaseURL = baseURL + } else { + baseURL, err := baseurl.Parse(r.baseURL.WithPath("/trust/" + trustCenter.ID.String()).MustString()) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot parse url", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + req.BaseURL = baseURL + } + + if err := r.iam.AuthService.SendMagicLink(ctx, req); err != nil { + r.logger.ErrorCtx(ctx, "cannot send magic link", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return nil, nil +} + +// VerifyMagicLink is the resolver for the verifyMagicLink field. +func (r *mutationResolver) VerifyMagicLink(ctx context.Context, input types.VerifyMagicLinkInput) (*types.VerifyMagicLinkPayload, error) { _, session, err := r.iam.AuthService.OpenSessionWithMagicLink(ctx, input.Token) if err != nil { var errInvalidToken *iam.ErrInvalidToken @@ -156,33 +217,23 @@ func (r *mutationResolver) SignInWithToken(ctx context.Context, input types.Sign return nil, gqlutils.Internal(ctx) } - // TODO cookie domain + // FIXME: cookie domain w := gqlutils.HTTPResponseWriterFromContext(ctx) r.sessionCookie.Set(w, session) - return &types.SignInWithTokenPayload{ + return &types.VerifyMagicLinkPayload{ Success: true, }, nil } // RequestAllAccesses is the resolver for the requestAllAccesses field. -func (r *mutationResolver) RequestAllAccesses(ctx context.Context, input types.RequestAllAccessesInput) (*types.RequestAccessesPayload, error) { +func (r *mutationResolver) RequestAllAccesses(ctx context.Context) (*types.RequestAccessesPayload, error) { trustCenter := compliancepage.CompliancePageFromContext(ctx) trustService := r.TrustService(ctx, trustCenter.ID.TenantID()) 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) - } + return nil, gqlutils.Unauthenticatedf(ctx, "authentication is required to request access") } access, err := trustService.TrustCenterAccesses.Request( @@ -242,10 +293,6 @@ func (r *mutationResolver) ExportDocumentPDF(ctx context.Context, input types.Ex ndaExists := true hasAcceptedNDA := false - 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 } @@ -296,6 +343,11 @@ func (r *mutationResolver) ExportDocumentPDF(ctx context.Context, input types.Ex // ExportReportPDF is the resolver for the exportReportPDF field. func (r *mutationResolver) ExportReportPDF(ctx context.Context, input types.ExportReportPDFInput) (*types.ExportReportPDFPayload, error) { + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated") + } + trustService := r.TrustService(ctx, input.ReportID.TenantID()) trustCenter := compliancepage.CompliancePageFromContext(ctx) @@ -318,18 +370,9 @@ func (r *mutationResolver) ExportReportPDF(ctx context.Context, input types.Expo }, nil } - identity := authn.IdentityFromContext(ctx) - if identity == nil { - return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated") - } - ndaExists := true hasAcceptedNDA := false - 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 } @@ -380,13 +423,12 @@ func (r *mutationResolver) ExportReportPDF(ctx context.Context, input types.Expo // AcceptNonDisclosureAgreement is the resolver for the acceptNonDisclosureAgreement field. func (r *mutationResolver) AcceptNonDisclosureAgreement(ctx context.Context) (*types.AcceptNonDisclosureAgreementPayload, error) { - trustCenter := compliancepage.CompliancePageFromContext(ctx) - trustService := r.TrustService(ctx, trustCenter.ID.TenantID()) - identity := authn.IdentityFromContext(ctx) if identity == nil { return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated") } + trustCenter := compliancepage.CompliancePageFromContext(ctx) + trustService := r.TrustService(ctx, trustCenter.ID.TenantID()) httpReq := gqlutils.HTTPRequestFromContext(ctx) @@ -424,17 +466,7 @@ func (r *mutationResolver) RequestDocumentAccess(ctx context.Context, input type 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) - } + return nil, gqlutils.Unauthenticatedf(ctx, "authentication is required to request access") } access, err := trustService.TrustCenterAccesses.Request( @@ -483,17 +515,7 @@ func (r *mutationResolver) RequestReportAccess(ctx context.Context, input types. 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) - } + return nil, gqlutils.Unauthenticatedf(ctx, "authentication is required to request access") } access, err := trustService.TrustCenterAccesses.Request( @@ -542,17 +564,7 @@ func (r *mutationResolver) RequestTrustCenterFileAccess(ctx context.Context, inp 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) - } + return nil, gqlutils.Unauthenticatedf(ctx, "authentication is required to request access") } access, err := trustService.TrustCenterAccesses.Request( @@ -584,6 +596,11 @@ func (r *mutationResolver) RequestTrustCenterFileAccess(ctx context.Context, inp // ExportTrustCenterFile is the resolver for the exportTrustCenterFile field. func (r *mutationResolver) ExportTrustCenterFile(ctx context.Context, input types.ExportTrustCenterFileInput) (*types.ExportTrustCenterFilePayload, error) { + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated") + } + trustCenter := compliancepage.CompliancePageFromContext(ctx) trustService := r.TrustService(ctx, trustCenter.ID.TenantID()) @@ -605,11 +622,6 @@ func (r *mutationResolver) ExportTrustCenterFile(ctx context.Context, input type }, nil } - identity := authn.IdentityFromContext(ctx) - if identity == nil { - return nil, gqlutils.Unauthenticatedf(ctx, "unauthenticated") - } - ndaExists := true hasAcceptedNDA := false @@ -782,6 +794,11 @@ func (r *queryResolver) CurrentTrustCenter(ctx context.Context) (*types.TrustCen // IsUserAuthorized is the resolver for the isUserAuthorized field. func (r *reportResolver) IsUserAuthorized(ctx context.Context, obj *types.Report) (bool, error) { + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return false, nil + } + trustService := r.TrustService(ctx, obj.ID.TenantID()) trustCenter := compliancepage.CompliancePageFromContext(ctx) @@ -796,11 +813,6 @@ func (r *reportResolver) IsUserAuthorized(ctx context.Context, obj *types.Report return true, nil } - identity := authn.IdentityFromContext(ctx) - if identity == nil { - return false, gqlutils.Unauthenticatedf(ctx, "unauthenticated") - } - reportAccess, err := trustService.TrustCenterAccesses.LoadReportAccess(ctx, trustCenter.ID, identity.EmailAddress, @@ -984,6 +996,11 @@ 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) { + identity := authn.IdentityFromContext(ctx) + if identity == nil { + return false, nil + } + trustService := r.TrustService(ctx, obj.ID.TenantID()) trustCenter := compliancepage.CompliancePageFromContext(ctx) @@ -998,11 +1015,6 @@ func (r *trustCenterFileResolver) IsUserAuthorized(ctx context.Context, obj *typ return true, nil } - identity := authn.IdentityFromContext(ctx) - if identity == nil { - return false, gqlutils.Unauthenticatedf(ctx, "unauthenticated") - } - fileAccess, err := trustService.TrustCenterAccesses.LoadTrustCenterFileAccess(ctx, trustCenter.ID, identity.EmailAddress, diff --git a/pkg/server/gqlutils/errors.go b/pkg/server/gqlutils/errors.go index 42cdb82ab..825638001 100644 --- a/pkg/server/gqlutils/errors.go +++ b/pkg/server/gqlutils/errors.go @@ -25,6 +25,19 @@ import ( "go.probo.inc/probo/pkg/validator" ) +func AlreadyAuthenticated(ctx context.Context, err error) *gqlerror.Error { + return &gqlerror.Error{ + Message: "Authentication not allowed for this resource/action", + Extensions: map[string]any{ + "code": "ALREADY_AUTHENTICATED", + }, + } +} + +func AlreadyAuthenticatedf(ctx context.Context, format string, a ...any) *gqlerror.Error { + return AlreadyAuthenticated(ctx, fmt.Errorf(format, a...)) +} + func Unauthenticated(ctx context.Context, err error) *gqlerror.Error { return &gqlerror.Error{ Message: err.Error(), @@ -39,15 +52,6 @@ func Unauthenticatedf(ctx context.Context, format string, a ...any) *gqlerror.Er return Unauthenticated(ctx, fmt.Errorf(format, a...)) } -func AlreadyUnauthenticated(ctx context.Context, err error) *gqlerror.Error { - return &gqlerror.Error{ - Message: "Authentication not allowed for this resource/action", - Extensions: map[string]any{ - "code": "ALREADY_AUTHENTICATED", - }, - } -} - func Forbidden(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 5874e893a..1432e8993 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -149,6 +149,8 @@ func (s *Server) stripTrustPrefix(next http.Handler) http.Handler { } r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix) + r.URL.Path = strings.TrimPrefix(r.URL.Path, "/auth") + if r.URL.Path == "" { r.URL.Path = "/" } diff --git a/pkg/trust/service.go b/pkg/trust/service.go index 2855d1231..9f5ba52b5 100644 --- a/pkg/trust/service.go +++ b/pkg/trust/service.go @@ -229,3 +229,16 @@ func (s *Service) GetByDomainName(ctx context.Context, domain string) (*coredata return trustCenter, err } + +func (s *Service) GetCustomDomainByOrganizationID(ctx context.Context, organizationID gid.GID) (*coredata.CustomDomain, error) { + customDomain := &coredata.CustomDomain{} + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + return customDomain.LoadByOrganizationID(ctx, conn, coredata.NewNoScope(), s.encryptionKey, organizationID) + }, + ) + + return customDomain, err +}