From d31f611e633e9085b4dc6f2f832638920d31782f Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Wed, 20 Aug 2025 23:34:50 +0200 Subject: [PATCH] Use relay and refacto public trust center Signed-off-by: Sacha Al Himdani --- apps/console/package.json | 2 +- apps/console/relay.config.json | 3 +- apps/console/relay.trust.config.json | 7 + .../src/hooks/graph/PublicTrustCenterGraph.ts | 56 --- .../src/layouts/PublicTrustCenterLayout.tsx | 7 +- .../src/pages/PublicTrustCenterPage.tsx | 234 ---------- .../{ => trustCenter}/TrustCenterPage.tsx | 0 .../src/providers/TrustRelayProvider.tsx | 89 ++-- apps/console/src/routes.tsx | 2 +- apps/console/src/routes/trustCenterRoutes.ts | 2 +- .../PublicTrustCenterAccessRequestDialog.tsx} | 121 ++--- .../components}/PublicTrustCenterAudits.tsx | 23 +- .../PublicTrustCenterDocuments.tsx | 107 ++--- .../components}/PublicTrustCenterVendors.tsx | 13 +- ...nterAccessRequestDialogMutation.graphql.ts | 123 +++++ ...enterDocumentsExportPDFMutation.graphql.ts | 92 ++++ .../src/trust/pages/PublicTrustCenterPage.tsx | 166 +++++++ .../PublicTrustCenterPageQuery.graphql.ts | 430 ++++++++++++++++++ pkg/trust/trust_center_access_service.go | 3 + 19 files changed, 982 insertions(+), 498 deletions(-) create mode 100644 apps/console/relay.trust.config.json delete mode 100644 apps/console/src/hooks/graph/PublicTrustCenterGraph.ts delete mode 100644 apps/console/src/pages/PublicTrustCenterPage.tsx rename apps/console/src/pages/organizations/{ => trustCenter}/TrustCenterPage.tsx (100%) rename apps/console/src/{components/trustCenter/TrustCenterAccessRequestDialog.tsx => trust/components/PublicTrustCenterAccessRequestDialog.tsx} (53%) rename apps/console/src/{components/trustCenter => trust/components}/PublicTrustCenterAudits.tsx (90%) rename apps/console/src/{components/trustCenter => trust/components}/PublicTrustCenterDocuments.tsx (57%) rename apps/console/src/{components/trustCenter => trust/components}/PublicTrustCenterVendors.tsx (93%) create mode 100644 apps/console/src/trust/components/__generated__/PublicTrustCenterAccessRequestDialogMutation.graphql.ts create mode 100644 apps/console/src/trust/components/__generated__/PublicTrustCenterDocumentsExportPDFMutation.graphql.ts create mode 100644 apps/console/src/trust/pages/PublicTrustCenterPage.tsx create mode 100644 apps/console/src/trust/pages/__generated__/PublicTrustCenterPageQuery.graphql.ts diff --git a/apps/console/package.json b/apps/console/package.json index 51d8268f7..b06fd3f5c 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", + "relay": "find src -type d -name \"__generated__\" -exec rm -rf {} + && npx relay-compiler ./relay.config.json && npx relay-compiler ./relay.trust.config.json", "build": "tsc -b && vite build", "lint": "eslint .", "check": "tsc --noEmit -p tsconfig.app.json", diff --git a/apps/console/relay.config.json b/apps/console/relay.config.json index 5151fee83..56677b108 100644 --- a/apps/console/relay.config.json +++ b/apps/console/relay.config.json @@ -5,6 +5,7 @@ "eagerEsModules": true, "noFutureProofEnums": true, "excludes": [ - "**/PublicTrustCenterGraph.ts" + "**/PublicTrustCenterGraph.ts", + "**/trust/**" ] } diff --git a/apps/console/relay.trust.config.json b/apps/console/relay.trust.config.json new file mode 100644 index 000000000..0d94d15d5 --- /dev/null +++ b/apps/console/relay.trust.config.json @@ -0,0 +1,7 @@ +{ + "src": "./src/trust", + "schema": "../../pkg/server/api/trust/v1/schema.graphql", + "language": "typescript", + "eagerEsModules": true, + "noFutureProofEnums": true +} diff --git a/apps/console/src/hooks/graph/PublicTrustCenterGraph.ts b/apps/console/src/hooks/graph/PublicTrustCenterGraph.ts deleted file mode 100644 index 6aaa04faa..000000000 --- a/apps/console/src/hooks/graph/PublicTrustCenterGraph.ts +++ /dev/null @@ -1,56 +0,0 @@ -// Manual query definition for trust API (not processed by relay compiler) -export const publicTrustCenterQuery = { - params: { - name: "PublicTrustCenterGraphQuery", - operationKind: "query", - text: ` - query PublicTrustCenterGraphQuery($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 - } - } - } - } - } - ` - } -}; diff --git a/apps/console/src/layouts/PublicTrustCenterLayout.tsx b/apps/console/src/layouts/PublicTrustCenterLayout.tsx index 3cfe9aa96..4429cea3d 100644 --- a/apps/console/src/layouts/PublicTrustCenterLayout.tsx +++ b/apps/console/src/layouts/PublicTrustCenterLayout.tsx @@ -17,13 +17,18 @@ export function PublicTrustCenterLayout({ organizationName, organizationLogo, ch const handleLogout = async () => { try { - await fetch(buildEndpoint('/api/trust/v1/auth/logout'), { + const response = await fetch(buildEndpoint('/api/trust/v1/auth/logout'), { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, credentials: 'include', }); + + if (!response.ok) { + throw new Error("Logout failed"); + } + window.location.reload(); } catch (error) { toast({ diff --git a/apps/console/src/pages/PublicTrustCenterPage.tsx b/apps/console/src/pages/PublicTrustCenterPage.tsx deleted file mode 100644 index a9c9dc160..000000000 --- a/apps/console/src/pages/PublicTrustCenterPage.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import { useParams, Navigate } from "react-router"; -import { usePageTitle } from "@probo/hooks"; -import { useTranslate } from "@probo/i18n"; -import { sprintf } from "@probo/helpers"; -import { publicTrustCenterQuery } from "/hooks/graph/PublicTrustCenterGraph"; -import { PublicTrustCenterLayout } from "/layouts/PublicTrustCenterLayout"; -import { PublicTrustCenterAudits } from "../components/trustCenter/PublicTrustCenterAudits"; -import { PublicTrustCenterVendors } from "../components/trustCenter/PublicTrustCenterVendors"; -import { PublicTrustCenterDocuments } from "../components/trustCenter/PublicTrustCenterDocuments"; -import { PageError } from "/components/PageError"; -import { TrustRelayProvider } from "/providers/TrustRelayProvider"; -import { useState, useEffect } from "react"; -import { buildEndpoint } from "/providers/RelayProviders"; -import { Spinner } from "@probo/ui"; -interface GraphQLError { - message: string; - path?: string[]; - locations?: Array<{ line: number; column: number }>; -} - -interface GraphQLResponse { - data?: T; - errors?: GraphQLError[]; -} - -interface Organization { - id: string; - name: string; - logoUrl?: string; -} - -interface Framework { - name: string; -} - -interface DocumentVersion { - id: string; - status: string; -} - -interface DocumentVersionConnection { - edges: Array<{ - node: DocumentVersion; - }>; -} - -interface Document { - id: string; - title: string; - documentType: string; - versions: DocumentVersionConnection; -} - -interface Audit { - id: string; - framework: Framework; - validFrom: string; - validUntil: string | null; - state: string; - createdAt: string; - report: { - id: string; - filename: string; - downloadUrl: string | null; - } | null; -} - -interface Vendor { - id: string; - name: string; - category: string; - description: string | null; - createdAt: string; - websiteUrl?: string | null; - privacyPolicyUrl?: string | null; -} - -interface Connection { - edges: Array<{ - node: T; - }>; -} - -interface TrustCenter { - id: string; - active: boolean; - slug: string; - organization: Organization; - documents: Connection; - audits: Connection; - vendors: Connection; -} - -interface PublicTrustCenterData { - trustCenterBySlug?: TrustCenter; -} - -function PublicTrustCenterContent() { - const { __ } = useTranslate(); - const { slug } = useParams<{ slug: string }>(); - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [isAuthenticated, setIsAuthenticated] = useState(false); - - const organizationName = data?.trustCenterBySlug?.organization?.name; - usePageTitle( - organizationName ? `${organizationName} - Trust Center` : "Trust Center" - ); - - useEffect(() => { - if (!slug) { - setLoading(false); - return; - } - - setLoading(true); - setIsAuthenticated(false); - - fetch(buildEndpoint("/api/trust/v1/graphql"), { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - credentials: "include", - body: JSON.stringify({ - operationName: publicTrustCenterQuery.params.name, - query: publicTrustCenterQuery.params.text, - variables: { slug }, - }), - }) - .then((response) => response.json()) - .then((result: GraphQLResponse) => { - const accessDeniedErrors = - result.errors?.filter((error: GraphQLError) => - error.message.includes("access denied") - ) || []; - - const nonAuthErrors = - result.errors?.filter( - (error: GraphQLError) => !error.message.includes("access denied") - ) || []; - - if (nonAuthErrors.length > 0) { - throw new Error(nonAuthErrors[0].message); - } - - setIsAuthenticated(accessDeniedErrors.length === 0); - setData(result.data || null); - }) - .catch(setError) - .finally(() => setLoading(false)); - }, [slug]); - - if (!slug) { - return ; - } - - if (loading) { - return ( -
- -
- ); - } - - if (error) { - return ; - } - - const { trustCenterBySlug } = data || {}; - - if (!trustCenterBySlug) { - return ; - } - - if (!trustCenterBySlug.active) { - return ; - } - - const { organization } = trustCenterBySlug; - - const documents = trustCenterBySlug.documents.edges.map((edge) => edge.node); - - const audits = trustCenterBySlug.audits.edges.map((edge) => edge.node); - - const vendors = trustCenterBySlug.vendors.edges.map((edge) => edge.node); - - return ( - -
-
-

- {sprintf(__("%s Trust Center"), organization.name)} -

-

- {__( - "Explore our security practices, compliance certifications, and transparency reports." - )} -

-
- - - -
-
- ); -} - -export default function PublicTrustCenterPage() { - return ( - - - - ); -} diff --git a/apps/console/src/pages/organizations/TrustCenterPage.tsx b/apps/console/src/pages/organizations/trustCenter/TrustCenterPage.tsx similarity index 100% rename from apps/console/src/pages/organizations/TrustCenterPage.tsx rename to apps/console/src/pages/organizations/trustCenter/TrustCenterPage.tsx diff --git a/apps/console/src/providers/TrustRelayProvider.tsx b/apps/console/src/providers/TrustRelayProvider.tsx index b438b3d82..25c464b15 100644 --- a/apps/console/src/providers/TrustRelayProvider.tsx +++ b/apps/console/src/providers/TrustRelayProvider.tsx @@ -8,6 +8,7 @@ import { 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 { @@ -17,7 +18,22 @@ export class TrustCenterError extends Error { } } -const fetchTrustRelay: FetchFunction = async (request, variables) => { +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: { @@ -25,7 +41,7 @@ const fetchTrustRelay: FetchFunction = async (request, variables) => { "application/graphql-response+json; charset=utf-8, application/json; charset=utf-8", "Content-Type": "application/json", }, - credentials: "include", // Include cookies for authentication + credentials: "include", body: JSON.stringify({ operationName: request.name, query: request.text, @@ -44,37 +60,58 @@ const fetchTrustRelay: FetchFunction = async (request, variables) => { const json = await response.json(); - if (json.errors) { - throw new TrustCenterError( - `Error fetching GraphQL query '${ - request.name - }' with variables '${JSON.stringify(variables)}': ${JSON.stringify( - json.errors - )}` + 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; }; -const trustSource = new RecordSource(); -const trustStore = new Store(trustSource, { - queryCacheExpirationTime: 5 * 60 * 1000, // 5 minutes for trust center content - gcReleaseBufferSize: 10, -}); - -export const trustRelayEnvironment = new Environment({ - network: Network.create(fetchTrustRelay), - store: trustStore, -}); - -/** - * Provider for trust center Relay environment (public API) - */ 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} - + + + {children} + + ); } diff --git a/apps/console/src/routes.tsx b/apps/console/src/routes.tsx index be10c680a..236e38802 100644 --- a/apps/console/src/routes.tsx +++ b/apps/console/src/routes.tsx @@ -112,7 +112,7 @@ const routes = [ path: "/trust/:slug", ErrorBoundary: ErrorBoundary, fallback: PageSkeleton, - Component: lazy(() => import("./pages/PublicTrustCenterPage")), + Component: lazy(() => import("./trust/pages/PublicTrustCenterPage")), }, { path: "/trust/:slug/access", diff --git a/apps/console/src/routes/trustCenterRoutes.ts b/apps/console/src/routes/trustCenterRoutes.ts index dc5470a2a..d869e9df1 100644 --- a/apps/console/src/routes/trustCenterRoutes.ts +++ b/apps/console/src/routes/trustCenterRoutes.ts @@ -13,7 +13,7 @@ export const trustCenterRoutes = [ queryLoader: ({ organizationId }) => loadQuery(relayEnvironment, trustCenterQuery, { organizationId }), Component: lazy( - () => import("/pages/organizations/TrustCenterPage") + () => import("/pages/organizations/trustCenter/TrustCenterPage") ), children: [ { diff --git a/apps/console/src/components/trustCenter/TrustCenterAccessRequestDialog.tsx b/apps/console/src/trust/components/PublicTrustCenterAccessRequestDialog.tsx similarity index 53% rename from apps/console/src/components/trustCenter/TrustCenterAccessRequestDialog.tsx rename to apps/console/src/trust/components/PublicTrustCenterAccessRequestDialog.tsx index 4d1121aca..5e27be233 100644 --- a/apps/console/src/components/trustCenter/TrustCenterAccessRequestDialog.tsx +++ b/apps/console/src/trust/components/PublicTrustCenterAccessRequestDialog.tsx @@ -12,41 +12,22 @@ import { useTranslate } from "@probo/i18n"; import { sprintf } from "@probo/helpers"; import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { z } from "zod"; -import { buildEndpoint } from "/providers/RelayProviders"; +import { useMutation, graphql } from "react-relay"; +import type { PublicTrustCenterAccessRequestDialogMutation } from "./__generated__/PublicTrustCenterAccessRequestDialogMutation.graphql"; -// Manual mutation for trust API (not processed by relay compiler) -const createTrustCenterAccessMutation = { - params: { - name: "CreateTrustCenterAccessMutation", - operationKind: "mutation", - text: ` - mutation CreateTrustCenterAccessMutation( - $input: CreateTrustCenterAccessInput! - ) { - createTrustCenterAccess(input: $input) { - trustCenterAccess { - id - email - name - } - } +const CreateTrustCenterAccessMutation = graphql` + mutation PublicTrustCenterAccessRequestDialogMutation( + $input: CreateTrustCenterAccessInput! + ) { + createTrustCenterAccess(input: $input) { + trustCenterAccess { + id + email + name } - ` + } } -}; - -type CreateTrustCenterAccessResponse = { - data?: { - createTrustCenterAccess?: { - trustCenterAccess: { - id: string; - email: string; - name: string; - }; - }; - }; - errors?: Array<{ message: string }>; -}; +`; type Props = { trigger: React.ReactNode; @@ -54,7 +35,7 @@ type Props = { organizationName: string; }; -export function TrustCenterAccessRequestDialog({ +export function PublicTrustCenterAccessRequestDialog({ trigger, trustCenterId, organizationName @@ -64,6 +45,8 @@ export function TrustCenterAccessRequestDialog({ const [isSubmitting, setIsSubmitting] = useState(false); const dialogRef = useDialogRef(); + const [commitMutation] = useMutation(CreateTrustCenterAccessMutation); + const schema = z.object({ name: z.string().min(1, __("Name is required")).min(2, __("Name must be at least 2 characters long")), email: z.string().min(1, __("Email is required")).email(__("Please enter a valid email address")), @@ -76,56 +59,38 @@ export function TrustCenterAccessRequestDialog({ const onSubmit = handleSubmit(async (data) => { setIsSubmitting(true); - try { - const response = await fetch(buildEndpoint("/api/trust/v1/graphql"), { - method: "POST", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", + commitMutation({ + variables: { + input: { + trustCenterId, + email: data.email, + name: data.name, }, - credentials: "include", - body: JSON.stringify({ - operationName: createTrustCenterAccessMutation.params.name, - query: createTrustCenterAccessMutation.params.text, - variables: { - input: { - trustCenterId, - email: data.email, - name: data.name - } - }, - }), - }); + }, + onCompleted: (response) => { + if (response.createTrustCenterAccess) { + toast({ + title: __("Request Submitted"), + description: __("Your access request has been submitted. You will receive an email if your request is approved."), + variant: "success", + }); - const result: CreateTrustCenterAccessResponse = await response.json(); + reset(); + dialogRef.current?.close(); + } + setIsSubmitting(false); + }, + onError: (error) => { + const errorMessage = error.message || __("An error occurred while submitting your request."); - if (result.errors) { - throw new Error(result.errors[0].message); - } - - if (result.data?.createTrustCenterAccess) { toast({ - title: __("Request Submitted"), - description: __("Your access request has been submitted. You will receive an email if your request is approved."), - variant: "success", + title: __("Request Failed"), + description: errorMessage, + variant: "error", }); - - reset(); - dialogRef.current?.close(); - } - } catch (error) { - const errorMessage = error instanceof Error - ? error.message - : __("An error occurred while submitting your request."); - - toast({ - title: __("Request Failed"), - description: errorMessage, - variant: "error", - }); - } finally { - setIsSubmitting(false); - } + setIsSubmitting(false); + }, + }); }); return ( diff --git a/apps/console/src/components/trustCenter/PublicTrustCenterAudits.tsx b/apps/console/src/trust/components/PublicTrustCenterAudits.tsx similarity index 90% rename from apps/console/src/components/trustCenter/PublicTrustCenterAudits.tsx rename to apps/console/src/trust/components/PublicTrustCenterAudits.tsx index 88c03bfc1..00feb7d36 100644 --- a/apps/console/src/components/trustCenter/PublicTrustCenterAudits.tsx +++ b/apps/console/src/trust/components/PublicTrustCenterAudits.tsx @@ -13,26 +13,11 @@ import { import { useTranslate } from "@probo/i18n"; import { sprintf } from "@probo/helpers"; import { FrameworkLogo } from "/components/FrameworkLogo"; -import { TrustCenterAccessRequestDialog } from "./TrustCenterAccessRequestDialog"; - -type Audit = { - id: string; - framework: { - name: string; - }; - validFrom: string; - validUntil: string | null; - state: string; - createdAt: string; - report: { - id: string; - filename: string; - downloadUrl: string | null; - } | null; -}; +import { PublicTrustCenterAccessRequestDialog } from "./PublicTrustCenterAccessRequestDialog"; +import type { TrustCenterAudit } from "../pages/PublicTrustCenterPage"; type Props = { - audits: Audit[]; + audits: TrustCenterAudit[]; organizationName: string; isAuthenticated: boolean; trustCenterId: string; @@ -105,7 +90,7 @@ export function PublicTrustCenterAudits({ {__("No report")} ) : !isAuthenticated ? ( - ; -}; - export function PublicTrustCenterDocuments({ documents, isAuthenticated, @@ -63,43 +44,31 @@ export function PublicTrustCenterDocuments({ const { __ } = useTranslate(); const { toast } = useToast(); - const handleDownload = async (document: Document) => { - try { - const response = await fetch(buildEndpoint("/api/trust/v1/graphql"), { - method: "POST", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - }, - credentials: "include", - body: JSON.stringify({ - operationName: exportDocumentPDFMutation.params.name, - query: exportDocumentPDFMutation.params.text, - variables: { input: { documentId: document.id } }, - }), - }); + const [commitMutation] = useMutation(ExportDocumentPDFMutation); - const result: ExportDocumentPDFResponse = await response.json(); - - if (result.errors) { - throw new Error(result.errors[0].message); - } - - if (result.data?.exportDocumentPDF?.data) { - const link = window.document.createElement("a"); - link.href = result.data.exportDocumentPDF.data; - link.download = `${document.title}.pdf`; - window.document.body.appendChild(link); - link.click(); - window.document.body.removeChild(link); - } - } catch (error) { - toast({ - title: __("Download Failed"), - description: __("Unable to download the document. Please try again."), - variant: "error", - }); - } + const handleDownload = async (document: TrustCenterDocument) => { + commitMutation({ + variables: { + input: { documentId: document.id } + }, + onCompleted: (response) => { + if (response.exportDocumentPDF?.data) { + const link = window.document.createElement("a"); + link.href = response.exportDocumentPDF.data; + link.download = `${document.title}.pdf`; + window.document.body.appendChild(link); + link.click(); + window.document.body.removeChild(link); + } + }, + onError: () => { + toast({ + title: __("Download Failed"), + description: __("Unable to download the document. Please try again."), + variant: "error", + }); + }, + }); }; if (documents.length === 0) { @@ -150,7 +119,7 @@ export function PublicTrustCenterDocuments({ {!isAuthenticated ? ( - > + * @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 new file mode 100644 index 000000000..223c883f3 --- /dev/null +++ b/apps/console/src/trust/components/__generated__/PublicTrustCenterDocumentsExportPDFMutation.graphql.ts @@ -0,0 +1,92 @@ +/** + * @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 new file mode 100644 index 000000000..088ccca05 --- /dev/null +++ b/apps/console/src/trust/pages/PublicTrustCenterPage.tsx @@ -0,0 +1,166 @@ +import { useParams, Navigate } from "react-router"; +import { usePageTitle } from "@probo/hooks"; +import { useTranslate } from "@probo/i18n"; +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"; + +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; +} + +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() { + const { __ } = useTranslate(); + const { slug } = useParams<{ slug: string }>(); + const { isAuthenticated } = useTrustAuth(); + + if (!slug) { + return ; + } + + const data = useLazyLoadQuery(PublicTrustCenterQuery, { slug }); + + const organization = data?.trustCenterBySlug?.organization; + const organizationName = organization?.name || ""; + + usePageTitle( + organizationName ? `${organizationName} - Trust Center` : "Trust Center" + ); + + if (!data?.trustCenterBySlug) { + return ( +
+
+

+ {__("Trust Center Not Found")} +

+

+ {__("The trust center you're looking for doesn't exist.")} +

+
+
+ ); + } + + const { documents, audits, vendors } = data.trustCenterBySlug; + + const trustCenterDocuments = documents.edges.map((edge) => edge.node) as TrustCenterDocument[]; + const trustCenterAudits = audits.edges.map((edge) => edge.node) as TrustCenterAudit[]; + const trustCenterVendors = vendors.edges.map((edge) => edge.node) as TrustCenterVendor[]; + + return ( + +
+ + + +
+
+ ); +} + +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 new file mode 100644 index 000000000..90efd138c --- /dev/null +++ b/apps/console/src/trust/pages/__generated__/PublicTrustCenterPageQuery.graphql.ts @@ -0,0 +1,430 @@ +/** + * @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/trust/trust_center_access_service.go b/pkg/trust/trust_center_access_service.go index 7203d8ffa..51a2e7954 100644 --- a/pkg/trust/trust_center_access_service.go +++ b/pkg/trust/trust_center_access_service.go @@ -102,6 +102,9 @@ func (s TrustCenterAccessService) Create( err := existingAccess.LoadByTrustCenterIDAndEmail(ctx, tx, s.svc.scope, req.TrustCenterID, req.Email) if err == nil { + if existingAccess.Active { + return fmt.Errorf("active trust center access already exists for this email") + } if err := existingAccess.Delete(ctx, tx, s.svc.scope); err != nil { return fmt.Errorf("cannot delete existing trust center access: %w", err) }