diff --git a/apps/trust/src/layouts/MainLayout.tsx b/apps/trust/src/layouts/MainLayout.tsx index 10fe501ef..ea2f06a19 100644 --- a/apps/trust/src/layouts/MainLayout.tsx +++ b/apps/trust/src/layouts/MainLayout.tsx @@ -28,7 +28,7 @@ export function MainLayout(props: Props) { } const nda = trustCenter.nonDisclosureAgreement; - const hasPendingNDA = nda + const hasPendingNDA = data.viewer && nda && (!nda.viewerSignature || nda.viewerSignature.status === "PENDING" || nda.viewerSignature.status === "FAILED"); if (hasPendingNDA) { diff --git a/apps/trust/src/pages/NDAPage.tsx b/apps/trust/src/pages/NDAPage.tsx index c1271bcc3..4bc0703be 100644 --- a/apps/trust/src/pages/NDAPage.tsx +++ b/apps/trust/src/pages/NDAPage.tsx @@ -47,7 +47,7 @@ const ndaPageFragment = graphql` fragment NDAPageFragment on TrustCenter @refetchable(queryName: "NDAPageRefetchQuery") { nonDisclosureAgreement @required(action: THROW) { - viewerSignature @required(action: THROW) { + viewerSignature { id status consentText @@ -124,11 +124,11 @@ export function NDAPage(props: { ); const isProcessing - = ndaSignature.status === "ACCEPTED" - || ndaSignature.status === "PROCESSING"; + = ndaSignature?.status === "ACCEPTED" + || ndaSignature?.status === "PROCESSING"; - const isFailed = ndaSignature.status === "FAILED"; - const isCompleted = ndaSignature.status === "COMPLETED"; + const isFailed = ndaSignature?.status === "FAILED"; + const isCompleted = ndaSignature?.status === "COMPLETED"; useEffect(() => { if (isCompleted) { @@ -200,11 +200,11 @@ export function NDAPage(props: { }); const nda = trustCenter.nonDisclosureAgreement; - if ( - !nda - || !nda.viewerSignature - || nda.viewerSignature.status === "COMPLETED" - ) { + if (!viewer) { + return ; + } + + if (!nda || !nda.viewerSignature || nda.viewerSignature.status === "COMPLETED") { return ; } diff --git a/apps/trust/src/pages/NDAPageLoader.tsx b/apps/trust/src/pages/NDAPageLoader.tsx index ea7f5bd9e..52389997f 100644 --- a/apps/trust/src/pages/NDAPageLoader.tsx +++ b/apps/trust/src/pages/NDAPageLoader.tsx @@ -1,4 +1,4 @@ -import { Suspense, useEffect } from "react"; +import { useEffect } from "react"; import { useQueryLoader } from "react-relay"; import { RelayProvider } from "#/providers/RelayProviders"; diff --git a/apps/trust/src/pages/__generated__/NDAPageFragment.graphql.ts b/apps/trust/src/pages/__generated__/NDAPageFragment.graphql.ts index 1915f4026..b545091fe 100644 --- a/apps/trust/src/pages/__generated__/NDAPageFragment.graphql.ts +++ b/apps/trust/src/pages/__generated__/NDAPageFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<99908411212decf464eafa45b1fc0e04>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -19,7 +19,7 @@ export type NDAPageFragment$data = { readonly id: string; readonly lastError: string | null | undefined; readonly status: ElectronicSignatureStatus; - }; + } | null | undefined; }; readonly " $fragmentType": "NDAPageFragment"; }; @@ -67,41 +67,37 @@ return { "plural": false, "selections": [ { - "kind": "RequiredField", - "field": { - "alias": null, - "args": null, - "concreteType": "ElectronicSignature", - "kind": "LinkedField", - "name": "viewerSignature", - "plural": false, - "selections": [ - (v0/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "status", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "consentText", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "lastError", - "storageKey": null - } - ], - "storageKey": null - }, - "action": "THROW" + "alias": null, + "args": null, + "concreteType": "ElectronicSignature", + "kind": "LinkedField", + "name": "viewerSignature", + "plural": false, + "selections": [ + (v0/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "consentText", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "lastError", + "storageKey": null + } + ], + "storageKey": null } ], "storageKey": null @@ -115,6 +111,6 @@ return { }; })(); -(node as any).hash = "503859b8c00b27214f9f3253bffcb670"; +(node as any).hash = "37f6ca36229313e341797c28d789035d"; export default node; diff --git a/apps/trust/src/pages/__generated__/NDAPageRefetchQuery.graphql.ts b/apps/trust/src/pages/__generated__/NDAPageRefetchQuery.graphql.ts index e1158a4f9..67d45ca30 100644 --- a/apps/trust/src/pages/__generated__/NDAPageRefetchQuery.graphql.ts +++ b/apps/trust/src/pages/__generated__/NDAPageRefetchQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<5c6134b4d6615f24a63ea534c80e76c1>> + * @generated SignedSource<<2b8f94503fe145fb141255a7ba79ddac>> * @lightSyntaxTransform * @nogrep */ @@ -161,6 +161,6 @@ return { }; })(); -(node as any).hash = "503859b8c00b27214f9f3253bffcb670"; +(node as any).hash = "37f6ca36229313e341797c28d789035d"; export default node; diff --git a/pkg/server/api/trust/v1/v1_resolver.go b/pkg/server/api/trust/v1/v1_resolver.go index 4c91126cd..500c2b311 100644 --- a/pkg/server/api/trust/v1/v1_resolver.go +++ b/pkg/server/api/trust/v1/v1_resolver.go @@ -174,7 +174,7 @@ func (r *mutationResolver) SendMagicLink(ctx context.Context, input types.SendMa // 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) + identity, session, err := r.iam.AuthService.OpenSessionWithMagicLink(ctx, input.Token) if err != nil { var errInvalidToken *iam.ErrInvalidToken if errors.As(err, &errInvalidToken) { @@ -185,6 +185,13 @@ func (r *mutationResolver) VerifyMagicLink(ctx context.Context, input types.Veri return nil, gqlutils.Internal(ctx) } + trustCenter := compliancepage.CompliancePageFromContext(ctx) + trustService := r.TrustService(ctx, trustCenter.ID.TenantID()) + + if _, err := trustService.TrustCenterAccesses.EnsureAccess(ctx, trustCenter.ID, identity.EmailAddress, identity.FullName); err != nil { + r.logger.ErrorCtx(ctx, "cannot ensure trust center access", log.Error(err)) + } + w := gqlutils.HTTPResponseWriterFromContext(ctx) r.sessionCookie.Set(w, session) diff --git a/pkg/trust/trust_center_access_service.go b/pkg/trust/trust_center_access_service.go index 0db27e036..4d2d08cc9 100644 --- a/pkg/trust/trust_center_access_service.go +++ b/pkg/trust/trust_center_access_service.go @@ -60,6 +60,87 @@ func (tcar *TrustCenterAccessRequest) Validate() error { return v.Error() } +func (s TrustCenterAccessService) ensureAccessInTx( + ctx context.Context, + tx pg.Conn, + trustCenterID gid.GID, + email mail.Addr, + fullName string, +) (*coredata.TrustCenterAccess, *coredata.TrustCenter, error) { + now := time.Now() + + trustCenter := &coredata.TrustCenter{} + if err := trustCenter.LoadByID(ctx, tx, s.svc.scope, trustCenterID); err != nil { + return nil, nil, fmt.Errorf("cannot load trust center: %w", err) + } + + existingAccess := &coredata.TrustCenterAccess{} + err := existingAccess.LoadByTrustCenterIDAndEmail(ctx, tx, s.svc.scope, trustCenterID, email) + if err == nil { + return existingAccess, trustCenter, nil + } + + if !errors.Is(err, coredata.ErrResourceNotFound) { + return nil, nil, fmt.Errorf("cannot load trust center access: %w", err) + } + + access := &coredata.TrustCenterAccess{ + ID: gid.New(s.svc.scope.GetTenantID(), coredata.TrustCenterAccessEntityType), + OrganizationID: trustCenter.OrganizationID, + TenantID: s.svc.scope.GetTenantID(), + TrustCenterID: trustCenterID, + Email: email, + Name: fullName, + State: coredata.TrustCenterAccessStateActive, + HasAcceptedNonDisclosureAgreement: false, + CreatedAt: now, + UpdatedAt: now, + } + + if trustCenter.NonDisclosureAgreementFileID != nil && s.svc.esign != nil { + sig, err := s.svc.esign.CreateSignature( + ctx, + tx, + &esign.CreateSignatureRequest{ + OrganizationID: access.OrganizationID, + DocumentType: coredata.ElectronicSignatureDocumentTypeNDA, + FileID: *trustCenter.NonDisclosureAgreementFileID, + SignerEmail: access.Email, + }, + ) + if err != nil { + return nil, nil, fmt.Errorf("cannot create pending signature: %w", err) + } + access.ElectronicSignatureID = &sig.ID + } + + if err := access.Insert(ctx, tx, s.svc.scope); err != nil { + return nil, nil, fmt.Errorf("cannot insert trust center access: %w", err) + } + + return access, trustCenter, nil +} + +func (s TrustCenterAccessService) EnsureAccess( + ctx context.Context, + trustCenterID gid.GID, + email mail.Addr, + fullName string, +) (*coredata.TrustCenterAccess, error) { + var access *coredata.TrustCenterAccess + + err := s.svc.pg.WithTx( + ctx, + func(tx pg.Conn) error { + var err error + access, _, err = s.ensureAccessInTx(ctx, tx, trustCenterID, email, fullName) + return err + }, + ) + + return access, err +} + func (s TrustCenterAccessService) Request( ctx context.Context, req *TrustCenterAccessRequest, @@ -69,20 +150,20 @@ func (s TrustCenterAccessService) Request( } var ( - now = time.Now() - access *coredata.TrustCenterAccess - trustCenter *coredata.TrustCenter + now = time.Now() + access *coredata.TrustCenterAccess ) err := s.svc.pg.WithTx( ctx, func(tx pg.Conn) error { - trustCenter = &coredata.TrustCenter{} - if err := trustCenter.LoadByID(ctx, tx, s.svc.scope, req.TrustCenterID); err != nil { - return fmt.Errorf("cannot load trust center: %w", err) - } + var trustCenter *coredata.TrustCenter + var err error - // TODO: load document to ensure they are requestable + access, trustCenter, err = s.ensureAccessInTx(ctx, tx, req.TrustCenterID, req.Email, req.FullName) + if err != nil { + return err + } organizationID := trustCenter.OrganizationID @@ -132,50 +213,6 @@ func (s TrustCenterAccessService) Request( } } - existingAccess := &coredata.TrustCenterAccess{} - err := existingAccess.LoadByTrustCenterIDAndEmail(ctx, tx, s.svc.scope, req.TrustCenterID, req.Email) - if err == nil { - access = existingAccess - } else { - if !errors.Is(err, coredata.ErrResourceNotFound) { - return fmt.Errorf("cannot load trust center access: %w", err) - } - - access = &coredata.TrustCenterAccess{ - ID: gid.New(s.svc.scope.GetTenantID(), coredata.TrustCenterAccessEntityType), - OrganizationID: organizationID, - TenantID: s.svc.scope.GetTenantID(), - TrustCenterID: req.TrustCenterID, - Email: req.Email, - Name: req.FullName, - State: coredata.TrustCenterAccessStateActive, - HasAcceptedNonDisclosureAgreement: false, - CreatedAt: now, - UpdatedAt: now, - } - - if trustCenter.NonDisclosureAgreementFileID != nil && s.svc.esign != nil { - sig, err := s.svc.esign.CreateSignature( - ctx, - tx, - &esign.CreateSignatureRequest{ - OrganizationID: access.OrganizationID, - DocumentType: coredata.ElectronicSignatureDocumentTypeNDA, - FileID: *trustCenter.NonDisclosureAgreementFileID, - SignerEmail: access.Email, - }, - ) - if err != nil { - return fmt.Errorf("cannot create pending signature: %w", err) - } - access.ElectronicSignatureID = &sig.ID - } - - if err := access.Insert(ctx, tx, s.svc.scope); err != nil { - return fmt.Errorf("cannot insert trust center access: %w", err) - } - } - var existingAccesses coredata.TrustCenterDocumentAccesses if err := existingAccesses.LoadAllByTrustCenterAccessID(ctx, tx, s.svc.scope, access.ID); err != nil { return fmt.Errorf("cannot load existing access records: %w", err)