Add document viewer with proper 404 handling for trust center
Move document download/view to a dedicated viewer page with PDF preview, access request flow, and a proper 404 error boundary when documents are not found. The backend now returns NOT_FOUND instead of INTERNAL for missing documents and reports. Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
2
.github/workflows/make.yaml
vendored
2
.github/workflows/make.yaml
vendored
@@ -153,8 +153,6 @@ jobs:
|
||||
name: "build-artifacts"
|
||||
- run: "chmod +x bin/probod"
|
||||
- run: "make generate"
|
||||
- run: "make go-fmt"
|
||||
- run: "make go-fix"
|
||||
- name: "Run go vet"
|
||||
run: "go vet ./..."
|
||||
- name: "Run golangci-lint"
|
||||
|
||||
20
GNUmakefile
20
GNUmakefile
@@ -59,7 +59,7 @@ endif
|
||||
all: build
|
||||
|
||||
.PHONY: lint
|
||||
lint: vet go-fmt go-fix go-lint npm-lint
|
||||
lint: vet go-lint npm-lint
|
||||
|
||||
.PHONY: vet
|
||||
vet: generate apps/console/dist/index.html apps/trust/dist/index.html @probo/emails
|
||||
@@ -69,24 +69,6 @@ vet: generate apps/console/dist/index.html apps/trust/dist/index.html @probo/ema
|
||||
npm-lint:
|
||||
$(NPM) run lint
|
||||
|
||||
.PHONY: go-fmt
|
||||
go-fmt: generate
|
||||
@output="$$(gofmt -l cmd pkg e2e)"; \
|
||||
if [ -n "$$output" ]; then \
|
||||
echo "error: 'gofmt' found unformatted files:"; \
|
||||
echo "$$output"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: go-fix
|
||||
go-fix: generate
|
||||
@output="$$($(GO_BASE) fix -diff ./cmd/... ./pkg/... ./e2e/...)"; \
|
||||
if [ -n "$$output" ]; then \
|
||||
echo "error: 'go fix' suggests changes; please apply them"; \
|
||||
echo "$$output"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: go-lint
|
||||
go-lint: generate
|
||||
$(GOLINTCMD) run ./...
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
(function () {
|
||||
var m = location.pathname.match(/^\/trust\/[^/]+/);
|
||||
if (m) {
|
||||
var b = document.createElement("base");
|
||||
b.href = m[0] + "/";
|
||||
document.head.appendChild(b);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- <link rel="apple-touch-icon-precomposed" sizes="57x57" href="/favicons/apple-touch-icon-57x57.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="/favicons/apple-touch-icon-114x114.png" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { downloadFile, formatError } from "@probo/helpers";
|
||||
import { formatError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { UnAuthenticatedError } from "@probo/relay";
|
||||
import {
|
||||
@@ -7,10 +7,9 @@ import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
FrameworkLogo,
|
||||
IconArrowInbox,
|
||||
IconArrowLink,
|
||||
IconLock,
|
||||
IconMedal,
|
||||
Spinner,
|
||||
Table,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
@@ -19,11 +18,9 @@ import { useFragment, useMutation } from "react-relay";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import { useMutationWithToasts } from "#/hooks/useMutationWithToast";
|
||||
import { getPathPrefix } from "#/utils/pathPrefix";
|
||||
|
||||
import type { AuditRow_requestAccessMutation } from "./__generated__/AuditRow_requestAccessMutation.graphql";
|
||||
import type { AuditRowDownloadMutation } from "./__generated__/AuditRowDownloadMutation.graphql";
|
||||
import type { AuditRowFragment$key } from "./__generated__/AuditRowFragment.graphql";
|
||||
|
||||
const requestAccessMutation = graphql`
|
||||
@@ -41,20 +38,11 @@ const requestAccessMutation = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
const downloadMutation = graphql`
|
||||
mutation AuditRowDownloadMutation($input: ExportReportPDFInput!) {
|
||||
exportReportPDF(input: $input) {
|
||||
data
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const auditRowFragment = graphql`
|
||||
fragment AuditRowFragment on Audit {
|
||||
name
|
||||
report {
|
||||
id
|
||||
filename
|
||||
isUserAuthorized
|
||||
access {
|
||||
id
|
||||
@@ -82,8 +70,6 @@ export function AuditRow(props: { audit: AuditRowFragment$key }) {
|
||||
|
||||
const [requestAccess, isRequestingAccess]
|
||||
= useMutation<AuditRow_requestAccessMutation>(requestAccessMutation);
|
||||
const [commitDownload, downloading]
|
||||
= useMutationWithToasts<AuditRowDownloadMutation>(downloadMutation);
|
||||
|
||||
const handleRequestAccess = () => {
|
||||
requestAccess({
|
||||
@@ -129,22 +115,6 @@ export function AuditRow(props: { audit: AuditRowFragment$key }) {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!audit.report?.id) {
|
||||
return;
|
||||
}
|
||||
await commitDownload({
|
||||
variables: {
|
||||
input: {
|
||||
reportId: audit.report.id,
|
||||
},
|
||||
},
|
||||
onSuccess(response) {
|
||||
downloadFile(response.exportReportPDF.data, audit.report!.filename);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-sm border border-border-solid -mt-px flex gap-3 flex-col md:flex-row md:justify-between px-6 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -156,11 +126,10 @@ export function AuditRow(props: { audit: AuditRowFragment$key }) {
|
||||
<Button
|
||||
className="w-full md:w-max"
|
||||
variant="secondary"
|
||||
disabled={downloading}
|
||||
icon={downloading ? Spinner : IconArrowInbox}
|
||||
onClick={() => void handleDownload()}
|
||||
icon={IconArrowLink}
|
||||
to={`/documents/${audit.report.id}`}
|
||||
>
|
||||
{downloading ? __("Downloading") : __("Download")}
|
||||
{__("View")}
|
||||
</Button>
|
||||
)
|
||||
: (
|
||||
|
||||
103
apps/trust/src/components/DocumentPageErrorBoundary.tsx
Normal file
103
apps/trust/src/components/DocumentPageErrorBoundary.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// 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.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
FullNameRequiredError,
|
||||
NDASignatureRequiredError,
|
||||
UnAuthenticatedError,
|
||||
} from "@probo/relay";
|
||||
import { Button, IconChevronLeft, IconPageCross } from "@probo/ui";
|
||||
import { Link, Navigate, useLocation, useRouteError } from "react-router";
|
||||
|
||||
import { getPathPrefix } from "#/utils/pathPrefix";
|
||||
|
||||
export function DocumentPageErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
const location = useLocation();
|
||||
const { __ } = useTranslate();
|
||||
|
||||
const search = new URLSearchParams();
|
||||
|
||||
if (location.pathname !== (getPathPrefix() || "/") || location.search !== "") {
|
||||
search.set("continue", window.location.href);
|
||||
}
|
||||
|
||||
const queryString = search.toString();
|
||||
|
||||
if (error instanceof UnAuthenticatedError) {
|
||||
return (
|
||||
<Navigate
|
||||
replace
|
||||
to={{
|
||||
pathname: "/connect",
|
||||
search: queryString ? "?" + queryString : "",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof FullNameRequiredError) {
|
||||
return (
|
||||
<Navigate
|
||||
replace
|
||||
to={{
|
||||
pathname: "/full-name",
|
||||
search: queryString ? "?" + queryString : "",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof NDASignatureRequiredError) {
|
||||
return (
|
||||
<Navigate
|
||||
replace
|
||||
to={{
|
||||
pathname: "/nda",
|
||||
search: queryString ? "?" + queryString : "",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-level-2">
|
||||
<header className="flex items-center h-12 gap-3 border-b border-border-solid px-4 flex-none bg-level-1">
|
||||
<Link
|
||||
to="/documents"
|
||||
className="size-8 grid place-items-center hover:bg-secondary-hover rounded-sm transition-all"
|
||||
>
|
||||
<IconChevronLeft size={16} />
|
||||
</Link>
|
||||
</header>
|
||||
<main className="flex-1 min-h-0 flex items-center justify-center">
|
||||
<div className="text-center max-w-sm">
|
||||
<IconPageCross size={32} className="mx-auto text-txt-tertiary mb-4" />
|
||||
<h2 className="text-lg font-medium mb-2">
|
||||
{__("Document not found")}
|
||||
</h2>
|
||||
<p className="text-sm text-txt-secondary mb-6">
|
||||
{__("The document you are looking for does not exist or has been removed.")}
|
||||
</p>
|
||||
<Button variant="secondary" asChild className="inline-flex">
|
||||
<Link to="/documents">
|
||||
{__("Back to documents")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +1,20 @@
|
||||
import { downloadFile, formatError } from "@probo/helpers";
|
||||
import { formatError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { UnAuthenticatedError } from "@probo/relay";
|
||||
import {
|
||||
Button,
|
||||
IconArrowInbox,
|
||||
IconArrowLink,
|
||||
IconLock,
|
||||
IconPageTextLine,
|
||||
Spinner,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import { useFragment, useMutation } from "react-relay";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import { useMutationWithToasts } from "#/hooks/useMutationWithToast";
|
||||
import { getPathPrefix } from "#/utils/pathPrefix";
|
||||
|
||||
import type { DocumentRow_requestAccessMutation } from "./__generated__/DocumentRow_requestAccessMutation.graphql";
|
||||
import type { DocumentRowDownloadMutation } from "./__generated__/DocumentRowDownloadMutation.graphql";
|
||||
import type { DocumentRowFragment$key } from "./__generated__/DocumentRowFragment.graphql";
|
||||
|
||||
const requestAccessMutation = graphql`
|
||||
@@ -35,14 +32,6 @@ const requestAccessMutation = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
const downloadMutation = graphql`
|
||||
mutation DocumentRowDownloadMutation($input: ExportDocumentPDFInput!) {
|
||||
exportDocumentPDF(input: $input) {
|
||||
data
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const documentRowFragment = graphql`
|
||||
fragment DocumentRowFragment on Document {
|
||||
id
|
||||
@@ -67,8 +56,6 @@ export function DocumentRow(props: { document: DocumentRowFragment$key }) {
|
||||
|
||||
const [requestAccess, isRequestingAccess]
|
||||
= useMutation<DocumentRow_requestAccessMutation>(requestAccessMutation);
|
||||
const [commitDownload, downloading]
|
||||
= useMutationWithToasts<DocumentRowDownloadMutation>(downloadMutation);
|
||||
|
||||
const handleRequestAccess = () => {
|
||||
requestAccess({
|
||||
@@ -114,19 +101,6 @@ export function DocumentRow(props: { document: DocumentRowFragment$key }) {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
await commitDownload({
|
||||
variables: {
|
||||
input: {
|
||||
documentId: document.id,
|
||||
},
|
||||
},
|
||||
onSuccess(response) {
|
||||
downloadFile(response.exportDocumentPDF.data, document.title);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-sm border border-border-solid -mt-px flex gap-3 flex-col md:flex-row md:justify-between px-6 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -138,11 +112,10 @@ export function DocumentRow(props: { document: DocumentRowFragment$key }) {
|
||||
<Button
|
||||
className="w-full md:w-max"
|
||||
variant="secondary"
|
||||
disabled={downloading}
|
||||
icon={downloading ? Spinner : IconArrowInbox}
|
||||
onClick={() => void handleDownload()}
|
||||
icon={IconArrowLink}
|
||||
onClick={() => void navigate(`/documents/${document.id}`)}
|
||||
>
|
||||
{downloading ? __("Downloading") : __("Download")}
|
||||
{__("View")}
|
||||
</Button>
|
||||
)
|
||||
: (
|
||||
|
||||
@@ -32,7 +32,7 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
|
||||
}
|
||||
}, [location, resetErrorBoundary]);
|
||||
|
||||
if (!error || (error instanceof Error && error.message.includes("PAGE_NOT_FOUND"))) {
|
||||
if (!error) {
|
||||
return (
|
||||
<div className={classNames.wrapper}>
|
||||
<h1 className={classNames.title}>
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
import { downloadFile, formatError } from "@probo/helpers";
|
||||
import { formatError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { UnAuthenticatedError } from "@probo/relay";
|
||||
import {
|
||||
Button,
|
||||
IconArrowInbox,
|
||||
IconArrowLink,
|
||||
IconLock,
|
||||
IconPageTextLine,
|
||||
Spinner,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import { useFragment, useMutation } from "react-relay";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import { useMutationWithToasts } from "#/hooks/useMutationWithToast";
|
||||
import { getPathPrefix } from "#/utils/pathPrefix";
|
||||
|
||||
import type { TrustCenterFileRow_requestAccessMutation } from "./__generated__/TrustCenterFileRow_requestAccessMutation.graphql";
|
||||
import type { TrustCenterFileRowDownloadMutation } from "./__generated__/TrustCenterFileRowDownloadMutation.graphql";
|
||||
import type { TrustCenterFileRowFragment$key } from "./__generated__/TrustCenterFileRowFragment.graphql";
|
||||
|
||||
const requestAccessMutation = graphql`
|
||||
@@ -35,16 +32,6 @@ const requestAccessMutation = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
const downloadMutation = graphql`
|
||||
mutation TrustCenterFileRowDownloadMutation(
|
||||
$input: ExportTrustCenterFileInput!
|
||||
) {
|
||||
exportTrustCenterFile(input: $input) {
|
||||
data
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const trustCenterFileRowFragment = graphql`
|
||||
fragment TrustCenterFileRowFragment on TrustCenterFile {
|
||||
id
|
||||
@@ -73,8 +60,6 @@ export function TrustCenterFileRow(props: {
|
||||
= useMutation<TrustCenterFileRow_requestAccessMutation>(
|
||||
requestAccessMutation,
|
||||
);
|
||||
const [commitDownload, downloading]
|
||||
= useMutationWithToasts<TrustCenterFileRowDownloadMutation>(downloadMutation);
|
||||
|
||||
const handleRequestAccess = () => {
|
||||
requestAccess({
|
||||
@@ -120,19 +105,6 @@ export function TrustCenterFileRow(props: {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
await commitDownload({
|
||||
variables: {
|
||||
input: {
|
||||
trustCenterFileId: file.id,
|
||||
},
|
||||
},
|
||||
onSuccess(response) {
|
||||
downloadFile(response.exportTrustCenterFile.data, file.name);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-sm border border-border-solid -mt-px flex gap-3 flex-col md:flex-row md:justify-between px-6 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -144,11 +116,10 @@ export function TrustCenterFileRow(props: {
|
||||
<Button
|
||||
className="w-full md:w-max"
|
||||
variant="secondary"
|
||||
disabled={downloading}
|
||||
icon={downloading ? Spinner : IconArrowInbox}
|
||||
onClick={() => void handleDownload()}
|
||||
icon={IconArrowLink}
|
||||
onClick={() => void navigate(`/documents/${file.id}`)}
|
||||
>
|
||||
{downloading ? __("Downloading") : __("Download")}
|
||||
{__("View")}
|
||||
</Button>
|
||||
)
|
||||
: (
|
||||
|
||||
481
apps/trust/src/pages/DocumentPage.tsx
Normal file
481
apps/trust/src/pages/DocumentPage.tsx
Normal file
@@ -0,0 +1,481 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// 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.
|
||||
|
||||
import { formatError } from "@probo/helpers";
|
||||
import { useSystemTheme } from "@probo/hooks";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { UnAuthenticatedError } from "@probo/relay";
|
||||
import {
|
||||
Button,
|
||||
IconArrowDown,
|
||||
IconChevronLeft,
|
||||
IconLock,
|
||||
Spinner,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
type PreloadedQuery,
|
||||
useMutation,
|
||||
usePreloadedQuery,
|
||||
} from "react-relay";
|
||||
import { Link, useLocation, useNavigate, useSearchParams } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import { PDFPreview } from "#/components/PDFPreview";
|
||||
import { getPathPrefix } from "#/utils/pathPrefix";
|
||||
|
||||
import type { DocumentPageExportDocumentMutation } from "./__generated__/DocumentPageExportDocumentMutation.graphql";
|
||||
import type { DocumentPageExportReportMutation } from "./__generated__/DocumentPageExportReportMutation.graphql";
|
||||
import type { DocumentPageExportTrustCenterFileMutation } from "./__generated__/DocumentPageExportTrustCenterFileMutation.graphql";
|
||||
import type { DocumentPageQuery as DocumentPageQueryType } from "./__generated__/DocumentPageQuery.graphql";
|
||||
import type { DocumentPageRequestDocumentAccessMutation } from "./__generated__/DocumentPageRequestDocumentAccessMutation.graphql";
|
||||
import type { DocumentPageRequestReportAccessMutation } from "./__generated__/DocumentPageRequestReportAccessMutation.graphql";
|
||||
import type { DocumentPageRequestTrustCenterFileAccessMutation } from "./__generated__/DocumentPageRequestTrustCenterFileAccessMutation.graphql";
|
||||
|
||||
export const documentPageQuery = graphql`
|
||||
query DocumentPageQuery($id: ID!) {
|
||||
currentTrustCenter {
|
||||
logoFileUrl
|
||||
darkLogoFileUrl
|
||||
}
|
||||
node(id: $id) @required(action: THROW) {
|
||||
__typename
|
||||
... on Document {
|
||||
id
|
||||
title
|
||||
isUserAuthorized
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
... on TrustCenterFile {
|
||||
id
|
||||
name
|
||||
isUserAuthorized
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
... on Report {
|
||||
id
|
||||
filename
|
||||
isUserAuthorized
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const exportDocumentMutation = graphql`
|
||||
mutation DocumentPageExportDocumentMutation(
|
||||
$input: ExportDocumentPDFInput!
|
||||
) {
|
||||
exportDocumentPDF(input: $input) {
|
||||
data
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const exportTrustCenterFileMutation = graphql`
|
||||
mutation DocumentPageExportTrustCenterFileMutation(
|
||||
$input: ExportTrustCenterFileInput!
|
||||
) {
|
||||
exportTrustCenterFile(input: $input) {
|
||||
data
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const exportReportMutation = graphql`
|
||||
mutation DocumentPageExportReportMutation(
|
||||
$input: ExportReportPDFInput!
|
||||
) {
|
||||
exportReportPDF(input: $input) {
|
||||
data
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const requestDocumentAccessMutation = graphql`
|
||||
mutation DocumentPageRequestDocumentAccessMutation(
|
||||
$input: RequestDocumentAccessInput!
|
||||
) {
|
||||
requestDocumentAccess(input: $input) {
|
||||
document {
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const requestTrustCenterFileAccessMutation = graphql`
|
||||
mutation DocumentPageRequestTrustCenterFileAccessMutation(
|
||||
$input: RequestTrustCenterFileAccessInput!
|
||||
) {
|
||||
requestTrustCenterFileAccess(input: $input) {
|
||||
file {
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const requestReportAccessMutation = graphql`
|
||||
mutation DocumentPageRequestReportAccessMutation(
|
||||
$input: RequestReportAccessInput!
|
||||
) {
|
||||
requestReportAccess(input: $input) {
|
||||
audit {
|
||||
report {
|
||||
access {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
queryRef: PreloadedQuery<DocumentPageQueryType>;
|
||||
};
|
||||
|
||||
function isPdfFilename(name: string): boolean {
|
||||
return name.toLowerCase().endsWith(".pdf");
|
||||
}
|
||||
|
||||
function getNodeTitle(node: DocumentPageQueryType["response"]["node"]): string | undefined {
|
||||
switch (node.__typename) {
|
||||
case "Document":
|
||||
return node.title;
|
||||
case "TrustCenterFile":
|
||||
return node.name;
|
||||
case "Report":
|
||||
return node.filename;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeId(node: DocumentPageQueryType["response"]["node"]): string | undefined {
|
||||
switch (node.__typename) {
|
||||
case "Document":
|
||||
case "TrustCenterFile":
|
||||
case "Report":
|
||||
return node.id;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function DocumentPage({ queryRef }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const theme = useSystemTheme();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [pdfData, setPdfData] = useState<string | null>(null);
|
||||
const [fileData, setFileData] = useState<string | null>(null);
|
||||
const [exportError, setExportError] = useState<string | null>(null);
|
||||
|
||||
const data = usePreloadedQuery(documentPageQuery, queryRef);
|
||||
const trustCenter = data.currentTrustCenter;
|
||||
const node = data.node;
|
||||
|
||||
if (
|
||||
node.__typename !== "Document"
|
||||
&& node.__typename !== "TrustCenterFile"
|
||||
&& node.__typename !== "Report"
|
||||
) {
|
||||
throw new Error(`Unexpected node type: ${node.__typename}`);
|
||||
}
|
||||
|
||||
const nodeTitle = getNodeTitle(node);
|
||||
const nodeId = getNodeId(node);
|
||||
|
||||
const logoFileUrl = theme === "dark"
|
||||
? (trustCenter?.darkLogoFileUrl ?? trustCenter?.logoFileUrl)
|
||||
: trustCenter?.logoFileUrl;
|
||||
|
||||
const [exportDocument, isExportingDocument]
|
||||
= useMutation<DocumentPageExportDocumentMutation>(exportDocumentMutation);
|
||||
const [exportFile, isExportingFile]
|
||||
= useMutation<DocumentPageExportTrustCenterFileMutation>(exportTrustCenterFileMutation);
|
||||
const [exportReport, isExportingReport]
|
||||
= useMutation<DocumentPageExportReportMutation>(exportReportMutation);
|
||||
const [requestAccess, isRequestingAccess]
|
||||
= useMutation<DocumentPageRequestDocumentAccessMutation>(requestDocumentAccessMutation);
|
||||
const [requestFileAccess, isRequestingFileAccess]
|
||||
= useMutation<DocumentPageRequestTrustCenterFileAccessMutation>(requestTrustCenterFileAccessMutation);
|
||||
const [requestReportAccess, isRequestingReportAccess]
|
||||
= useMutation<DocumentPageRequestReportAccessMutation>(requestReportAccessMutation);
|
||||
|
||||
const isExporting = isExportingDocument || isExportingFile || isExportingReport;
|
||||
|
||||
const [prevNodeId, setPrevNodeId] = useState(nodeId);
|
||||
if (prevNodeId !== nodeId) {
|
||||
setPrevNodeId(nodeId);
|
||||
setPdfData(null);
|
||||
setFileData(null);
|
||||
setExportError(null);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!node.isUserAuthorized || pdfData || fileData || exportError) return;
|
||||
|
||||
const onError = (error: Error) => {
|
||||
setExportError(error.message ?? __("Cannot export document"));
|
||||
};
|
||||
|
||||
const onCompletedErrors = (errors: readonly { message: string }[] | null | undefined) => {
|
||||
if (errors?.length) {
|
||||
setExportError(formatError(__("Cannot export document"), [...errors]));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
switch (node.__typename) {
|
||||
case "Document":
|
||||
exportDocument({
|
||||
variables: { input: { documentId: node.id } },
|
||||
onCompleted: (response, errors) => {
|
||||
if (onCompletedErrors(errors)) return;
|
||||
setPdfData(response.exportDocumentPDF.data);
|
||||
},
|
||||
onError,
|
||||
});
|
||||
break;
|
||||
case "TrustCenterFile":
|
||||
exportFile({
|
||||
variables: { input: { trustCenterFileId: node.id } },
|
||||
onCompleted: (response, errors) => {
|
||||
if (onCompletedErrors(errors)) return;
|
||||
if (isPdfFilename(node.name)) {
|
||||
setPdfData(response.exportTrustCenterFile.data);
|
||||
} else {
|
||||
setFileData(response.exportTrustCenterFile.data);
|
||||
}
|
||||
},
|
||||
onError,
|
||||
});
|
||||
break;
|
||||
case "Report":
|
||||
exportReport({
|
||||
variables: { input: { reportId: node.id } },
|
||||
onCompleted: (response, errors) => {
|
||||
if (onCompletedErrors(errors)) return;
|
||||
setPdfData(response.exportReportPDF.data);
|
||||
},
|
||||
onError,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}, [node, pdfData, fileData, exportError, exportDocument, exportFile, exportReport, __]);
|
||||
|
||||
const handleRequestAccess = () => {
|
||||
const onError = (error: Error) => {
|
||||
if (error instanceof UnAuthenticatedError) {
|
||||
const pathPrefix = getPathPrefix();
|
||||
const urlSearchParams = new URLSearchParams([[
|
||||
"continue",
|
||||
window.location.origin + pathPrefix + location.pathname + "?" + searchParams.toString(),
|
||||
]]);
|
||||
void navigate(`/connect?${urlSearchParams.toString()}`);
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: error.message ?? __("Cannot request access"),
|
||||
variant: "error",
|
||||
});
|
||||
};
|
||||
|
||||
const onCompleted = (_: unknown, errors: readonly { message: string }[] | null | undefined) => {
|
||||
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",
|
||||
});
|
||||
};
|
||||
|
||||
switch (node.__typename) {
|
||||
case "Document":
|
||||
requestAccess({
|
||||
variables: { input: { documentId: node.id } },
|
||||
onCompleted,
|
||||
onError,
|
||||
});
|
||||
break;
|
||||
case "TrustCenterFile":
|
||||
requestFileAccess({
|
||||
variables: { input: { trustCenterFileId: node.id } },
|
||||
onCompleted,
|
||||
onError,
|
||||
});
|
||||
break;
|
||||
case "Report":
|
||||
requestReportAccess({
|
||||
variables: { input: { reportId: node.id } },
|
||||
onCompleted,
|
||||
onError,
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const isRequesting = isRequestingAccess || isRequestingFileAccess || isRequestingReportAccess;
|
||||
const hasRequested = node.access?.status === "REQUESTED";
|
||||
const isPdf = node.__typename === "Document" || node.__typename === "Report" || (node.__typename === "TrustCenterFile" && isPdfFilename(node.name));
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!fileData || !nodeTitle) return;
|
||||
const byteCharacters = atob(fileData);
|
||||
const byteNumbers = new Uint8Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||
}
|
||||
const blob = new Blob([byteNumbers]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = nodeTitle;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-level-2">
|
||||
<header className="flex items-center h-12 gap-3 border-b border-border-solid px-4 flex-none bg-level-1">
|
||||
<Link
|
||||
to="/documents"
|
||||
className="size-8 grid place-items-center hover:bg-secondary-hover rounded-sm transition-all"
|
||||
>
|
||||
<IconChevronLeft size={16} />
|
||||
</Link>
|
||||
{logoFileUrl && (
|
||||
<img
|
||||
alt=""
|
||||
src={logoFileUrl}
|
||||
className="h-6 w-auto"
|
||||
/>
|
||||
)}
|
||||
<span className="text-sm font-medium truncate">
|
||||
{nodeTitle}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 min-h-0">
|
||||
{exportError
|
||||
? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center max-w-sm">
|
||||
<h2 className="text-lg font-medium mb-2">
|
||||
{__("Failed to load document")}
|
||||
</h2>
|
||||
<p className="text-sm text-txt-secondary">
|
||||
{exportError}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: node.isUserAuthorized
|
||||
? (
|
||||
isPdf
|
||||
? (
|
||||
isExporting || !pdfData
|
||||
? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<PDFPreview src={pdfData} name={nodeTitle ?? ""} />
|
||||
)
|
||||
)
|
||||
: (
|
||||
isExporting || !fileData
|
||||
? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center max-w-sm">
|
||||
<h2 className="text-lg font-medium mb-2">
|
||||
{nodeTitle}
|
||||
</h2>
|
||||
<p className="text-sm text-txt-secondary mb-6">
|
||||
{__("This file cannot be previewed in the browser.")}
|
||||
</p>
|
||||
<Button
|
||||
icon={IconArrowDown}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
{__("Download file")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
)
|
||||
: (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center max-w-sm">
|
||||
<IconLock size={32} className="mx-auto text-txt-tertiary mb-4" />
|
||||
<h2 className="text-lg font-medium mb-2">
|
||||
{nodeTitle}
|
||||
</h2>
|
||||
<p className="text-sm text-txt-secondary mb-6">
|
||||
{__("This document requires access approval before viewing.")}
|
||||
</p>
|
||||
<Button
|
||||
disabled={hasRequested || isRequesting}
|
||||
icon={IconLock}
|
||||
onClick={handleRequestAccess}
|
||||
>
|
||||
{hasRequested
|
||||
? __("Access requested")
|
||||
: __("Request access")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
apps/trust/src/pages/DocumentPageLoader.tsx
Normal file
45
apps/trust/src/pages/DocumentPageLoader.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// 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.
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useQueryLoader } from "react-relay";
|
||||
import { useParams } from "react-router";
|
||||
|
||||
import { RelayProvider } from "#/providers/RelayProviders";
|
||||
|
||||
import type { DocumentPageQuery } from "./__generated__/DocumentPageQuery.graphql";
|
||||
import { DocumentPage, documentPageQuery } from "./DocumentPage";
|
||||
|
||||
function DocumentPageQueryLoader() {
|
||||
const { documentId } = useParams<{ documentId: string }>();
|
||||
const [queryRef, loadQuery] = useQueryLoader<DocumentPageQuery>(documentPageQuery);
|
||||
|
||||
useEffect(() => {
|
||||
if (documentId) {
|
||||
loadQuery({ id: documentId });
|
||||
}
|
||||
}, [documentId, loadQuery]);
|
||||
|
||||
if (!queryRef) return null;
|
||||
|
||||
return <DocumentPage queryRef={queryRef} />;
|
||||
}
|
||||
|
||||
export default function DocumentPageLoader() {
|
||||
return (
|
||||
<RelayProvider>
|
||||
<DocumentPageQueryLoader />
|
||||
</RelayProvider>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
currentTrustVendorsQuery,
|
||||
} from "#/queries/TrustGraph";
|
||||
|
||||
import { DocumentPageErrorBoundary } from "./components/DocumentPageErrorBoundary";
|
||||
import { PageError } from "./components/PageError";
|
||||
import { RootErrorBoundary } from "./components/RootErrorBoundary";
|
||||
import { MainSkeleton } from "./components/Skeletons/MainSkeleton";
|
||||
@@ -75,6 +76,11 @@ const routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/documents/:documentId",
|
||||
Component: lazy(() => import("#/pages/DocumentPageLoader")),
|
||||
ErrorBoundary: DocumentPageErrorBoundary,
|
||||
},
|
||||
{
|
||||
path: "/documents",
|
||||
loader: loaderFromQueryLoader(() =>
|
||||
|
||||
@@ -874,7 +874,7 @@ type RecordSigningEventPayload {
|
||||
|
||||
type Query {
|
||||
viewer: Identity
|
||||
node(id: ID!): Node!
|
||||
node(id: ID!): Node
|
||||
currentTrustCenter: TrustCenter
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,9 @@ func (r *auditResolver) Report(ctx context.Context, obj *types.Audit) (*types.Re
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
report, err := trustService.Reports.Get(ctx, *audit.ReportID)
|
||||
trustCenter := compliancepage.CompliancePageFromContext(ctx)
|
||||
|
||||
report, err := trustService.Reports.Get(ctx, trustCenter.OrganizationID, *audit.ReportID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot load report", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
@@ -91,7 +93,7 @@ func (r *documentResolver) IsUserAuthorized(ctx context.Context, obj *types.Docu
|
||||
trustService := r.TrustService(ctx, obj.ID.TenantID())
|
||||
trustCenter := compliancepage.CompliancePageFromContext(ctx)
|
||||
|
||||
document, err := trustService.Documents.Get(ctx, obj.ID)
|
||||
document, err := trustService.Documents.Get(ctx, trustCenter.OrganizationID, obj.ID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot load document", log.Error(err))
|
||||
return false, gqlutils.Internal(ctx)
|
||||
@@ -355,7 +357,7 @@ func (r *mutationResolver) ExportDocumentPDF(ctx context.Context, input types.Ex
|
||||
trustService := r.TrustService(ctx, input.DocumentID.TenantID())
|
||||
trustCenter := compliancepage.CompliancePageFromContext(ctx)
|
||||
|
||||
document, err := trustService.Documents.Get(ctx, input.DocumentID)
|
||||
document, err := trustService.Documents.Get(ctx, trustCenter.OrganizationID, input.DocumentID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot load document", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
@@ -462,8 +464,11 @@ func (r *mutationResolver) ExportTrustCenterFile(ctx context.Context, input type
|
||||
trustCenter := compliancepage.CompliancePageFromContext(ctx)
|
||||
trustService := r.TrustService(ctx, trustCenter.ID.TenantID())
|
||||
|
||||
trustCenterFile, err := trustService.TrustCenterFiles.Get(ctx, input.TrustCenterFileID)
|
||||
trustCenterFile, err := trustService.TrustCenterFiles.Get(ctx, trustCenter.OrganizationID, input.TrustCenterFileID)
|
||||
if err != nil {
|
||||
if errors.Is(err, trust.ErrTrustCenterFileNotFound) || errors.Is(err, trust.ErrTrustCenterFileNotVisible) {
|
||||
return nil, gqlutils.NotFoundf(ctx, "trust center file %q not found", input.TrustCenterFileID)
|
||||
}
|
||||
r.logger.ErrorCtx(ctx, "cannot load trust center file", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
@@ -514,7 +519,7 @@ func (r *mutationResolver) RequestDocumentAccess(ctx context.Context, input type
|
||||
trustCenter := compliancepage.CompliancePageFromContext(ctx)
|
||||
trustService := r.TrustService(ctx, trustCenter.ID.TenantID())
|
||||
|
||||
document, err := trustService.Documents.Get(ctx, input.DocumentID)
|
||||
document, err := trustService.Documents.Get(ctx, trustCenter.OrganizationID, input.DocumentID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot load document", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
@@ -597,8 +602,11 @@ func (r *mutationResolver) RequestTrustCenterFileAccess(ctx context.Context, inp
|
||||
trustCenter := compliancepage.CompliancePageFromContext(ctx)
|
||||
trustService := r.TrustService(ctx, trustCenter.ID.TenantID())
|
||||
|
||||
trustCenterFile, err := trustService.TrustCenterFiles.Get(ctx, input.TrustCenterFileID)
|
||||
trustCenterFile, err := trustService.TrustCenterFiles.Get(ctx, trustCenter.OrganizationID, input.TrustCenterFileID)
|
||||
if err != nil {
|
||||
if errors.Is(err, trust.ErrTrustCenterFileNotFound) || errors.Is(err, trust.ErrTrustCenterFileNotVisible) {
|
||||
return nil, gqlutils.NotFoundf(ctx, "trust center file %q not found", input.TrustCenterFileID)
|
||||
}
|
||||
r.logger.ErrorCtx(ctx, "cannot load trust center file", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
@@ -852,8 +860,13 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
|
||||
return types.NewOrganization(organization), nil
|
||||
|
||||
case coredata.DocumentEntityType:
|
||||
document, err := trustService.Documents.Get(ctx, id)
|
||||
trustCenter := compliancepage.CompliancePageFromContext(ctx)
|
||||
|
||||
document, err := trustService.Documents.Get(ctx, trustCenter.OrganizationID, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, trust.ErrDocumentNotFound) || errors.Is(err, trust.ErrDocumentNotVisible) || errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return nil, gqlutils.NotFoundf(ctx, "node %q not found", id)
|
||||
}
|
||||
r.logger.ErrorCtx(ctx, "cannot get document", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
@@ -868,8 +881,13 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
|
||||
return types.NewFramework(framework), nil
|
||||
|
||||
case coredata.ReportEntityType:
|
||||
report, err := trustService.Reports.Get(ctx, id)
|
||||
trustCenter := compliancepage.CompliancePageFromContext(ctx)
|
||||
|
||||
report, err := trustService.Reports.Get(ctx, trustCenter.OrganizationID, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, trust.ErrReportNotFound) || errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return nil, gqlutils.NotFoundf(ctx, "node %q not found", id)
|
||||
}
|
||||
r.logger.ErrorCtx(ctx, "cannot get report", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
@@ -907,6 +925,20 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
|
||||
}
|
||||
return types.NewTrustCenterReference(reference), nil
|
||||
|
||||
case coredata.TrustCenterFileEntityType:
|
||||
trustCenter := compliancepage.CompliancePageFromContext(ctx)
|
||||
|
||||
trustCenterFile, err := trustService.TrustCenterFiles.Get(ctx, trustCenter.OrganizationID, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, trust.ErrTrustCenterFileNotFound) || errors.Is(err, trust.ErrTrustCenterFileNotVisible) {
|
||||
return nil, gqlutils.NotFoundf(ctx, "node %q not found", id)
|
||||
}
|
||||
r.logger.ErrorCtx(ctx, "cannot get trust center file", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewTrustCenterFile(trustCenterFile), nil
|
||||
|
||||
default:
|
||||
return nil, gqlutils.NotFoundf(ctx, "node %q not found", id)
|
||||
}
|
||||
@@ -1251,8 +1283,11 @@ func (r *trustCenterFileResolver) IsUserAuthorized(ctx context.Context, obj *typ
|
||||
|
||||
trustCenter := compliancepage.CompliancePageFromContext(ctx)
|
||||
|
||||
trustCenterFile, err := trustService.TrustCenterFiles.Get(ctx, obj.ID)
|
||||
trustCenterFile, err := trustService.TrustCenterFiles.Get(ctx, trustCenter.OrganizationID, obj.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, trust.ErrTrustCenterFileNotFound) || errors.Is(err, trust.ErrTrustCenterFileNotVisible) {
|
||||
return false, gqlutils.NotFoundf(ctx, "trust center file %q not found", obj.ID)
|
||||
}
|
||||
r.logger.ErrorCtx(ctx, "cannot load trust center file", log.Error(err))
|
||||
return false, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ func (s *DocumentService) ExportPDFWithoutWatermark(
|
||||
|
||||
func (s DocumentService) Get(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
documentID gid.GID,
|
||||
) (*coredata.Document, error) {
|
||||
document := &coredata.Document{}
|
||||
@@ -110,6 +111,14 @@ func (s DocumentService) Get(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if document.OrganizationID != organizationID {
|
||||
return nil, ErrDocumentNotFound
|
||||
}
|
||||
|
||||
if document.TrustCenterVisibility == coredata.TrustCenterVisibilityNone {
|
||||
return nil, ErrDocumentNotVisible
|
||||
}
|
||||
|
||||
return document, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -23,5 +23,10 @@ var (
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrUserInactive = errors.New("user inactive")
|
||||
ErrDocumentAccessNotFound = errors.New("document access not found")
|
||||
ErrNDAFileNotFound = errors.New("NDA file not found")
|
||||
ErrNDAFileNotFound = errors.New("NDA file not found")
|
||||
ErrDocumentNotFound = errors.New("document not found")
|
||||
ErrDocumentNotVisible = errors.New("document not visible")
|
||||
ErrReportNotFound = errors.New("report not found")
|
||||
ErrTrustCenterFileNotFound = errors.New("trust center file not found")
|
||||
ErrTrustCenterFileNotVisible = errors.New("trust center file not visible")
|
||||
)
|
||||
|
||||
@@ -33,6 +33,23 @@ type ReportService struct {
|
||||
}
|
||||
|
||||
func (s ReportService) Get(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
reportID gid.GID,
|
||||
) (*coredata.Report, error) {
|
||||
report, err := s.loadByID(ctx, reportID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if report.OrganizationID != organizationID {
|
||||
return nil, ErrReportNotFound
|
||||
}
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func (s ReportService) loadByID(
|
||||
ctx context.Context,
|
||||
reportID gid.GID,
|
||||
) (*coredata.Report, error) {
|
||||
@@ -62,7 +79,7 @@ func (s ReportService) GenerateDownloadURL(
|
||||
reportID gid.GID,
|
||||
expiresIn time.Duration,
|
||||
) (*string, error) {
|
||||
report, err := s.Get(ctx, reportID)
|
||||
report, err := s.loadByID(ctx, reportID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get report: %w", err)
|
||||
}
|
||||
@@ -114,7 +131,7 @@ func (s ReportService) exportPDFData(
|
||||
ctx context.Context,
|
||||
reportID gid.GID,
|
||||
) ([]byte, error) {
|
||||
report, err := s.Get(ctx, reportID)
|
||||
report, err := s.loadByID(ctx, reportID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get report: %w", err)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ type TrustCenterFileService struct {
|
||||
|
||||
func (s *TrustCenterFileService) Get(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
trustCenterFileID gid.GID,
|
||||
) (*coredata.TrustCenterFile, error) {
|
||||
trustCenterFile := &coredata.TrustCenterFile{}
|
||||
@@ -54,6 +55,14 @@ func (s *TrustCenterFileService) Get(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if trustCenterFile.OrganizationID != organizationID {
|
||||
return nil, ErrTrustCenterFileNotFound
|
||||
}
|
||||
|
||||
if trustCenterFile.TrustCenterVisibility == coredata.TrustCenterVisibilityNone {
|
||||
return nil, ErrTrustCenterFileNotVisible
|
||||
}
|
||||
|
||||
return trustCenterFile, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user