diff --git a/apps/console/src/components/PageError.tsx b/apps/console/src/components/PageError.tsx
index 1c0675c34..3142552b4 100644
--- a/apps/console/src/components/PageError.tsx
+++ b/apps/console/src/components/PageError.tsx
@@ -60,20 +60,6 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
);
}
- if (error instanceof Error && error.message.includes("UNAUTHORIZED")) {
- return (
-
-
-
- {__("Access denied")}
-
-
- {__("You don't have permission to access this organization")}
-
-
- );
- }
-
return (
{__("Unexpected error :(")}
diff --git a/apps/trust/src/layouts/MainLayout.tsx b/apps/trust/src/layouts/MainLayout.tsx
index 140a15003..bf12e4626 100644
--- a/apps/trust/src/layouts/MainLayout.tsx
+++ b/apps/trust/src/layouts/MainLayout.tsx
@@ -8,8 +8,8 @@ import { OrganizationSidebar } from "#/components/OrganizationSidebar";
import { useRequestAccessCallback } from "#/hooks/useRequestAccessCallback";
import { TrustCenterProvider } from "#/providers/TrustCenterProvider";
import { Viewer } from "#/providers/Viewer";
-import type { TrustGraphCurrentQuery } from "#/queries/__generated__/TrustGraphCurrentQuery.graphql";
import { currentTrustGraphQuery } from "#/queries/TrustGraph";
+import type { TrustGraphCurrentQuery } from "#/queries/__generated__/TrustGraphCurrentQuery.graphql";
type Props = {
queryRef: PreloadedQuery;
diff --git a/packages/relay/src/errors.ts b/packages/relay/src/errors.ts
index ee15363a5..450979eb8 100644
--- a/packages/relay/src/errors.ts
+++ b/packages/relay/src/errors.ts
@@ -30,14 +30,6 @@ export class AssumptionRequiredError extends Error {
}
}
-export class UnauthorizedError extends Error {
- constructor(message?: string) {
- super(message || "UNAUTHORIZED");
- this.name = "UnauthorizedError";
- Object.setPrototypeOf(this, UnauthorizedError.prototype);
- }
-}
-
export class ForbiddenError extends Error {
constructor(message?: string) {
super(message || "FORBIDDEN");
diff --git a/packages/relay/src/fetch.ts b/packages/relay/src/fetch.ts
index 1b1d9a4b5..95e86b37f 100644
--- a/packages/relay/src/fetch.ts
+++ b/packages/relay/src/fetch.ts
@@ -2,9 +2,9 @@ import { type FetchFunction } from "relay-runtime";
import {
InternalServerError,
UnAuthenticatedError,
- UnauthorizedError,
ForbiddenError,
AssumptionRequiredError,
+ NDASignatureRequiredError,
} from "./errors";
import { GraphQLError } from "graphql";
@@ -14,8 +14,8 @@ const hasUnauthenticatedError = (error: GraphQLError) =>
const hasAssumptionRequiredError = (error: GraphQLError) =>
error.extensions?.code == "ASSUMPTION_REQUIRED";
-const hasUnauthorizedError = (error: GraphQLError) =>
- error.extensions?.code == "UNAUTHORIZED";
+const hasNDASignatureRequiredError = (error: GraphQLError) =>
+ error.extensions?.code == "NDA_SIGNATURE_REQUIRED";
const hasForbiddenError = (error: GraphQLError) =>
error.extensions?.code == "FORBIDDEN";
@@ -88,9 +88,9 @@ export const makeFetchQuery = (endpoint: string): FetchFunction => {
throw new AssumptionRequiredError(assumptionRequiredError.message)
}
- const unauthorizedError = errors.find(hasUnauthorizedError);
- if (unauthorizedError) {
- throw new UnauthorizedError(unauthorizedError.message);
+ const ndaSignatureRequiredError = errors.find(hasNDASignatureRequiredError);
+ if (ndaSignatureRequiredError) {
+ throw new NDASignatureRequiredError(ndaSignatureRequiredError.message);
}
const forbiddenError = errors.find(hasForbiddenError);
diff --git a/pkg/esign/errors.go b/pkg/esign/errors.go
new file mode 100644
index 000000000..65b7d13be
--- /dev/null
+++ b/pkg/esign/errors.go
@@ -0,0 +1,21 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package esign
+
+import "errors"
+
+var (
+ ErrElectronicSignatureNotFound = errors.New("electronic signature not found")
+)
diff --git a/pkg/esign/service.go b/pkg/esign/service.go
index 14c866fc3..2437a6779 100644
--- a/pkg/esign/service.go
+++ b/pkg/esign/service.go
@@ -17,6 +17,7 @@ package esign
import (
"bytes"
"context"
+ "errors"
"fmt"
"strings"
"time"
@@ -338,6 +339,10 @@ func (s *Service) GetSignatureByID(ctx context.Context, id gid.GID) (*coredata.E
ctx,
func(conn pg.Conn) error {
if err := signature.LoadByID(ctx, conn, scope, id); err != nil {
+ if errors.Is(err, coredata.ErrResourceNotFound) {
+ return ErrElectronicSignatureNotFound
+ }
+
return fmt.Errorf("cannot load electronic signature: %w", err)
}
diff --git a/pkg/server/api/compliancepage/nda_middleware.go b/pkg/server/api/compliancepage/nda_middleware.go
new file mode 100644
index 000000000..9d15a1ba6
--- /dev/null
+++ b/pkg/server/api/compliancepage/nda_middleware.go
@@ -0,0 +1,102 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+package compliancepage
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/99designs/gqlgen/graphql"
+ "github.com/vektah/gqlparser/v2/gqlerror"
+ "go.gearno.de/kit/httpserver"
+ "go.gearno.de/kit/log"
+ "go.probo.inc/probo/pkg/coredata"
+ "go.probo.inc/probo/pkg/esign"
+ "go.probo.inc/probo/pkg/server/gqlutils"
+ "go.probo.inc/probo/pkg/trust"
+)
+
+func NewNDAMiddleware(trustSvc *trust.Service, esignSvc *esign.Service, logger *log.Logger) func(next http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ membership := ComplianceMembershipFromContext(ctx)
+
+ if membership == nil {
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ compliancePage := CompliancePageFromContext(ctx)
+
+ if _, err := trustSvc.GetNDAFile(ctx, compliancePage.ID); err != nil {
+ if errors.Is(err, trust.ErrNDAFileNotFound) {
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ logger.ErrorCtx(ctx, "cannot get NDA file", log.Error(err))
+ httpserver.RenderJSON(
+ w,
+ http.StatusInternalServerError,
+ &graphql.Response{
+ Errors: gqlerror.List{
+ gqlutils.Internal(ctx),
+ },
+ },
+ )
+ return
+ }
+
+ if membership.ElectronicSignatureID == nil {
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ sig, err := esignSvc.GetSignatureByID(ctx, *membership.ElectronicSignatureID)
+ if err != nil {
+ logger.ErrorCtx(ctx, "cannot get NDA signature", log.Error(err))
+ httpserver.RenderJSON(
+ w,
+ http.StatusInternalServerError,
+ &graphql.Response{
+ Errors: gqlerror.List{
+ gqlutils.Internal(ctx),
+ },
+ },
+ )
+ return
+ }
+
+ if sig.Status != coredata.ElectronicSignatureStatusCompleted {
+ httpserver.RenderJSON(
+ w,
+ http.StatusForbidden,
+ &graphql.Response{
+ Errors: gqlerror.List{
+ gqlutils.NDASignatureRequiredf(ctx, "NDA signature required"),
+ },
+ },
+ )
+ return
+ }
+
+ next.ServeHTTP(w, r)
+ },
+ )
+ }
+}
diff --git a/pkg/server/api/trust/v1/resolver.go b/pkg/server/api/trust/v1/resolver.go
index dcdd058b6..4d1145233 100644
--- a/pkg/server/api/trust/v1/resolver.go
+++ b/pkg/server/api/trust/v1/resolver.go
@@ -88,6 +88,7 @@ func NewMux(
r.Use(compliancepage.NewCompliancePagePresenceMiddleware())
r.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig))
r.Use(compliancepage.NewMembershipMiddleware(trustSvc, logger))
+ r.Use(compliancepage.NewNDAMiddleware(trustSvc, esignSvc, logger))
graphqlHandler := NewGraphQLHandler(iamSvc, trustSvc, esignSvc, logger, baseURL, cookieConfig)
diff --git a/pkg/server/api/trust/v1/v1_resolver.go b/pkg/server/api/trust/v1/v1_resolver.go
index 48834649d..c6f7dbe38 100644
--- a/pkg/server/api/trust/v1/v1_resolver.go
+++ b/pkg/server/api/trust/v1/v1_resolver.go
@@ -289,10 +289,6 @@ func (r *mutationResolver) ExportDocumentPDF(ctx context.Context, input types.Ex
return nil, gqlutils.Forbiddenf(ctx, "access denied: no permission to access this document")
}
- if err := r.checkNDASignature(ctx, trustCenter, identity); err != nil {
- return nil, err
- }
-
pdf, err := trustService.Documents.ExportPDF(ctx, input.DocumentID, identity.EmailAddress)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot export document PDF", log.Error(err))
@@ -347,10 +343,6 @@ func (r *mutationResolver) ExportReportPDF(ctx context.Context, input types.Expo
return nil, gqlutils.Forbiddenf(ctx, "access denied: no permission to access this report")
}
- if err := r.checkNDASignature(ctx, trustCenter, identity); err != nil {
- return nil, err
- }
-
pdf, err := trustService.Reports.ExportPDF(ctx, input.ReportID, identity.EmailAddress)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot export report PDF", log.Error(err))
@@ -403,10 +395,6 @@ func (r *mutationResolver) ExportTrustCenterFile(ctx context.Context, input type
return nil, gqlutils.Forbiddenf(ctx, "access denied: no permission to access this file")
}
- if err := r.checkNDASignature(ctx, trustCenter, identity); err != nil {
- return nil, err
- }
-
fileData, err := trustService.TrustCenterFiles.ExportFile(ctx, input.TrustCenterFileID, identity.EmailAddress)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot export trust center file", log.Error(err))
diff --git a/pkg/server/gqlutils/errors.go b/pkg/server/gqlutils/errors.go
index 4f7307efd..5dce8027c 100644
--- a/pkg/server/gqlutils/errors.go
+++ b/pkg/server/gqlutils/errors.go
@@ -66,6 +66,20 @@ func AssumptionRequiredf(ctx context.Context, format string, a ...any) *gqlerror
return AssumptionRequired(ctx, fmt.Errorf(format, a...))
}
+func NDASignatureRequired(ctx context.Context, err error) *gqlerror.Error {
+ return &gqlerror.Error{
+ Message: err.Error(),
+ Path: graphql.GetPath(ctx),
+ Extensions: map[string]any{
+ "code": "NDA_SIGNATURE_REQUIRED",
+ },
+ }
+}
+
+func NDASignatureRequiredf(ctx context.Context, format string, a ...any) *gqlerror.Error {
+ return NDASignatureRequired(ctx, fmt.Errorf(format, a...))
+}
+
func Forbidden(ctx context.Context, err error) *gqlerror.Error {
return &gqlerror.Error{
Message: err.Error(),
diff --git a/pkg/trust/errors.go b/pkg/trust/errors.go
index 3a73989e5..135fb906d 100644
--- a/pkg/trust/errors.go
+++ b/pkg/trust/errors.go
@@ -22,4 +22,5 @@ var (
ErrMembershipNotFound = errors.New("membership not found")
ErrMembershipInactive = errors.New("membership inactive")
ErrDocumentAccessNotFound = errors.New("document access not found")
+ ErrNDAFileNotFound = errors.New("NDA file not found")
)
diff --git a/pkg/trust/service.go b/pkg/trust/service.go
index ef1dccff9..53c651b41 100644
--- a/pkg/trust/service.go
+++ b/pkg/trust/service.go
@@ -300,3 +300,43 @@ func (s *Service) GetMembershipByCompliancePageIDAndEmail(ctx context.Context, c
return membership, nil
}
+
+func (s *Service) GetNDAFile(
+ ctx context.Context,
+ compliancePageID gid.GID,
+) (*coredata.File, error) {
+ var (
+ file *coredata.File
+ scope = coredata.NewScopeFromObjectID(compliancePageID)
+ )
+
+ err := s.pg.WithConn(
+ ctx,
+ func(conn pg.Conn) error {
+ trustCenter := &coredata.TrustCenter{}
+ if err := trustCenter.LoadByID(ctx, conn, scope, compliancePageID); err != nil {
+ return fmt.Errorf("cannot load trust center: %w", err)
+ }
+
+ if trustCenter.NonDisclosureAgreementFileID == nil {
+ return nil
+ }
+
+ file = &coredata.File{}
+ if err := file.LoadByID(ctx, conn, scope, *trustCenter.NonDisclosureAgreementFileID); err != nil {
+ if errors.Is(err, coredata.ErrResourceNotFound) {
+ return ErrNDAFileNotFound
+ }
+
+ return fmt.Errorf("cannot load file: %w", err)
+ }
+
+ return nil
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ return file, nil
+}