Refacto public trust center

- More robust front authentification check to display the right button
- Remove trust relay to avoid query loop

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-08-22 18:16:22 +02:00
parent 6d2867258e
commit 3639735215
14 changed files with 493 additions and 958 deletions

View File

@@ -5,7 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "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", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"check": "tsc --noEmit -p tsconfig.app.json", "check": "tsc --noEmit -p tsconfig.app.json",

View File

@@ -1,7 +0,0 @@
{
"src": "./src/trust",
"schema": "../../pkg/server/api/trust/v1/schema.graphql",
"language": "typescript",
"eagerEsModules": true,
"noFutureProofEnums": true
}

View File

@@ -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<T = unknown> {
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<string, never>;
async function trustCenterGraphQLRequest<T = unknown>(
operationName: string,
query: string,
variables: GraphQLVariables = {}
): Promise<GraphQLResponse<T>> {
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<TrustCenterQueryData>({
queryKey: ["trust-center", slug],
queryFn: async () => {
const result = await trustCenterGraphQLRequest<TrustCenterQueryData>(
"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<ExportDocumentPDFData, Error, string>({
mutationFn: async (documentId: string) => {
const result = await trustCenterGraphQLRequest<ExportDocumentPDFData>(
"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<CreateTrustCenterAccessData, Error, { trustCenterId: string; email: string; name: string }>({
mutationFn: async (input: { trustCenterId: string; email: string; name: string }) => {
const result = await trustCenterGraphQLRequest<CreateTrustCenterAccessData>(
"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;
},
});
}

View File

@@ -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<TrustAuthContextType | null>(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<Environment | null>(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 (
<TrustAuthContext.Provider value={authContextValue}>
<RelayEnvironmentProvider environment={environmentRef.current}>
{children}
</RelayEnvironmentProvider>
</TrustAuthContext.Provider>
);
}

View File

@@ -12,22 +12,7 @@ import { useTranslate } from "@probo/i18n";
import { sprintf } from "@probo/helpers"; import { sprintf } from "@probo/helpers";
import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { z } from "zod"; import { z } from "zod";
import { useMutation, graphql } from "react-relay"; import { useCreateTrustCenterAccess } from "/hooks/useTrustCenterQueries";
import type { PublicTrustCenterAccessRequestDialogMutation } from "./__generated__/PublicTrustCenterAccessRequestDialogMutation.graphql";
const CreateTrustCenterAccessMutation = graphql`
mutation PublicTrustCenterAccessRequestDialogMutation(
$input: CreateTrustCenterAccessInput!
) {
createTrustCenterAccess(input: $input) {
trustCenterAccess {
id
email
name
}
}
}
`;
type Props = { type Props = {
trigger: React.ReactNode; trigger: React.ReactNode;
@@ -45,7 +30,7 @@ export function PublicTrustCenterAccessRequestDialog({
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const [commitMutation] = useMutation<PublicTrustCenterAccessRequestDialogMutation>(CreateTrustCenterAccessMutation); const mutation = useCreateTrustCenterAccess();
const schema = z.object({ const schema = z.object({
name: z.string().min(1, __("Name is required")).min(2, __("Name must be at least 2 characters long")), name: z.string().min(1, __("Name is required")).min(2, __("Name must be at least 2 characters long")),
@@ -58,17 +43,15 @@ export function PublicTrustCenterAccessRequestDialog({
const onSubmit = handleSubmit(async (data) => { const onSubmit = handleSubmit(async (data) => {
setIsSubmitting(true); setIsSubmitting(true);
mutation.mutate(
commitMutation({ {
variables: {
input: {
trustCenterId, trustCenterId,
email: data.email, email: data.email,
name: data.name, name: data.name,
}, },
}, {
onCompleted: (response) => { onSuccess: (result) => {
if (response.createTrustCenterAccess) { if (result.createTrustCenterAccess) {
toast({ toast({
title: __("Request Submitted"), title: __("Request Submitted"),
description: __("Your access request has been submitted. You will receive an email if your request is approved."), description: __("Your access request has been submitted. You will receive an email if your request is approved."),
@@ -80,17 +63,16 @@ export function PublicTrustCenterAccessRequestDialog({
} }
setIsSubmitting(false); setIsSubmitting(false);
}, },
onError: (error) => { onError: (_: Error) => {
const errorMessage = error.message || __("An error occurred while submitting your request.");
toast({ toast({
title: __("Request Failed"), title: __("Error"),
description: errorMessage, description: __("An error occurred while submitting your request."),
variant: "error", variant: "error",
}); });
setIsSubmitting(false); setIsSubmitting(false);
}, },
}); }
);
}); });
return ( return (
@@ -128,9 +110,9 @@ export function PublicTrustCenterAccessRequestDialog({
<DialogFooter> <DialogFooter>
<Button <Button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting || mutation.isPending}
> >
{isSubmitting ? __("Submitting...") : __("Submit Request")} {isSubmitting || mutation.isPending ? __("Submitting...") : __("Submit Request")}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>

View File

@@ -13,20 +13,8 @@ import {
useToast, useToast,
} from "@probo/ui"; } from "@probo/ui";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { useMutation, graphql } from "react-relay"; import { useExportDocumentPDF, type TrustCenterDocument } from "/hooks/useTrustCenterQueries";
import { PublicTrustCenterAccessRequestDialog } from "./PublicTrustCenterAccessRequestDialog"; 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 = { type Props = {
documents: TrustCenterDocument[]; documents: TrustCenterDocument[];
@@ -44,17 +32,14 @@ export function PublicTrustCenterDocuments({
const { __ } = useTranslate(); const { __ } = useTranslate();
const { toast } = useToast(); const { toast } = useToast();
const [commitMutation] = useMutation<PublicTrustCenterDocumentsExportPDFMutation>(ExportDocumentPDFMutation); const mutation = useExportDocumentPDF();
const handleDownload = async (document: TrustCenterDocument) => { const handleDownload = (document: TrustCenterDocument) => {
commitMutation({ mutation.mutate(document.id, {
variables: { onSuccess: (data) => {
input: { documentId: document.id } if (data.exportDocumentPDF?.data) {
},
onCompleted: (response) => {
if (response.exportDocumentPDF?.data) {
const link = window.document.createElement("a"); const link = window.document.createElement("a");
link.href = response.exportDocumentPDF.data; link.href = data.exportDocumentPDF.data;
link.download = `${document.title}.pdf`; link.download = `${document.title}.pdf`;
window.document.body.appendChild(link); window.document.body.appendChild(link);
link.click(); link.click();
@@ -136,8 +121,9 @@ export function PublicTrustCenterDocuments({
variant="secondary" variant="secondary"
icon={IconArrowDown} icon={IconArrowDown}
onClick={() => handleDownload(document)} onClick={() => handleDownload(document)}
disabled={mutation.isPending}
> >
{__("Download")} {mutation.isPending ? __("Downloading...") : __("Download")}
</Button> </Button>
)} )}
</Td> </Td>

View File

@@ -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;

View File

@@ -1,92 +0,0 @@
/**
* @generated SignedSource<<e737361b21a8767acd6a604702875029>>
* @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;

View File

@@ -5,99 +5,16 @@ import { PublicTrustCenterLayout } from "/layouts/PublicTrustCenterLayout";
import { PublicTrustCenterAudits } from "../components/PublicTrustCenterAudits"; import { PublicTrustCenterAudits } from "../components/PublicTrustCenterAudits";
import { PublicTrustCenterVendors } from "../components/PublicTrustCenterVendors"; import { PublicTrustCenterVendors } from "../components/PublicTrustCenterVendors";
import { PublicTrustCenterDocuments } from "../components/PublicTrustCenterDocuments"; 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 { 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 { export type { TrustCenterDocument, TrustCenterAudit, TrustCenterVendor };
id: string;
title: string;
documentType: string;
}
export interface TrustCenterAudit { export default function PublicTrustCenterPage() {
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 { __ } = useTranslate();
const { slug } = useParams<{ slug: string }>(); const { slug } = useParams<{ slug: string }>();
const { isAuthenticated } = useTrustAuth();
if (!slug) { const { data, isLoading, error } = useTrustCenterQuery(slug || "");
return <Navigate to="/" replace />;
}
const data = useLazyLoadQuery<PublicTrustCenterPageQuery>(PublicTrustCenterQuery, { slug });
const organization = data?.trustCenterBySlug?.organization; const organization = data?.trustCenterBySlug?.organization;
const organizationName = organization?.name || ""; const organizationName = organization?.name || "";
@@ -106,6 +23,33 @@ function PublicTrustCenterContent() {
organizationName ? `${organizationName} - Trust Center` : "Trust Center" organizationName ? `${organizationName} - Trust Center` : "Trust Center"
); );
if (!slug) {
return <Navigate to="/" replace />;
}
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<Spinner />
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-2">
{__("Error Loading Trust Center")}
</h1>
<p className="text-gray-600">
{__("There was an error loading the trust center. Please try again later.")}
</p>
</div>
</div>
);
}
if (!data?.trustCenterBySlug) { if (!data?.trustCenterBySlug) {
return ( return (
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
@@ -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 trustCenterDocuments = documents.edges.map((edge) => edge.node) as TrustCenterDocument[];
const trustCenterAudits = audits.edges.map((edge) => edge.node) as TrustCenterAudit[]; const trustCenterAudits = audits.edges.map((edge) => edge.node) as TrustCenterAudit[];
@@ -131,20 +76,20 @@ function PublicTrustCenterContent() {
<PublicTrustCenterLayout <PublicTrustCenterLayout
organizationName={organizationName} organizationName={organizationName}
organizationLogo={organization?.logoUrl} organizationLogo={organization?.logoUrl}
isAuthenticated={isAuthenticated} isAuthenticated={isUserAuthenticated}
> >
<div className="space-y-12"> <div className="space-y-12">
<PublicTrustCenterAudits <PublicTrustCenterAudits
audits={trustCenterAudits} audits={trustCenterAudits}
organizationName={organizationName} organizationName={organizationName}
isAuthenticated={isAuthenticated} isAuthenticated={isUserAuthenticated}
trustCenterId={data.trustCenterBySlug.id} trustCenterId={trustCenterBySlug.id}
/> />
<PublicTrustCenterDocuments <PublicTrustCenterDocuments
documents={trustCenterDocuments} documents={trustCenterDocuments}
organizationName={organizationName} organizationName={organizationName}
isAuthenticated={isAuthenticated} isAuthenticated={isUserAuthenticated}
trustCenterId={data.trustCenterBySlug.id} trustCenterId={trustCenterBySlug.id}
/> />
<PublicTrustCenterVendors <PublicTrustCenterVendors
vendors={trustCenterVendors} vendors={trustCenterVendors}
@@ -154,13 +99,3 @@ function PublicTrustCenterContent() {
</PublicTrustCenterLayout> </PublicTrustCenterLayout>
); );
} }
export default function PublicTrustCenterPage() {
return (
<TrustRelayProvider>
<Suspense fallback={<Spinner />}>
<PublicTrustCenterContent />
</Suspense>
</TrustRelayProvider>
);
}

View File

@@ -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;

View File

@@ -200,6 +200,7 @@ type TrustCenter implements Node {
active: Boolean! active: Boolean!
slug: String! slug: String!
organization: Organization! @goField(forceResolver: true) organization: Organization! @goField(forceResolver: true)
isUserAuthenticated: Boolean! @goField(forceResolver: true)
documents( documents(
first: Int first: Int

View File

@@ -135,6 +135,7 @@ type ComplexityRoot struct {
Audits func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) 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 Documents func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
ID func(childComplexity int) int ID func(childComplexity int) int
IsUserAuthenticated func(childComplexity int) int
Organization func(childComplexity int) int Organization func(childComplexity int) int
Slug func(childComplexity int) int Slug func(childComplexity int) int
Vendors func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int Vendors func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
@@ -186,6 +187,7 @@ type ReportResolver interface {
} }
type TrustCenterResolver interface { type TrustCenterResolver interface {
Organization(ctx context.Context, obj *types.TrustCenter) (*types.Organization, error) 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) 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) 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) 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 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": case "TrustCenter.organization":
if e.complexity.TrustCenter.Organization == nil { if e.complexity.TrustCenter.Organization == nil {
break break
@@ -913,6 +922,7 @@ type TrustCenter implements Node {
active: Boolean! active: Boolean!
slug: String! slug: String!
organization: Organization! @goField(forceResolver: true) organization: Organization! @goField(forceResolver: true)
isUserAuthenticated: Boolean! @goField(forceResolver: true)
documents( documents(
first: Int first: Int
@@ -2839,6 +2849,8 @@ func (ec *executionContext) fieldContext_Query_trustCenterBySlug(ctx context.Con
return ec.fieldContext_TrustCenter_slug(ctx, field) return ec.fieldContext_TrustCenter_slug(ctx, field)
case "organization": case "organization":
return ec.fieldContext_TrustCenter_organization(ctx, field) return ec.fieldContext_TrustCenter_organization(ctx, field)
case "isUserAuthenticated":
return ec.fieldContext_TrustCenter_isUserAuthenticated(ctx, field)
case "documents": case "documents":
return ec.fieldContext_TrustCenter_documents(ctx, field) return ec.fieldContext_TrustCenter_documents(ctx, field)
case "audits": case "audits":
@@ -3334,6 +3346,50 @@ func (ec *executionContext) fieldContext_TrustCenter_organization(_ context.Cont
return fc, nil 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) { 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) fc, err := ec.fieldContext_TrustCenter_documents(ctx, field)
if err != nil { if err != nil {
@@ -7090,6 +7146,42 @@ func (ec *executionContext) _TrustCenter(ctx context.Context, sel ast.SelectionS
continue 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) }) out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
case "documents": case "documents":
field := field field := field

View File

@@ -119,6 +119,7 @@ type TrustCenter struct {
Active bool `json:"active"` Active bool `json:"active"`
Slug string `json:"slug"` Slug string `json:"slug"`
Organization *Organization `json:"organization"` Organization *Organization `json:"organization"`
IsUserAuthenticated bool `json:"isUserAuthenticated"`
Documents *DocumentConnection `json:"documents"` Documents *DocumentConnection `json:"documents"`
Audits *AuditConnection `json:"audits"` Audits *AuditConnection `json:"audits"`
Vendors *VendorConnection `json:"vendors"` Vendors *VendorConnection `json:"vendors"`

View File

@@ -153,6 +153,14 @@ func (r *trustCenterResolver) Organization(ctx context.Context, obj *types.Trust
return obj.Organization, nil 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. // 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) { 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()) trust := r.trustCenterSvc.WithTenant(obj.Organization.ID.TenantID())