diff --git a/apps/console/package.json b/apps/console/package.json index b06fd3f5c..51d8268f7 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "relay": "find src -type d -name \"__generated__\" -exec rm -rf {} + && npx relay-compiler ./relay.config.json && npx relay-compiler ./relay.trust.config.json", + "relay": "find src -type d -name \"__generated__\" -exec rm -rf {} + && npx relay-compiler ./relay.config.json", "build": "tsc -b && vite build", "lint": "eslint .", "check": "tsc --noEmit -p tsconfig.app.json", diff --git a/apps/console/relay.trust.config.json b/apps/console/relay.trust.config.json deleted file mode 100644 index 0d94d15d5..000000000 --- a/apps/console/relay.trust.config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "src": "./src/trust", - "schema": "../../pkg/server/api/trust/v1/schema.graphql", - "language": "typescript", - "eagerEsModules": true, - "noFutureProofEnums": true -} diff --git a/apps/console/src/hooks/useTrustCenterQueries.ts b/apps/console/src/hooks/useTrustCenterQueries.ts new file mode 100644 index 000000000..2eb3ad01b --- /dev/null +++ b/apps/console/src/hooks/useTrustCenterQueries.ts @@ -0,0 +1,299 @@ +import { useQuery, useMutation } from "@tanstack/react-query"; +import { GraphQLError } from "graphql"; +import { buildEndpoint } from "/providers/RelayProviders"; + +export interface TrustCenterDocument { + id: string; + title: string; + documentType: string; +} + +export interface TrustCenterAudit { + id: string; + framework: { + name: string; + }; + report: { + id: string; + filename: string; + downloadUrl: string | null; + } | null; +} + +export interface TrustCenterVendor { + id: string; + name: string; + category: string; + privacyPolicyUrl?: string | null; + websiteUrl?: string | null; +} + +interface TrustCenterQueryData { + trustCenterBySlug: { + id: string; + active: boolean; + slug: string; + isUserAuthenticated: boolean; + organization: { + id: string; + name: string; + logoUrl: string | null; + }; + documents: { + edges: Array<{ + node: TrustCenterDocument; + }>; + }; + audits: { + edges: Array<{ + node: TrustCenterAudit; + }>; + }; + vendors: { + edges: Array<{ + node: TrustCenterVendor; + }>; + }; + } | null; +} + +interface GraphQLResponse { + data?: T; + errors?: GraphQLError[]; +} + +interface ExportDocumentPDFData { + exportDocumentPDF: { + data: string; + }; +} + +interface CreateTrustCenterAccessData { + createTrustCenterAccess: { + trustCenterAccess: { + id: string; + email: string; + name: string; + }; + }; +} + +interface TrustCenterQueryVariables { + slug: string; +} + +interface ExportDocumentPDFVariables { + input: { + documentId: string; + }; +} + +interface CreateTrustCenterAccessVariables { + input: { + trustCenterId: string; + email: string; + name: string; + }; +} + +type GraphQLVariables = TrustCenterQueryVariables | ExportDocumentPDFVariables | CreateTrustCenterAccessVariables | Record; + +async function trustCenterGraphQLRequest( + operationName: string, + query: string, + variables: GraphQLVariables = {} +): Promise> { + const response = await fetch(buildEndpoint("/api/trust/v1/graphql"), { + method: "POST", + credentials: "include", + headers: { + Accept: "application/graphql-response+json; charset=utf-8, application/json; charset=utf-8", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + operationName, + query, + variables, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return result; +} + +function isCriticalError(error: GraphQLError): boolean { + const message = error.message?.toLowerCase() || ''; + const path = error.path || []; + + if (message.includes('access denied') || message.includes('authentication required')) { + if (path.length > 2) { + return false; + } + } + + return true; +} + +const TRUST_CENTER_QUERY = ` + query PublicTrustCenterPageQuery($slug: String!) { + trustCenterBySlug(slug: $slug) { + id + active + slug + isUserAuthenticated + organization { + id + name + logoUrl + } + documents(first: 100) { + edges { + node { + id + title + documentType + } + } + } + audits(first: 100) { + edges { + node { + id + framework { + name + } + report { + id + filename + downloadUrl + } + } + } + } + vendors(first: 100) { + edges { + node { + id + name + category + websiteUrl + privacyPolicyUrl + } + } + } + } + } +`; + +const EXPORT_DOCUMENT_PDF_MUTATION = ` + mutation PublicTrustCenterDocumentsExportPDFMutation( + $input: ExportDocumentPDFInput! + ) { + exportDocumentPDF(input: $input) { + data + } + } +`; + +const CREATE_TRUST_CENTER_ACCESS_MUTATION = ` + mutation PublicTrustCenterAccessRequestDialogMutation( + $input: CreateTrustCenterAccessInput! + ) { + createTrustCenterAccess(input: $input) { + trustCenterAccess { + id + email + name + } + } + } +`; + +export function useTrustCenterQuery(slug: string) { + return useQuery({ + queryKey: ["trust-center", slug], + queryFn: async () => { + const result = await trustCenterGraphQLRequest( + "PublicTrustCenterPageQuery", + TRUST_CENTER_QUERY, + { slug } + ); + + if (result.errors && result.errors.length > 0) { + const criticalErrors = result.errors.filter(isCriticalError); + + if (criticalErrors.length > 0) { + throw new Error( + `GraphQL error: ${criticalErrors.map((e) => e.message).join(", ")}` + ); + } + } + + if (!result.data) { + throw new Error("No data returned from GraphQL query"); + } + + return result.data; + }, + enabled: !!slug, + staleTime: 5 * 60 * 1000, // 5 minutes + retry: (failureCount, error) => { + if (error.message.includes("UNAUTHENTICATED") || error.message.includes("401")) { + return false; + } + return failureCount < 3; + }, + }); +} + +export function useExportDocumentPDF() { + return useMutation({ + mutationFn: async (documentId: string) => { + const result = await trustCenterGraphQLRequest( + "PublicTrustCenterDocumentsExportPDFMutation", + EXPORT_DOCUMENT_PDF_MUTATION, + { input: { documentId } } + ); + + if (result.errors && result.errors.length > 0) { + throw new Error( + `GraphQL error: ${result.errors.map((e) => e.message).join(", ")}` + ); + } + + if (!result.data) { + throw new Error("No data returned from mutation"); + } + + return result.data; + }, + }); +} + +export function useCreateTrustCenterAccess() { + return useMutation({ + mutationFn: async (input: { trustCenterId: string; email: string; name: string }) => { + const result = await trustCenterGraphQLRequest( + "PublicTrustCenterAccessRequestDialogMutation", + CREATE_TRUST_CENTER_ACCESS_MUTATION, + { input } + ); + + if (result.errors && result.errors.length > 0) { + throw new Error( + `GraphQL error: ${result.errors.map((e) => e.message).join(", ")}` + ); + } + + if (!result.data) { + throw new Error("No data returned from mutation"); + } + + return result.data; + }, + }); +} diff --git a/apps/console/src/providers/TrustRelayProvider.tsx b/apps/console/src/providers/TrustRelayProvider.tsx deleted file mode 100644 index 25c464b15..000000000 --- a/apps/console/src/providers/TrustRelayProvider.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { - Environment, - type FetchFunction, - Network, - RecordSource, - Store, -} from "relay-runtime"; - -import type { PropsWithChildren } from "react"; -import { RelayEnvironmentProvider } from "react-relay"; -import { createContext, useContext, useState, useRef } from "react"; -import { buildEndpoint } from "./RelayProviders"; - -export class TrustCenterError extends Error { - constructor(message: string) { - super(message); - this.name = "TrustCenterError"; - } -} - -type TrustAuthContextType = { - isAuthenticated: boolean; - setAuthenticated: (auth: boolean) => void; -}; - -const TrustAuthContext = createContext(null); - -export function useTrustAuth() { - const context = useContext(TrustAuthContext); - if (!context) { - throw new Error('useTrustAuth must be used within a TrustRelayProvider'); - } - return context; -} - -const createFetchTrustRelay = (setAuthenticated: (auth: boolean) => void): FetchFunction => async (request, variables) => { - const requestInit: RequestInit = { - method: "POST", - headers: { - Accept: - "application/graphql-response+json; charset=utf-8, application/json; charset=utf-8", - "Content-Type": "application/json", - }, - credentials: "include", - body: JSON.stringify({ - operationName: request.name, - query: request.text, - variables, - }), - }; - - const response = await fetch( - buildEndpoint("/api/trust/v1/graphql"), - requestInit - ); - - if (response.status === 500) { - throw new TrustCenterError("Internal server error"); - } - - const json = await response.json(); - - if (json.errors?.length > 0) { - const hasAccessDeniedErrors = json.errors.some((error: any) => - error.message.toLowerCase().includes("access denied") || - error.message.toLowerCase().includes("unauthorized") || - error.extensions?.code === "UNAUTHENTICATED" - ); - - if (hasAccessDeniedErrors) { - setAuthenticated(false); - } else { - throw new TrustCenterError( - `Error fetching GraphQL query '${ - request.name - }' with variables '${JSON.stringify(variables)}': ${JSON.stringify( - json.errors - )}` - ); - } - } else { - setAuthenticated(true); - } - - return json; -}; - -export function TrustRelayProvider({ children }: PropsWithChildren) { - const [isAuthenticated, setIsAuthenticated] = useState(true); - const environmentRef = useRef(null); - - if (!environmentRef.current) { - const trustSource = new RecordSource(); - const trustStore = new Store(trustSource, { - queryCacheExpirationTime: 5 * 60 * 1000, // 5 minutes - gcReleaseBufferSize: 10, - }); - - environmentRef.current = new Environment({ - network: Network.create(createFetchTrustRelay(setIsAuthenticated)), - store: trustStore, - }); - } - - const authContextValue: TrustAuthContextType = { - isAuthenticated, - setAuthenticated: setIsAuthenticated, - }; - - return ( - - - {children} - - - ); -} diff --git a/apps/console/src/trust/components/PublicTrustCenterAccessRequestDialog.tsx b/apps/console/src/trust/components/PublicTrustCenterAccessRequestDialog.tsx index 5e27be233..c3ebcba00 100644 --- a/apps/console/src/trust/components/PublicTrustCenterAccessRequestDialog.tsx +++ b/apps/console/src/trust/components/PublicTrustCenterAccessRequestDialog.tsx @@ -12,22 +12,7 @@ import { useTranslate } from "@probo/i18n"; import { sprintf } from "@probo/helpers"; import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { z } from "zod"; -import { useMutation, graphql } from "react-relay"; -import type { PublicTrustCenterAccessRequestDialogMutation } from "./__generated__/PublicTrustCenterAccessRequestDialogMutation.graphql"; - -const CreateTrustCenterAccessMutation = graphql` - mutation PublicTrustCenterAccessRequestDialogMutation( - $input: CreateTrustCenterAccessInput! - ) { - createTrustCenterAccess(input: $input) { - trustCenterAccess { - id - email - name - } - } - } -`; +import { useCreateTrustCenterAccess } from "/hooks/useTrustCenterQueries"; type Props = { trigger: React.ReactNode; @@ -45,7 +30,7 @@ export function PublicTrustCenterAccessRequestDialog({ const [isSubmitting, setIsSubmitting] = useState(false); const dialogRef = useDialogRef(); - const [commitMutation] = useMutation(CreateTrustCenterAccessMutation); + const mutation = useCreateTrustCenterAccess(); const schema = z.object({ name: z.string().min(1, __("Name is required")).min(2, __("Name must be at least 2 characters long")), @@ -58,39 +43,36 @@ export function PublicTrustCenterAccessRequestDialog({ const onSubmit = handleSubmit(async (data) => { setIsSubmitting(true); + mutation.mutate( + { + trustCenterId, + email: data.email, + name: data.name, + }, + { + onSuccess: (result) => { + if (result.createTrustCenterAccess) { + toast({ + title: __("Request Submitted"), + description: __("Your access request has been submitted. You will receive an email if your request is approved."), + variant: "success", + }); - commitMutation({ - variables: { - input: { - trustCenterId, - email: data.email, - name: data.name, + reset(); + dialogRef.current?.close(); + } + setIsSubmitting(false); }, - }, - onCompleted: (response) => { - if (response.createTrustCenterAccess) { + onError: (_: Error) => { toast({ - title: __("Request Submitted"), - description: __("Your access request has been submitted. You will receive an email if your request is approved."), - variant: "success", + title: __("Error"), + description: __("An error occurred while submitting your request."), + variant: "error", }); - - reset(); - dialogRef.current?.close(); - } - setIsSubmitting(false); - }, - onError: (error) => { - const errorMessage = error.message || __("An error occurred while submitting your request."); - - toast({ - title: __("Request Failed"), - description: errorMessage, - variant: "error", - }); - setIsSubmitting(false); - }, - }); + setIsSubmitting(false); + }, + } + ); }); return ( @@ -128,9 +110,9 @@ export function PublicTrustCenterAccessRequestDialog({ diff --git a/apps/console/src/trust/components/PublicTrustCenterDocuments.tsx b/apps/console/src/trust/components/PublicTrustCenterDocuments.tsx index 8a1b1d1fe..e574ec8bd 100644 --- a/apps/console/src/trust/components/PublicTrustCenterDocuments.tsx +++ b/apps/console/src/trust/components/PublicTrustCenterDocuments.tsx @@ -13,20 +13,8 @@ import { useToast, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; -import { useMutation, graphql } from "react-relay"; +import { useExportDocumentPDF, type TrustCenterDocument } from "/hooks/useTrustCenterQueries"; import { PublicTrustCenterAccessRequestDialog } from "./PublicTrustCenterAccessRequestDialog"; -import type { PublicTrustCenterDocumentsExportPDFMutation } from "./__generated__/PublicTrustCenterDocumentsExportPDFMutation.graphql"; -import type { TrustCenterDocument } from "../pages/PublicTrustCenterPage"; - -const ExportDocumentPDFMutation = graphql` - mutation PublicTrustCenterDocumentsExportPDFMutation( - $input: ExportDocumentPDFInput! - ) { - exportDocumentPDF(input: $input) { - data - } - } -`; type Props = { documents: TrustCenterDocument[]; @@ -44,17 +32,14 @@ export function PublicTrustCenterDocuments({ const { __ } = useTranslate(); const { toast } = useToast(); - const [commitMutation] = useMutation(ExportDocumentPDFMutation); + const mutation = useExportDocumentPDF(); - const handleDownload = async (document: TrustCenterDocument) => { - commitMutation({ - variables: { - input: { documentId: document.id } - }, - onCompleted: (response) => { - if (response.exportDocumentPDF?.data) { + const handleDownload = (document: TrustCenterDocument) => { + mutation.mutate(document.id, { + onSuccess: (data) => { + if (data.exportDocumentPDF?.data) { const link = window.document.createElement("a"); - link.href = response.exportDocumentPDF.data; + link.href = data.exportDocumentPDF.data; link.download = `${document.title}.pdf`; window.document.body.appendChild(link); link.click(); @@ -136,8 +121,9 @@ export function PublicTrustCenterDocuments({ variant="secondary" icon={IconArrowDown} onClick={() => handleDownload(document)} + disabled={mutation.isPending} > - {__("Download")} + {mutation.isPending ? __("Downloading...") : __("Download")} )} diff --git a/apps/console/src/trust/components/__generated__/PublicTrustCenterAccessRequestDialogMutation.graphql.ts b/apps/console/src/trust/components/__generated__/PublicTrustCenterAccessRequestDialogMutation.graphql.ts deleted file mode 100644 index fbc0d00cc..000000000 --- a/apps/console/src/trust/components/__generated__/PublicTrustCenterAccessRequestDialogMutation.graphql.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * @generated SignedSource<<4ce5109725ad53ad77aedf4d39bf463b>> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ConcreteRequest } from 'relay-runtime'; -export type CreateTrustCenterAccessInput = { - email: string; - name: string; - trustCenterId: string; -}; -export type PublicTrustCenterAccessRequestDialogMutation$variables = { - input: CreateTrustCenterAccessInput; -}; -export type PublicTrustCenterAccessRequestDialogMutation$data = { - readonly createTrustCenterAccess: { - readonly trustCenterAccess: { - readonly email: string; - readonly id: string; - readonly name: string; - }; - }; -}; -export type PublicTrustCenterAccessRequestDialogMutation = { - response: PublicTrustCenterAccessRequestDialogMutation$data; - variables: PublicTrustCenterAccessRequestDialogMutation$variables; -}; - -const node: ConcreteRequest = (function(){ -var v0 = [ - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "input" - } -], -v1 = [ - { - "alias": null, - "args": [ - { - "kind": "Variable", - "name": "input", - "variableName": "input" - } - ], - "concreteType": "CreateTrustCenterAccessPayload", - "kind": "LinkedField", - "name": "createTrustCenterAccess", - "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 - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "email", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": null - } -]; -return { - "fragment": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Fragment", - "metadata": null, - "name": "PublicTrustCenterAccessRequestDialogMutation", - "selections": (v1/*: any*/), - "type": "Mutation", - "abstractKey": null - }, - "kind": "Request", - "operation": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Operation", - "name": "PublicTrustCenterAccessRequestDialogMutation", - "selections": (v1/*: any*/) - }, - "params": { - "cacheID": "bdc03aeaa40a243db4af3886abffb1a0", - "id": null, - "metadata": {}, - "name": "PublicTrustCenterAccessRequestDialogMutation", - "operationKind": "mutation", - "text": "mutation PublicTrustCenterAccessRequestDialogMutation(\n $input: CreateTrustCenterAccessInput!\n) {\n createTrustCenterAccess(input: $input) {\n trustCenterAccess {\n id\n email\n name\n }\n }\n}\n" - } -}; -})(); - -(node as any).hash = "d895df22aae3dcc8438bb794910cbfcc"; - -export default node; diff --git a/apps/console/src/trust/components/__generated__/PublicTrustCenterDocumentsExportPDFMutation.graphql.ts b/apps/console/src/trust/components/__generated__/PublicTrustCenterDocumentsExportPDFMutation.graphql.ts deleted file mode 100644 index 223c883f3..000000000 --- a/apps/console/src/trust/components/__generated__/PublicTrustCenterDocumentsExportPDFMutation.graphql.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @generated SignedSource<> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ConcreteRequest } from 'relay-runtime'; -export type ExportDocumentPDFInput = { - documentId: string; -}; -export type PublicTrustCenterDocumentsExportPDFMutation$variables = { - input: ExportDocumentPDFInput; -}; -export type PublicTrustCenterDocumentsExportPDFMutation$data = { - readonly exportDocumentPDF: { - readonly data: string; - }; -}; -export type PublicTrustCenterDocumentsExportPDFMutation = { - response: PublicTrustCenterDocumentsExportPDFMutation$data; - variables: PublicTrustCenterDocumentsExportPDFMutation$variables; -}; - -const node: ConcreteRequest = (function(){ -var v0 = [ - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "input" - } -], -v1 = [ - { - "alias": null, - "args": [ - { - "kind": "Variable", - "name": "input", - "variableName": "input" - } - ], - "concreteType": "ExportDocumentPDFPayload", - "kind": "LinkedField", - "name": "exportDocumentPDF", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "data", - "storageKey": null - } - ], - "storageKey": null - } -]; -return { - "fragment": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Fragment", - "metadata": null, - "name": "PublicTrustCenterDocumentsExportPDFMutation", - "selections": (v1/*: any*/), - "type": "Mutation", - "abstractKey": null - }, - "kind": "Request", - "operation": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Operation", - "name": "PublicTrustCenterDocumentsExportPDFMutation", - "selections": (v1/*: any*/) - }, - "params": { - "cacheID": "bb7c09ea21c22c0a728b18dde2449f72", - "id": null, - "metadata": {}, - "name": "PublicTrustCenterDocumentsExportPDFMutation", - "operationKind": "mutation", - "text": "mutation PublicTrustCenterDocumentsExportPDFMutation(\n $input: ExportDocumentPDFInput!\n) {\n exportDocumentPDF(input: $input) {\n data\n }\n}\n" - } -}; -})(); - -(node as any).hash = "00a3f3d260fe38b35fe33a0033cf2400"; - -export default node; diff --git a/apps/console/src/trust/pages/PublicTrustCenterPage.tsx b/apps/console/src/trust/pages/PublicTrustCenterPage.tsx index 088ccca05..62194e3d1 100644 --- a/apps/console/src/trust/pages/PublicTrustCenterPage.tsx +++ b/apps/console/src/trust/pages/PublicTrustCenterPage.tsx @@ -5,99 +5,16 @@ import { PublicTrustCenterLayout } from "/layouts/PublicTrustCenterLayout"; import { PublicTrustCenterAudits } from "../components/PublicTrustCenterAudits"; import { PublicTrustCenterVendors } from "../components/PublicTrustCenterVendors"; import { PublicTrustCenterDocuments } from "../components/PublicTrustCenterDocuments"; -import { TrustRelayProvider, useTrustAuth } from "/providers/TrustRelayProvider"; -import { Suspense } from "react"; -import { useLazyLoadQuery } from "react-relay"; -import { graphql } from "react-relay"; import { Spinner } from "@probo/ui"; -import type { PublicTrustCenterPageQuery } from "./__generated__/PublicTrustCenterPageQuery.graphql"; +import { useTrustCenterQuery, type TrustCenterDocument, type TrustCenterAudit, type TrustCenterVendor } from "/hooks/useTrustCenterQueries"; -export interface TrustCenterDocument { - id: string; - title: string; - documentType: string; -} +export type { TrustCenterDocument, TrustCenterAudit, TrustCenterVendor }; -export interface TrustCenterAudit { - id: string; - framework: { - name: string; - }; - report: { - id: string; - filename: string; - downloadUrl: string | null; - } | null; -} - -export interface TrustCenterVendor { - id: string; - name: string; - category: string; - privacyPolicyUrl?: string | null; - websiteUrl?: string | null; -} - -const PublicTrustCenterQuery = graphql` - query PublicTrustCenterPageQuery($slug: String!) { - trustCenterBySlug(slug: $slug) { - id - active - slug - organization { - id - name - logoUrl - } - documents(first: 100) { - edges { - node { - id - title - documentType - } - } - } - audits(first: 100) { - edges { - node { - id - framework { - name - } - report { - id - filename - downloadUrl - } - } - } - } - vendors(first: 100) { - edges { - node { - id - name - category - websiteUrl - privacyPolicyUrl - } - } - } - } - } -`; - -function PublicTrustCenterContent() { +export default function PublicTrustCenterPage() { const { __ } = useTranslate(); const { slug } = useParams<{ slug: string }>(); - const { isAuthenticated } = useTrustAuth(); - if (!slug) { - return ; - } - - const data = useLazyLoadQuery(PublicTrustCenterQuery, { slug }); + const { data, isLoading, error } = useTrustCenterQuery(slug || ""); const organization = data?.trustCenterBySlug?.organization; const organizationName = organization?.name || ""; @@ -106,6 +23,33 @@ function PublicTrustCenterContent() { organizationName ? `${organizationName} - Trust Center` : "Trust Center" ); + if (!slug) { + return ; + } + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+
+

+ {__("Error Loading Trust Center")} +

+

+ {__("There was an error loading the trust center. Please try again later.")} +

+
+
+ ); + } + if (!data?.trustCenterBySlug) { return (
@@ -121,7 +65,8 @@ function PublicTrustCenterContent() { ); } - const { documents, audits, vendors } = data.trustCenterBySlug; + const { trustCenterBySlug } = data; + const { documents, audits, vendors, isUserAuthenticated } = trustCenterBySlug; const trustCenterDocuments = documents.edges.map((edge) => edge.node) as TrustCenterDocument[]; const trustCenterAudits = audits.edges.map((edge) => edge.node) as TrustCenterAudit[]; @@ -131,20 +76,20 @@ function PublicTrustCenterContent() {
); } - -export default function PublicTrustCenterPage() { - return ( - - }> - - - - ); -} diff --git a/apps/console/src/trust/pages/__generated__/PublicTrustCenterPageQuery.graphql.ts b/apps/console/src/trust/pages/__generated__/PublicTrustCenterPageQuery.graphql.ts deleted file mode 100644 index 90efd138c..000000000 --- a/apps/console/src/trust/pages/__generated__/PublicTrustCenterPageQuery.graphql.ts +++ /dev/null @@ -1,430 +0,0 @@ -/** - * @generated SignedSource<<7510cf085d283274b579e0571eb503c4>> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ConcreteRequest } from 'relay-runtime'; -export type DocumentType = "ISMS" | "OTHER" | "POLICY"; -export type VendorCategory = "ANALYTICS" | "CLOUD_MONITORING" | "CLOUD_PROVIDER" | "COLLABORATION" | "CUSTOMER_SUPPORT" | "DATA_STORAGE_AND_PROCESSING" | "DOCUMENT_MANAGEMENT" | "EMPLOYEE_MANAGEMENT" | "ENGINEERING" | "FINANCE" | "IDENTITY_PROVIDER" | "IT" | "MARKETING" | "OFFICE_OPERATIONS" | "OTHER" | "PASSWORD_MANAGEMENT" | "PRODUCT_AND_DESIGN" | "PROFESSIONAL_SERVICES" | "RECRUITING" | "SALES" | "SECURITY" | "VERSION_CONTROL"; -export type PublicTrustCenterPageQuery$variables = { - slug: string; -}; -export type PublicTrustCenterPageQuery$data = { - readonly trustCenterBySlug: { - readonly active: boolean; - readonly audits: { - readonly edges: ReadonlyArray<{ - readonly node: { - readonly framework: { - readonly name: string; - }; - readonly id: string; - readonly report: { - readonly downloadUrl: string | null | undefined; - readonly filename: string; - readonly id: string; - } | null | undefined; - }; - }>; - }; - readonly documents: { - readonly edges: ReadonlyArray<{ - readonly node: { - readonly documentType: DocumentType; - readonly id: string; - readonly title: string; - }; - }>; - }; - readonly id: string; - readonly organization: { - readonly id: string; - readonly logoUrl: string | null | undefined; - readonly name: string; - }; - readonly slug: string; - readonly vendors: { - readonly edges: ReadonlyArray<{ - readonly node: { - readonly category: VendorCategory; - readonly id: string; - readonly name: string; - readonly privacyPolicyUrl: string | null | undefined; - readonly websiteUrl: string | null | undefined; - }; - }>; - }; - } | null | undefined; -}; -export type PublicTrustCenterPageQuery = { - response: PublicTrustCenterPageQuery$data; - variables: PublicTrustCenterPageQuery$variables; -}; - -const node: ConcreteRequest = (function(){ -var v0 = [ - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "slug" - } -], -v1 = [ - { - "kind": "Variable", - "name": "slug", - "variableName": "slug" - } -], -v2 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null -}, -v3 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "active", - "storageKey": null -}, -v4 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "slug", - "storageKey": null -}, -v5 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null -}, -v6 = { - "alias": null, - "args": null, - "concreteType": "Organization", - "kind": "LinkedField", - "name": "organization", - "plural": false, - "selections": [ - (v2/*: any*/), - (v5/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "logoUrl", - "storageKey": null - } - ], - "storageKey": null -}, -v7 = [ - { - "kind": "Literal", - "name": "first", - "value": 100 - } -], -v8 = { - "alias": null, - "args": (v7/*: any*/), - "concreteType": "DocumentConnection", - "kind": "LinkedField", - "name": "documents", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "DocumentEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Document", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v2/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "title", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "documentType", - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": "documents(first:100)" -}, -v9 = { - "alias": null, - "args": null, - "concreteType": "Report", - "kind": "LinkedField", - "name": "report", - "plural": false, - "selections": [ - (v2/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "filename", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "downloadUrl", - "storageKey": null - } - ], - "storageKey": null -}, -v10 = { - "alias": null, - "args": (v7/*: any*/), - "concreteType": "VendorConnection", - "kind": "LinkedField", - "name": "vendors", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "VendorEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Vendor", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v2/*: any*/), - (v5/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "category", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "websiteUrl", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "privacyPolicyUrl", - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": "vendors(first:100)" -}; -return { - "fragment": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Fragment", - "metadata": null, - "name": "PublicTrustCenterPageQuery", - "selections": [ - { - "alias": null, - "args": (v1/*: any*/), - "concreteType": "TrustCenter", - "kind": "LinkedField", - "name": "trustCenterBySlug", - "plural": false, - "selections": [ - (v2/*: any*/), - (v3/*: any*/), - (v4/*: any*/), - (v6/*: any*/), - (v8/*: any*/), - { - "alias": null, - "args": (v7/*: any*/), - "concreteType": "AuditConnection", - "kind": "LinkedField", - "name": "audits", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "AuditEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Audit", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v2/*: any*/), - { - "alias": null, - "args": null, - "concreteType": "Framework", - "kind": "LinkedField", - "name": "framework", - "plural": false, - "selections": [ - (v5/*: any*/) - ], - "storageKey": null - }, - (v9/*: any*/) - ], - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": "audits(first:100)" - }, - (v10/*: any*/) - ], - "storageKey": null - } - ], - "type": "Query", - "abstractKey": null - }, - "kind": "Request", - "operation": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Operation", - "name": "PublicTrustCenterPageQuery", - "selections": [ - { - "alias": null, - "args": (v1/*: any*/), - "concreteType": "TrustCenter", - "kind": "LinkedField", - "name": "trustCenterBySlug", - "plural": false, - "selections": [ - (v2/*: any*/), - (v3/*: any*/), - (v4/*: any*/), - (v6/*: any*/), - (v8/*: any*/), - { - "alias": null, - "args": (v7/*: any*/), - "concreteType": "AuditConnection", - "kind": "LinkedField", - "name": "audits", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "AuditEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Audit", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v2/*: any*/), - { - "alias": null, - "args": null, - "concreteType": "Framework", - "kind": "LinkedField", - "name": "framework", - "plural": false, - "selections": [ - (v5/*: any*/), - (v2/*: any*/) - ], - "storageKey": null - }, - (v9/*: any*/) - ], - "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": "audits(first:100)" - }, - (v10/*: any*/) - ], - "storageKey": null - } - ] - }, - "params": { - "cacheID": "9d0437e87c7c88083619611892bcc422", - "id": null, - "metadata": {}, - "name": "PublicTrustCenterPageQuery", - "operationKind": "query", - "text": "query PublicTrustCenterPageQuery(\n $slug: String!\n) {\n trustCenterBySlug(slug: $slug) {\n id\n active\n slug\n organization {\n id\n name\n logoUrl\n }\n documents(first: 100) {\n edges {\n node {\n id\n title\n documentType\n }\n }\n }\n audits(first: 100) {\n edges {\n node {\n id\n framework {\n name\n id\n }\n report {\n id\n filename\n downloadUrl\n }\n }\n }\n }\n vendors(first: 100) {\n edges {\n node {\n id\n name\n category\n websiteUrl\n privacyPolicyUrl\n }\n }\n }\n }\n}\n" - } -}; -})(); - -(node as any).hash = "7fc4b49f2a965be237cf3d51baa815bd"; - -export default node; diff --git a/pkg/server/api/trust/v1/schema.graphql b/pkg/server/api/trust/v1/schema.graphql index fe2e455ee..c4a7ca08b 100644 --- a/pkg/server/api/trust/v1/schema.graphql +++ b/pkg/server/api/trust/v1/schema.graphql @@ -200,6 +200,7 @@ type TrustCenter implements Node { active: Boolean! slug: String! organization: Organization! @goField(forceResolver: true) + isUserAuthenticated: Boolean! @goField(forceResolver: true) documents( first: Int diff --git a/pkg/server/api/trust/v1/schema/schema.go b/pkg/server/api/trust/v1/schema/schema.go index 955d41a1e..116ef25c4 100644 --- a/pkg/server/api/trust/v1/schema/schema.go +++ b/pkg/server/api/trust/v1/schema/schema.go @@ -131,13 +131,14 @@ type ComplexityRoot struct { } TrustCenter struct { - Active func(childComplexity int) int - Audits func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int - Documents func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int - ID func(childComplexity int) int - Organization func(childComplexity int) int - Slug func(childComplexity int) int - Vendors func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int + Active func(childComplexity int) int + Audits func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int + Documents func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int + ID func(childComplexity int) int + IsUserAuthenticated func(childComplexity int) int + Organization func(childComplexity int) int + Slug func(childComplexity int) int + Vendors func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int } TrustCenterAccess struct { @@ -186,6 +187,7 @@ type ReportResolver interface { } type TrustCenterResolver interface { Organization(ctx context.Context, obj *types.TrustCenter) (*types.Organization, error) + IsUserAuthenticated(ctx context.Context, obj *types.TrustCenter) (bool, error) Documents(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.DocumentConnection, error) Audits(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.AuditConnection, error) Vendors(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.VendorConnection, error) @@ -480,6 +482,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.TrustCenter.ID(childComplexity), true + case "TrustCenter.isUserAuthenticated": + if e.complexity.TrustCenter.IsUserAuthenticated == nil { + break + } + + return e.complexity.TrustCenter.IsUserAuthenticated(childComplexity), true + case "TrustCenter.organization": if e.complexity.TrustCenter.Organization == nil { break @@ -913,6 +922,7 @@ type TrustCenter implements Node { active: Boolean! slug: String! organization: Organization! @goField(forceResolver: true) + isUserAuthenticated: Boolean! @goField(forceResolver: true) documents( first: Int @@ -2839,6 +2849,8 @@ func (ec *executionContext) fieldContext_Query_trustCenterBySlug(ctx context.Con return ec.fieldContext_TrustCenter_slug(ctx, field) case "organization": return ec.fieldContext_TrustCenter_organization(ctx, field) + case "isUserAuthenticated": + return ec.fieldContext_TrustCenter_isUserAuthenticated(ctx, field) case "documents": return ec.fieldContext_TrustCenter_documents(ctx, field) case "audits": @@ -3334,6 +3346,50 @@ func (ec *executionContext) fieldContext_TrustCenter_organization(_ context.Cont return fc, nil } +func (ec *executionContext) _TrustCenter_isUserAuthenticated(ctx context.Context, field graphql.CollectedField, obj *types.TrustCenter) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TrustCenter_isUserAuthenticated(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.TrustCenter().IsUserAuthenticated(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_TrustCenter_isUserAuthenticated(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TrustCenter", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _TrustCenter_documents(ctx context.Context, field graphql.CollectedField, obj *types.TrustCenter) (ret graphql.Marshaler) { fc, err := ec.fieldContext_TrustCenter_documents(ctx, field) if err != nil { @@ -7090,6 +7146,42 @@ func (ec *executionContext) _TrustCenter(ctx context.Context, sel ast.SelectionS continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "isUserAuthenticated": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._TrustCenter_isUserAuthenticated(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "documents": field := field diff --git a/pkg/server/api/trust/v1/types/types.go b/pkg/server/api/trust/v1/types/types.go index ead807084..9e5fccec5 100644 --- a/pkg/server/api/trust/v1/types/types.go +++ b/pkg/server/api/trust/v1/types/types.go @@ -115,13 +115,14 @@ func (Report) IsNode() {} func (this Report) GetID() gid.GID { return this.ID } type TrustCenter struct { - ID gid.GID `json:"id"` - Active bool `json:"active"` - Slug string `json:"slug"` - Organization *Organization `json:"organization"` - Documents *DocumentConnection `json:"documents"` - Audits *AuditConnection `json:"audits"` - Vendors *VendorConnection `json:"vendors"` + ID gid.GID `json:"id"` + Active bool `json:"active"` + Slug string `json:"slug"` + Organization *Organization `json:"organization"` + IsUserAuthenticated bool `json:"isUserAuthenticated"` + Documents *DocumentConnection `json:"documents"` + Audits *AuditConnection `json:"audits"` + Vendors *VendorConnection `json:"vendors"` } func (TrustCenter) IsNode() {} diff --git a/pkg/server/api/trust/v1/v1_resolver.go b/pkg/server/api/trust/v1/v1_resolver.go index f566ad65b..25a4df990 100644 --- a/pkg/server/api/trust/v1/v1_resolver.go +++ b/pkg/server/api/trust/v1/v1_resolver.go @@ -153,6 +153,14 @@ func (r *trustCenterResolver) Organization(ctx context.Context, obj *types.Trust return obj.Organization, nil } +// IsUserAuthenticated is the resolver for the isUserAuthenticated field. +func (r *trustCenterResolver) IsUserAuthenticated(ctx context.Context, obj *types.TrustCenter) (bool, error) { + if err := auth.ValidateTenantAccess(ctx, r, userTenantContextKey, obj.Organization.ID.TenantID()); err != nil { + return false, nil + } + return true, nil +} + // Documents is the resolver for the documents field. func (r *trustCenterResolver) Documents(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.DocumentConnection, error) { trust := r.trustCenterSvc.WithTenant(obj.Organization.ID.TenantID())