Add magic link login for trust center

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-01-15 12:31:10 +04:00
committed by Bryan Frimin
parent 8ff4693bfc
commit 8b3bda56e6
42 changed files with 1880 additions and 993 deletions

View File

@@ -1,6 +1,6 @@
import { graphql } from "relay-runtime";
import type { AuditRowFragment$key } from "./__generated__/AuditRowFragment.graphql";
import { useFragment } from "react-relay";
import { useFragment, useMutation } from "react-relay";
import {
Breadcrumb,
Button,
@@ -12,14 +12,27 @@ import {
IconMedal,
Spinner,
Table,
useToast,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import type { AuditRowDownloadMutation } from "./__generated__/AuditRowDownloadMutation.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToast";
import { downloadFile } from "@probo/helpers";
import { type PropsWithChildren, useState } from "react";
import { downloadFile, formatError } from "@probo/helpers";
import { type PropsWithChildren, use, useState } from "react";
import { useLocation } from "react-router";
import { RequestAccessDialog } from "/components/RequestAccessDialog.tsx";
import { Viewer } from "/providers/Viewer";
import { MagicLinkDialog } from "./MagicLinkDialog";
import type { AuditRow_requestAccessMutation } from "./__generated__/AuditRow_requestAccessMutation.graphql";
const requestAccessMutation = graphql`
mutation AuditRow_requestAccessMutation($input: RequestReportAccessInput!) {
requestReportAccess(input: $input) {
trustCenterAccess {
id
}
}
}
`;
const downloadMutation = graphql`
mutation AuditRowDownloadMutation($input: ExportReportPDFInput!) {
@@ -47,10 +60,53 @@ const auditRowFragment = graphql`
`;
export function AuditRow(props: { audit: AuditRowFragment$key }) {
const audit = useFragment(auditRowFragment, props.audit);
const { __ } = useTranslate();
const viewer = use(Viewer);
const { toast } = useToast();
const audit = useFragment(auditRowFragment, props.audit);
const [hasRequested, setHasRequested] = useState(
audit.report?.hasUserRequestedAccess,
);
const [requestAccess, isRequestingAccess] =
useMutation<AuditRow_requestAccessMutation>(requestAccessMutation);
const [commitDownload, downloading] =
useMutationWithToasts<AuditRowDownloadMutation>(downloadMutation);
const handleRequestAccess = () => {
requestAccess({
variables: {
input: {
reportId: audit.report?.id ?? "",
},
},
onCompleted: (_, errors) => {
if (errors?.length) {
toast({
title: __("Error"),
description: formatError(__("Cannot request access"), errors),
variant: "error",
});
return;
}
setHasRequested(true);
toast({
title: __("Success"),
description: __("Access request submitted successfully."),
variant: "success",
});
},
onError: (error) => {
toast({
title: __("Error"),
description: error.message ?? __("Cannot request access"),
variant: "error",
});
},
});
};
const handleDownload = () => {
if (!audit.report?.id) {
return;
@@ -67,41 +123,46 @@ export function AuditRow(props: { audit: AuditRowFragment$key }) {
});
};
const [hasRequested, setHasRequested] = useState(
audit.report?.hasUserRequestedAccess
);
return (
<div className="text-sm border border-border-solid -mt-px flex gap-3 flex-col md:flex-row md:justify-between px-6 py-3">
<div className="flex items-center gap-2">
<IconMedal size={16} className="flex-none text-txt-tertiary" />
{audit.framework.name}
</div>
{audit.report && audit.report.isUserAuthorized && (
<Button
className="w-full md:w-max"
variant="secondary"
disabled={downloading}
icon={downloading ? Spinner : IconArrowInbox}
onClick={handleDownload}
>
{__("Download")}
</Button>
)}
{audit.report && !audit.report.isUserAuthorized && (
<RequestAccessDialog
reportId={audit.report.id}
onSuccess={() => setHasRequested(true)}
>
{!viewer && (
<MagicLinkDialog>
<Button
disabled={hasRequested}
className="w-full md:w-max"
variant="secondary"
icon={IconLock}
>
{hasRequested ? __("Access requested") : __("Request access")}
</Button>
</RequestAccessDialog>
</MagicLinkDialog>
)}
{viewer &&
audit.report &&
(audit.report.isUserAuthorized ? (
<Button
className="w-full md:w-max"
variant="secondary"
disabled={downloading}
icon={downloading ? Spinner : IconArrowInbox}
onClick={handleDownload}
>
{__("Download")}
</Button>
) : (
<Button
disabled={hasRequested || isRequestingAccess}
className="w-full md:w-max"
variant="secondary"
icon={IconLock}
onClick={handleRequestAccess}
>
{hasRequested ? __("Access requested") : __("Request access")}
</Button>
))}
</div>
);
}
@@ -134,7 +195,7 @@ export function AuditRowAvatar(props: { audit: AuditRowFragment$key }) {
}
function AuditDialog(
props: PropsWithChildren<{ audit: AuditRowFragment$key; logo?: string }>
props: PropsWithChildren<{ audit: AuditRowFragment$key; logo?: string }>,
) {
const audit = useFragment(auditRowFragment, props.audit);
const location = useLocation();

View File

@@ -1,19 +1,34 @@
import { graphql } from "relay-runtime";
import type { DocumentRowFragment$key } from "./__generated__/DocumentRowFragment.graphql";
import { useFragment } from "react-relay";
import { useFragment, useMutation } from "react-relay";
import {
Button,
IconArrowInbox,
IconLock,
IconPageTextLine,
Spinner,
useToast,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import type { DocumentRowDownloadMutation } from "./__generated__/DocumentRowDownloadMutation.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToast";
import { downloadFile } from "@probo/helpers";
import { RequestAccessDialog } from "/components/RequestAccessDialog.tsx";
import { useState } from "react";
import { downloadFile, formatError } from "@probo/helpers";
import { use, useState } from "react";
import { MagicLinkDialog } from "./MagicLinkDialog";
import { Viewer } from "/providers/Viewer";
import type { DocumentRow_requestAccessMutation } from "./__generated__/DocumentRow_requestAccessMutation.graphql";
const requestAccessMutation = graphql`
mutation DocumentRow_requestAccessMutation(
$input: RequestDocumentAccessInput!
) {
requestDocumentAccess(input: $input) {
trustCenterAccess {
id
}
}
}
`;
const downloadMutation = graphql`
mutation DocumentRowDownloadMutation($input: ExportDocumentPDFInput!) {
@@ -33,10 +48,53 @@ const documentRowFragment = graphql`
`;
export function DocumentRow(props: { document: DocumentRowFragment$key }) {
const document = useFragment(documentRowFragment, props.document);
const { __ } = useTranslate();
const viewer = use(Viewer);
const { toast } = useToast();
const document = useFragment(documentRowFragment, props.document);
const [hasRequested, setHasRequested] = useState(
document.hasUserRequestedAccess,
);
const [requestAccess, isRequestingAccess] =
useMutation<DocumentRow_requestAccessMutation>(requestAccessMutation);
const [commitDownload, downloading] =
useMutationWithToasts<DocumentRowDownloadMutation>(downloadMutation);
const handleRequestAccess = () => {
requestAccess({
variables: {
input: {
documentId: document.id,
},
},
onCompleted: (_, errors) => {
if (errors?.length) {
toast({
title: __("Error"),
description: formatError(__("Cannot request access"), errors),
variant: "error",
});
return;
}
setHasRequested(true);
toast({
title: __("Success"),
description: __("Access request submitted successfully."),
variant: "success",
});
},
onError: (error) => {
toast({
title: __("Error"),
description: error.message ?? __("Cannot request access"),
variant: "error",
});
},
});
};
const handleDownload = () => {
commitDownload({
variables: {
@@ -49,40 +107,46 @@ export function DocumentRow(props: { document: DocumentRowFragment$key }) {
},
});
};
const [hasRequested, setHasRequested] = useState(
document.hasUserRequestedAccess,
);
return (
<div className="text-sm border border-border-solid -mt-px flex gap-3 flex-col md:flex-row md:justify-between px-6 py-3">
<div className="flex items-center gap-2">
<IconPageTextLine size={16} className=" flex-none text-txt-tertiary" />
{document.title}
</div>
{document.isUserAuthorized ? (
<Button
className="w-full md:w-max"
variant="secondary"
disabled={downloading}
icon={downloading ? Spinner : IconArrowInbox}
onClick={handleDownload}
>
{__("Download")}
</Button>
) : (
<RequestAccessDialog
documentId={document.id}
onSuccess={() => setHasRequested(true)}
>
{!viewer && (
<MagicLinkDialog>
<Button
disabled={hasRequested}
className="w-full md:w-max"
variant="secondary"
icon={IconLock}
>
{hasRequested ? __("Access requested") : __("Request access")}
</Button>
</RequestAccessDialog>
</MagicLinkDialog>
)}
{viewer &&
(document.isUserAuthorized ? (
<Button
className="w-full md:w-max"
variant="secondary"
disabled={downloading}
icon={downloading ? Spinner : IconArrowInbox}
onClick={handleDownload}
>
{__("Download")}
</Button>
) : (
<Button
disabled={hasRequested || isRequestingAccess}
className="w-full md:w-max"
variant="secondary"
icon={IconLock}
onClick={handleRequestAccess}
>
{hasRequested ? __("Access requested") : __("Request access")}
</Button>
))}
</div>
);
}

View File

@@ -0,0 +1,122 @@
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogTitle,
Field,
useDialogRef,
useToast,
} from "@probo/ui";
import { useMutation } from "react-relay";
import { graphql } from "relay-runtime";
import z from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { useTranslate } from "@probo/i18n";
import type { MagicLinkDialogMutation } from "./__generated__/MagicLinkDialogMutation.graphql";
import type { PropsWithChildren } from "react";
const sendMagicLinkMutation = graphql`
mutation MagicLinkDialogMutation($input: SendMagicLinkInput!) {
sendMagicLink(input: $input) {
success
}
}
`;
const schema = z.object({
email: z.string().email(),
});
type FormData = z.infer<typeof schema>;
export function MagicLinkDialog(props: PropsWithChildren) {
const { children } = props;
const { __ } = useTranslate();
const { toast } = useToast();
const dialogRef = useDialogRef();
const [sendMagicLink] = useMutation<MagicLinkDialogMutation>(
sendMagicLinkMutation,
);
const {
handleSubmit: handleSubmitWrapper,
register,
formState,
} = useFormWithSchema(schema, {
defaultValues: {
email: "",
},
});
const handleSubmit = ({ email }: FormData) => {
sendMagicLink({
variables: {
input: {
email,
},
},
onCompleted: (_, errors) => {
if (errors?.length) {
toast({
title: __("Error"),
description: __("Cannot send magic link"),
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __(
"Magic link sent! Please check your emails to authenticate",
),
variant: "success",
});
dialogRef.current?.close();
},
onError: (error) => {
toast({
title: __("Error"),
description: error.message,
variant: "error",
});
},
});
};
return (
<Dialog
trigger={children}
ref={dialogRef}
className="max-w-[500px] text-txt-primary"
>
<form onSubmit={handleSubmitWrapper(handleSubmit)}>
<DialogTitle className="text-2xl font-semibold mb-4 pt-4 md:pt-8 px-4 md:px-8">
{__("Create account or sign in")}
</DialogTitle>
<DialogContent className="px-4 md:px-8 pb-4 md:pb-8 text-txt-primary">
<p className="text-txt-secondary mb-4">
{__(
"To be able to request access to documents, you need to authenticate. Please enter your email to get a magic link.",
)}
</p>
<Field
label={__("Email")}
placeholder="john.doe@acme.com"
{...register("email")}
type="email"
error={formState.errors.email?.message}
/>
</DialogContent>
<DialogFooter>
<Button disabled={formState.isSubmitting} type="submit">
{__("Continue")}
</Button>
</DialogFooter>
</form>
</Dialog>
);
}

View File

@@ -1,11 +1,31 @@
import { useTranslate } from "@probo/i18n";
import { Button, Card, IconBlock, IconLock, IconMedal } from "@probo/ui";
import {
Button,
Card,
IconBlock,
IconLock,
IconMedal,
useToast,
} from "@probo/ui";
import type { TrustGraphQuery$data } from "/queries/__generated__/TrustGraphQuery.graphql";
import { use, type PropsWithChildren } from "react";
import { domain } from "@probo/helpers";
import { domain, formatError } from "@probo/helpers";
import { AuditRowAvatar } from "./AuditRow";
import { RequestAccessDialog } from "./RequestAccessDialog";
import { Viewer } from "/providers/Viewer";
import { MagicLinkDialog } from "./MagicLinkDialog";
import { graphql } from "relay-runtime";
import { useMutation } from "react-relay";
import type { OrganizationSidebar_requestAllAccessesMutation } from "./__generated__/OrganizationSidebar_requestAllAccessesMutation.graphql";
const requestAllAccessesMutation = graphql`
mutation OrganizationSidebar_requestAllAccessesMutation {
requestAllAccesses {
trustCenterAccess {
id
}
}
}
`;
export function OrganizationSidebar({
trustCenter,
@@ -14,6 +34,40 @@ export function OrganizationSidebar({
}) {
const { __ } = useTranslate();
const isAuthenticated = !!use(Viewer);
const { toast } = useToast();
const [requestAllAccesses, isRequestingAccess] =
useMutation<OrganizationSidebar_requestAllAccessesMutation>(
requestAllAccessesMutation,
);
const handleRequestAllAccesses = () => {
requestAllAccesses({
variables: {},
onCompleted: (_, errors) => {
if (errors?.length) {
toast({
title: __("Error"),
description: formatError(__("Cannot request access"), errors),
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __("Access request submitted successfully."),
variant: "success",
});
},
onError: (error) => {
toast({
title: __("Error"),
description: error.message ?? __("Cannot request access"),
variant: "error",
});
},
});
};
if (!trustCenter) {
return null;
@@ -95,12 +149,22 @@ export function OrganizationSidebar({
)}
{/* Actions */}
{!isAuthenticated && (
<RequestAccessDialog>
{isAuthenticated ? (
<Button
disabled={isRequestingAccess}
variant="primary"
icon={IconLock}
className="w-full h-10"
onClick={handleRequestAllAccesses}
>
{__("Request access")}
</Button>
) : (
<MagicLinkDialog>
<Button variant="primary" icon={IconLock} className="w-full h-10">
{__("Request access")}
</Button>
</RequestAccessDialog>
</MagicLinkDialog>
)}
{/* <Button variant="secondary" icon={IconMail} className="w-full h-10">
{__("Subscribe to updates")}

View File

@@ -1,273 +0,0 @@
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogTitle,
Field,
useDialogRef,
useToast,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { sprintf } from "@probo/helpers";
import { z } from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { graphql } from "relay-runtime";
import { useMutationWithToasts } from "/hooks/useMutationWithToast";
import { useTrustCenter } from "/hooks/useTrustCenter";
import { use, type FormEventHandler, type PropsWithChildren } from "react";
import { InvalidError } from "/providers/RelayProviders";
import { Viewer } from "/providers/Viewer";
type Props = PropsWithChildren<{
documentId?: string;
reportId?: string;
trustCenterFileId?: string;
onSuccess?: () => void;
}>;
const schema = z.object({
fullName: z.string(),
email: z.string().email(),
});
export function RequestAccessDialog({
children,
documentId,
reportId,
trustCenterFileId,
onSuccess,
}: Props) {
const trustCenter = useTrustCenter();
const { toast } = useToast();
const { __ } = useTranslate();
const viewer = use(Viewer);
const { handleSubmit, register, setError, formState } = useFormWithSchema(
schema,
{
defaultValues: {
fullName: "",
email: "",
},
},
);
const dialogRef = useDialogRef();
const [commitMutation, isMutating] = useMutation({
documentId,
reportId,
trustCenterFileId,
});
const submitCallback = (data: z.infer<typeof schema> | null) => {
commitMutation({
email: data?.email ?? viewer?.email ?? "",
fullName: data?.fullName ?? viewer?.fullName ?? "",
})
.then(() => {
onSuccess?.();
toast({
title: __("Success"),
description: __("Access request submitted successfully"),
variant: "success",
});
dialogRef.current?.close();
})
.catch((error) => {
if (error instanceof InvalidError) {
if (error.field === "email") {
setError(error.field, { message: error.message });
}
return;
}
toast({
title: __("Error"),
description: __("Cannot request access"),
variant: "error",
});
});
};
const onSubmit: FormEventHandler<HTMLFormElement> = viewer
? (e) => {
e.preventDefault();
submitCallback(null);
}
: handleSubmit(submitCallback);
return (
<Dialog
ref={dialogRef}
trigger={children}
className="max-w-[500px] text-txt-primary"
>
<form onSubmit={onSubmit}>
<DialogTitle className="text-2xl font-semibold mb-4 pt-4 md:pt-8 px-4 md:px-8">
{__("Request access to documentation")}
</DialogTitle>
<DialogContent className="px-4 md:px-8 pb-4 md:pb-8 text-txt-primary">
<p className="text-txt-secondary mb-4">
{sprintf(
__(
"Request access to %s's Trust Center. Your request will be reviewed and you will receive an email notification with access instructions if approved.",
),
trustCenter.organization.name,
)}
</p>
{!viewer && (
<div className="space-y-4">
<Field
label={__("Full name")}
placeholder="John Doe"
{...register("fullName")}
type="text"
/>
<Field
label={__("Email")}
placeholder="john.doe@acme.com"
{...register("email")}
type="email"
error={formState.errors.email?.message}
/>
</div>
)}
</DialogContent>
<DialogFooter>
<Button disabled={isMutating} type="submit">
{__("Continue")}
</Button>
</DialogFooter>
</form>
</Dialog>
);
}
const requestAccessMutation = graphql`
mutation RequestAccessDialogMutation($input: RequestAllAccessesInput!) {
requestAllAccesses(input: $input) {
trustCenterAccess {
id
}
}
}
`;
const requestDocumentAccessMutation = graphql`
mutation RequestAccessDialogDocumentMutation(
$input: RequestDocumentAccessInput!
) {
requestDocumentAccess(input: $input) {
trustCenterAccess {
id
}
}
}
`;
const requestReportAccessMutation = graphql`
mutation RequestAccessDialogReportMutation(
$input: RequestReportAccessInput!
) {
requestReportAccess(input: $input) {
trustCenterAccess {
id
}
}
}
`;
const requestTrustCenterFileAccessMutation = graphql`
mutation RequestAccessDialogTrustCenterFileMutation(
$input: RequestTrustCenterFileAccessInput!
) {
requestTrustCenterFileAccess(input: $input) {
trustCenterAccess {
id
}
}
}
`;
/**
* Use the correct mutation using the shape
*/
function useMutation({
documentId,
reportId,
trustCenterFileId,
}: Pick<Props, "documentId" | "reportId" | "trustCenterFileId">): [
(data: z.infer<typeof schema> | null) => Promise<unknown>,
boolean,
] {
const trustCenter = useTrustCenter();
const [commitRequestAccess, isRequestingAccess] = useMutationWithToasts(
requestAccessMutation,
);
const [commitRequestDocumentAccess, isRequestingDocumentAccess] =
useMutationWithToasts(requestDocumentAccessMutation);
const [commitRequestReportAccess, isRequestingReportAccess] =
useMutationWithToasts(requestReportAccessMutation);
const [
commitRequestTrustCenterFileAccess,
isRequestingTrustCenterFileAccess,
] = useMutationWithToasts(requestTrustCenterFileAccessMutation);
if (trustCenterFileId) {
return [
(data) => {
return commitRequestTrustCenterFileAccess({
variables: {
input: {
trustCenterId: trustCenter.id,
trustCenterFileId: trustCenterFileId,
...data,
},
},
});
},
isRequestingTrustCenterFileAccess,
];
} else if (reportId) {
return [
(data) => {
return commitRequestReportAccess({
variables: {
input: {
trustCenterId: trustCenter.id,
reportId: reportId,
...data,
},
},
});
},
isRequestingReportAccess,
];
} else if (documentId) {
return [
(data) => {
return commitRequestDocumentAccess({
variables: {
input: {
trustCenterId: trustCenter.id,
documentId: documentId,
...data,
},
},
});
},
isRequestingDocumentAccess,
];
} else {
return [
(data) => {
return commitRequestAccess({
variables: {
input: {
trustCenterId: trustCenter.id,
...data,
},
},
});
},
isRequestingAccess,
];
}
}

View File

@@ -1,22 +1,39 @@
import { graphql } from "relay-runtime";
import type { TrustCenterFileRowFragment$key } from "./__generated__/TrustCenterFileRowFragment.graphql";
import { useFragment } from "react-relay";
import { useFragment, useMutation } from "react-relay";
import {
Button,
IconArrowInbox,
IconLock,
IconPageTextLine,
Spinner,
useToast,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import type { TrustCenterFileRowDownloadMutation } from "./__generated__/TrustCenterFileRowDownloadMutation.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToast";
import { downloadFile } from "@probo/helpers";
import { RequestAccessDialog } from "/components/RequestAccessDialog.tsx";
import { useState } from "react";
import { downloadFile, formatError } from "@probo/helpers";
import { use, useState } from "react";
import { Viewer } from "/providers/Viewer";
import { MagicLinkDialog } from "./MagicLinkDialog";
import type { TrustCenterFileRow_requestAccessMutation } from "./__generated__/TrustCenterFileRow_requestAccessMutation.graphql";
const requestAccessMutation = graphql`
mutation TrustCenterFileRow_requestAccessMutation(
$input: RequestTrustCenterFileAccessInput!
) {
requestTrustCenterFileAccess(input: $input) {
trustCenterAccess {
id
}
}
}
`;
const downloadMutation = graphql`
mutation TrustCenterFileRowDownloadMutation($input: ExportTrustCenterFileInput!) {
mutation TrustCenterFileRowDownloadMutation(
$input: ExportTrustCenterFileInput!
) {
exportTrustCenterFile(input: $input) {
data
}
@@ -32,11 +49,56 @@ const trustCenterFileRowFragment = graphql`
}
`;
export function TrustCenterFileRow(props: { file: TrustCenterFileRowFragment$key }) {
const file = useFragment(trustCenterFileRowFragment, props.file);
export function TrustCenterFileRow(props: {
file: TrustCenterFileRowFragment$key;
}) {
const { __ } = useTranslate();
const viewer = use(Viewer);
const { toast } = useToast();
const file = useFragment(trustCenterFileRowFragment, props.file);
const [hasRequested, setHasRequested] = useState(file.hasUserRequestedAccess);
const [requestAccess, isRequestingAccess] =
useMutation<TrustCenterFileRow_requestAccessMutation>(
requestAccessMutation,
);
const [commitDownload, downloading] =
useMutationWithToasts<TrustCenterFileRowDownloadMutation>(downloadMutation);
const handleRequestAccess = () => {
requestAccess({
variables: {
input: {
trustCenterFileId: file.id,
},
},
onCompleted: (_, errors) => {
if (errors?.length) {
toast({
title: __("Error"),
description: formatError(__("Cannot request access"), errors),
variant: "error",
});
return;
}
setHasRequested(true);
toast({
title: __("Success"),
description: __("Access request submitted successfully."),
variant: "success",
});
},
onError: (error) => {
toast({
title: __("Error"),
description: error.message ?? __("Cannot request access"),
variant: "error",
});
},
});
};
const handleDownload = () => {
commitDownload({
variables: {
@@ -49,40 +111,46 @@ export function TrustCenterFileRow(props: { file: TrustCenterFileRowFragment$key
},
});
};
const [hasRequested, setHasRequested] = useState(
file.hasUserRequestedAccess,
);
return (
<div className="text-sm border-1 border-border-solid -mt-[1px] flex gap-3 flex-col md:flex-row md:justify-between px-6 py-3">
<div className="text-sm border border-border-solid -mt-px flex gap-3 flex-col md:flex-row md:justify-between px-6 py-3">
<div className="flex items-center gap-2">
<IconPageTextLine size={16} className=" flex-none text-txt-tertiary" />
{file.name}
</div>
{file.isUserAuthorized ? (
<Button
className="w-full md:w-max"
variant="secondary"
disabled={downloading}
icon={downloading ? Spinner : IconArrowInbox}
onClick={handleDownload}
>
{__("Download")}
</Button>
) : (
<RequestAccessDialog
trustCenterFileId={file.id}
onSuccess={() => setHasRequested(true)}
>
{!viewer && (
<MagicLinkDialog>
<Button
disabled={hasRequested}
className="w-full md:w-max"
variant="secondary"
icon={IconLock}
>
{hasRequested ? __("Access requested") : __("Request access")}
</Button>
</RequestAccessDialog>
</MagicLinkDialog>
)}
{viewer &&
(file.isUserAuthorized ? (
<Button
className="w-full md:w-max"
variant="secondary"
disabled={downloading}
icon={downloading ? Spinner : IconArrowInbox}
onClick={handleDownload}
>
{__("Download")}
</Button>
) : (
<Button
disabled={hasRequested || isRequestingAccess}
className="w-full md:w-max"
variant="secondary"
icon={IconLock}
onClick={handleRequestAccess}
>
{hasRequested ? __("Access requested") : __("Request access")}
</Button>
))}
</div>
);
}

View File

@@ -0,0 +1,105 @@
/**
* @generated SignedSource<<0e5c234b565a3031a774995bff249b97>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type RequestReportAccessInput = {
reportId: string;
};
export type AuditRow_requestAccessMutation$variables = {
input: RequestReportAccessInput;
};
export type AuditRow_requestAccessMutation$data = {
readonly requestReportAccess: {
readonly trustCenterAccess: {
readonly id: string;
};
};
};
export type AuditRow_requestAccessMutation = {
response: AuditRow_requestAccessMutation$data;
variables: AuditRow_requestAccessMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "RequestAccessesPayload",
"kind": "LinkedField",
"name": "requestReportAccess",
"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
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "AuditRow_requestAccessMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "AuditRow_requestAccessMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "6760f1ec8a6990f62461c3fab2b2934a",
"id": null,
"metadata": {},
"name": "AuditRow_requestAccessMutation",
"operationKind": "mutation",
"text": "mutation AuditRow_requestAccessMutation(\n $input: RequestReportAccessInput!\n) {\n requestReportAccess(input: $input) {\n trustCenterAccess {\n id\n }\n }\n}\n"
}
};
})();
(node as any).hash = "6ab5eb701126385e717a381e21e55ae0";
export default node;

View File

@@ -0,0 +1,105 @@
/**
* @generated SignedSource<<e62c28d53365ca5d5f3d53282aefd4e7>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type RequestDocumentAccessInput = {
documentId: string;
};
export type DocumentRow_requestAccessMutation$variables = {
input: RequestDocumentAccessInput;
};
export type DocumentRow_requestAccessMutation$data = {
readonly requestDocumentAccess: {
readonly trustCenterAccess: {
readonly id: string;
};
};
};
export type DocumentRow_requestAccessMutation = {
response: DocumentRow_requestAccessMutation$data;
variables: DocumentRow_requestAccessMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "RequestAccessesPayload",
"kind": "LinkedField",
"name": "requestDocumentAccess",
"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
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "DocumentRow_requestAccessMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "DocumentRow_requestAccessMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "13715b7cb8784d050b6d0381e460bbad",
"id": null,
"metadata": {},
"name": "DocumentRow_requestAccessMutation",
"operationKind": "mutation",
"text": "mutation DocumentRow_requestAccessMutation(\n $input: RequestDocumentAccessInput!\n) {\n requestDocumentAccess(input: $input) {\n trustCenterAccess {\n id\n }\n }\n}\n"
}
};
})();
(node as any).hash = "ca9026b04c2de91067759e3a80e5c276";
export default node;

View File

@@ -0,0 +1,92 @@
/**
* @generated SignedSource<<1d6509148442dad28e2af20068216f49>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type SendMagicLinkInput = {
email: any;
};
export type MagicLinkDialogMutation$variables = {
input: SendMagicLinkInput;
};
export type MagicLinkDialogMutation$data = {
readonly sendMagicLink: {
readonly success: boolean;
} | null | undefined;
};
export type MagicLinkDialogMutation = {
response: MagicLinkDialogMutation$data;
variables: MagicLinkDialogMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "SendMagicLinkPayload",
"kind": "LinkedField",
"name": "sendMagicLink",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "success",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "MagicLinkDialogMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "MagicLinkDialogMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "d0e02db0cec956d7a21f5ad5a5a69d61",
"id": null,
"metadata": {},
"name": "MagicLinkDialogMutation",
"operationKind": "mutation",
"text": "mutation MagicLinkDialogMutation(\n $input: SendMagicLinkInput!\n) {\n sendMagicLink(input: $input) {\n success\n }\n}\n"
}
};
})();
(node as any).hash = "c1ee1fe3ef7c232fcb43b5cf8e56c2de";
export default node;

View File

@@ -0,0 +1,87 @@
/**
* @generated SignedSource<<e1d14b17505ed1107ac96553e3d8f922>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type OrganizationSidebar_requestAllAccessesMutation$variables = Record<PropertyKey, never>;
export type OrganizationSidebar_requestAllAccessesMutation$data = {
readonly requestAllAccesses: {
readonly trustCenterAccess: {
readonly id: string;
};
};
};
export type OrganizationSidebar_requestAllAccessesMutation = {
response: OrganizationSidebar_requestAllAccessesMutation$data;
variables: OrganizationSidebar_requestAllAccessesMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"alias": null,
"args": null,
"concreteType": "RequestAccessesPayload",
"kind": "LinkedField",
"name": "requestAllAccesses",
"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
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "OrganizationSidebar_requestAllAccessesMutation",
"selections": (v0/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [],
"kind": "Operation",
"name": "OrganizationSidebar_requestAllAccessesMutation",
"selections": (v0/*: any*/)
},
"params": {
"cacheID": "a298b2a1c2f62300ae5056743d2c2ec3",
"id": null,
"metadata": {},
"name": "OrganizationSidebar_requestAllAccessesMutation",
"operationKind": "mutation",
"text": "mutation OrganizationSidebar_requestAllAccessesMutation {\n requestAllAccesses {\n trustCenterAccess {\n id\n }\n }\n}\n"
}
};
})();
(node as any).hash = "a22225757510c4dd097e99dcd3c066a6";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<9f01a480ccc5fa00e310296e9aed1020>>
* @generated SignedSource<<2ea33498f7101d861d940b9c52ed3283>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -11,8 +11,6 @@
import { ConcreteRequest } from 'relay-runtime';
export type RequestDocumentAccessInput = {
documentId: string;
email: any;
fullName: string;
};
export type RequestAccessDialogDocumentMutation$variables = {
input: RequestDocumentAccessInput;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<0777fa7e6e3d0c75eedfc8380d9965a4>>
* @generated SignedSource<<b06c18474ea9953c2fb496b919b8e247>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,13 +9,7 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type RequestAllAccessesInput = {
email: any;
fullName: string;
};
export type RequestAccessDialogMutation$variables = {
input: RequestAllAccessesInput;
};
export type RequestAccessDialogMutation$variables = Record<PropertyKey, never>;
export type RequestAccessDialogMutation$data = {
readonly requestAllAccesses: {
readonly trustCenterAccess: {
@@ -30,22 +24,9 @@ export type RequestAccessDialogMutation = {
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"args": null,
"concreteType": "RequestAccessesPayload",
"kind": "LinkedField",
"name": "requestAllAccesses",
@@ -75,32 +56,32 @@ v1 = [
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "RequestAccessDialogMutation",
"selections": (v1/*: any*/),
"selections": (v0/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"argumentDefinitions": [],
"kind": "Operation",
"name": "RequestAccessDialogMutation",
"selections": (v1/*: any*/)
"selections": (v0/*: any*/)
},
"params": {
"cacheID": "99eb2902ce5a921515d68db30f8a2189",
"cacheID": "b48847e88c57289efc120745c743c8f6",
"id": null,
"metadata": {},
"name": "RequestAccessDialogMutation",
"operationKind": "mutation",
"text": "mutation RequestAccessDialogMutation(\n $input: RequestAllAccessesInput!\n) {\n requestAllAccesses(input: $input) {\n trustCenterAccess {\n id\n }\n }\n}\n"
"text": "mutation RequestAccessDialogMutation {\n requestAllAccesses {\n trustCenterAccess {\n id\n }\n }\n}\n"
}
};
})();
(node as any).hash = "cc4d5c8753438eff23833b4182dd485d";
(node as any).hash = "44c3538831968b7146fbf2c9e027a778";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<6fde198cd3eaa998d665639c48f5e630>>
* @generated SignedSource<<3e5433cc33d54bff9511d01eb4c65b00>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -10,8 +10,6 @@
import { ConcreteRequest } from 'relay-runtime';
export type RequestReportAccessInput = {
email: any;
fullName: string;
reportId: string;
};
export type RequestAccessDialogReportMutation$variables = {

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<7c329901bcf9bbf05b1899356016b151>>
* @generated SignedSource<<447bd664ebefbd6a672ccde86c63c028>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -10,8 +10,6 @@
import { ConcreteRequest } from 'relay-runtime';
export type RequestTrustCenterFileAccessInput = {
email: any;
fullName: string;
trustCenterFileId: string;
};
export type RequestAccessDialogTrustCenterFileMutation$variables = {

View File

@@ -0,0 +1,105 @@
/**
* @generated SignedSource<<a8da5aa24d8696b55231f44d861f347c>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type RequestTrustCenterFileAccessInput = {
trustCenterFileId: string;
};
export type TrustCenterFileRow_requestAccessMutation$variables = {
input: RequestTrustCenterFileAccessInput;
};
export type TrustCenterFileRow_requestAccessMutation$data = {
readonly requestTrustCenterFileAccess: {
readonly trustCenterAccess: {
readonly id: string;
};
};
};
export type TrustCenterFileRow_requestAccessMutation = {
response: TrustCenterFileRow_requestAccessMutation$data;
variables: TrustCenterFileRow_requestAccessMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "RequestAccessesPayload",
"kind": "LinkedField",
"name": "requestTrustCenterFileAccess",
"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
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "TrustCenterFileRow_requestAccessMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "TrustCenterFileRow_requestAccessMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "77a09810aa9c3ddf3a3baddf709f9a42",
"id": null,
"metadata": {},
"name": "TrustCenterFileRow_requestAccessMutation",
"operationKind": "mutation",
"text": "mutation TrustCenterFileRow_requestAccessMutation(\n $input: RequestTrustCenterFileAccessInput!\n) {\n requestTrustCenterFileAccess(input: $input) {\n trustCenterAccess {\n id\n }\n }\n}\n"
}
};
})();
(node as any).hash = "eba04489de9ec18ab9816328a481ef33";
export default node;

View File

@@ -1,76 +0,0 @@
import { useTranslate } from "@probo/i18n";
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router";
import { buildEndpoint } from "/providers/RelayProviders";
import { PageError } from "/components/PageError";
import { Spinner } from "@probo/ui";
/**
* Page requested with an access token to authenticate the user for the Trust center
*/
export function AccessPage() {
const { __ } = useTranslate();
const [searchParams] = useSearchParams();
const token = searchParams.get("token");
const navigate = useNavigate();
const isValidRequest = !!token;
const [error, setError] = useState<string | null>(() => {
if (!token) {
return __("Invalid access token");
}
return null;
});
// Initiate an authentication attempt
useEffect(() => {
if (!isValidRequest) {
return;
}
fetch(buildEndpoint("/api/trust/v1/auth/authenticate"), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
token,
}),
})
.then((response) => {
// For invalid response throw an error
if (!response.ok) {
const defaultMessage = `HTTP ${response.status}: ${response.statusText}`;
return response
.json()
.then((json) => {
throw new Error(json.message ?? defaultMessage);
})
.catch(() => {
throw new Error(defaultMessage);
});
}
return response.json();
})
.then((data) => {
if (data.success) {
navigate("/overview");
return;
}
throw new Error(data.message ?? __("Authentication failed"));
})
.catch((error) => {
setError(error.message);
});
}, [isValidRequest, token, __, navigate]);
if (error) {
return <PageError error={error} />;
}
return (
<div className="p-4 text-center flex items-center justify-center gap-2">
<Spinner size={16} />
{__("Redirecting to trust center")}
</div>
);
}

View File

@@ -0,0 +1,19 @@
import { Logo } from "@probo/ui";
import { Outlet } from "react-router";
export default function () {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 min-h-screen text-txt-primary">
<div className="bg-level-0 flex flex-col items-center justify-center">
<div className="w-full max-w-md px-6">
<Outlet />
</div>
</div>
<div className="hidden lg:flex bg-dialog font-bold flex-col items-center justify-center p-8 text-txt-primary lg:p-10">
<div className="flex flex-col items-center justify-center gap-4">
<Logo withPicto className="w-[440px]" />
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,119 @@
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button, Field, useToast } from "@probo/ui";
import { useSearchParams } from "react-router";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import z from "zod";
import { useEffect, useRef } from "react";
import { graphql } from "relay-runtime";
import { useMutation } from "react-relay";
import { formatError } from "@probo/helpers";
import type { VerifyMagicLinkPageMutation } from "./__generated__/VerifyMagicLinkPageMutation.graphql";
import { getPathPrefix } from "/utils/pathPrefix";
const verifyMagicLinkMutation = graphql`
mutation VerifyMagicLinkPageMutation($input: VerifyMagicLinkInput!) {
verifyMagicLink(input: $input) {
success
}
}
`;
const verifyMagicLinkSchema = z.object({
token: z.string().min(1, "Please enter a magic token"),
});
export default function VerifyMagicLinkPagePageMutation() {
const { __ } = useTranslate();
const { toast } = useToast();
const [searchParams] = useSearchParams();
const submittedRef = useRef<boolean>(false);
usePageTitle(__("Verify Magic Link"));
const form = useFormWithSchema(verifyMagicLinkSchema, {
defaultValues: {
token: searchParams.get("token") ?? "",
},
});
const [verifyMagicLink] = useMutation<VerifyMagicLinkPageMutation>(
verifyMagicLinkMutation,
);
const handleSubmit = form.handleSubmit(async (data) => {
verifyMagicLink({
variables: {
input: {
token: data.token.trim(),
},
},
onCompleted: (_, errors) => {
if (errors) {
toast({
title: __("Error"),
description: formatError(__("Failed to connect"), errors),
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __("Your have successfully signed in"),
variant: "success",
});
window.location.href = getPathPrefix();
},
onError: (err) => {
toast({
title: __("Error"),
description: err.message,
variant: "error",
});
},
});
});
useEffect(() => {
if (!submittedRef.current && searchParams.get("token")) {
handleSubmit();
submittedRef.current = true;
}
});
return (
<div className="space-y-6 w-full max-w-md mx-auto">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">{__("Email Confirmation")}</h1>
<p className="text-txt-tertiary">
{__("Confirm your email address to complete registration")}
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<Field
label={__("Confirmation Token")}
type="text"
placeholder={__("Enter your confirmation token")}
{...form.register("token")}
error={form.formState.errors.token?.message}
disabled={form.formState.isSubmitting}
help={__(
"The token has been automatically filled from the URL if available",
)}
/>
<Button
type="submit"
className="w-full"
disabled={form.formState.isSubmitting}
>
{form.formState.isSubmitting
? __("Confirming...")
: __("Confirm Email")}
</Button>
</form>
</div>
);
}

View File

@@ -0,0 +1,92 @@
/**
* @generated SignedSource<<5037d9722f3c6cc15c2a323ef954e3d6>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type VerifyMagicLinkInput = {
token: string;
};
export type VerifyMagicLinkPageMutation$variables = {
input: VerifyMagicLinkInput;
};
export type VerifyMagicLinkPageMutation$data = {
readonly verifyMagicLink: {
readonly success: boolean;
} | null | undefined;
};
export type VerifyMagicLinkPageMutation = {
response: VerifyMagicLinkPageMutation$data;
variables: VerifyMagicLinkPageMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "VerifyMagicLinkPayload",
"kind": "LinkedField",
"name": "verifyMagicLink",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "success",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "VerifyMagicLinkPageMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "VerifyMagicLinkPageMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "07cf89de3f37725d847cda46557467f5",
"id": null,
"metadata": {},
"name": "VerifyMagicLinkPageMutation",
"operationKind": "mutation",
"text": "mutation VerifyMagicLinkPageMutation(\n $input: VerifyMagicLinkInput!\n) {\n verifyMagicLink(input: $input) {\n success\n }\n}\n"
}
};
})();
(node as any).hash = "074415601c4d50f50d177c06dfab64ef";
export default node;

View File

@@ -8,6 +8,7 @@ import {
import { GraphQLError } from "graphql";
import type { PropsWithChildren } from "react";
import { RelayEnvironmentProvider } from "react-relay";
import { getPathPrefix } from "/utils/pathPrefix";
export class UnAuthenticatedError extends Error {
constructor() {
@@ -35,11 +36,11 @@ export class InternalServerError extends Error {
}
}
export function buildEndpoint(path: string): string {
const host = import.meta.env.VITE_API_URL;
export function buildEndpoint(): string {
let host = import.meta.env.VITE_API_URL;
if (!host) {
return path;
host = window.location.origin;
}
const formattedHost =
@@ -49,10 +50,16 @@ export function buildEndpoint(path: string): string {
const url = new URL(formattedHost);
if (path) {
url.pathname = path.startsWith("/") ? path : `/${path}`;
const prefix = getPathPrefix();
let path: string;
if (prefix) {
path = `${prefix}/api/trust/v1/graphql`;
} else {
path = `/api/trust/v1/graphql`;
}
url.pathname = path;
return url.toString();
}
@@ -66,7 +73,7 @@ const fetchRelay: FetchFunction = async (
request,
variables,
_,
uploadables
uploadables,
) => {
const requestInit: RequestInit = {
method: "POST",
@@ -82,7 +89,7 @@ const fetchRelay: FetchFunction = async (
operationName: request.name,
query: request.text,
variables: variables,
})
}),
);
const uploadableMap: {
@@ -119,13 +126,7 @@ const fetchRelay: FetchFunction = async (
});
}
// Use relative API path to ensure it goes through the same routing context
// For /trust/slug/overview, this resolves to /trust/slug/api/trust/v1/graphql
// For custom domains at /overview, this resolves to /api/trust/v1/graphql
const response = await fetch(
buildEndpoint("./api/trust/v1/graphql"),
requestInit
);
const response = await fetch(buildEndpoint(), requestInit);
if (response.status === 500) {
throw new InternalServerError();
@@ -144,8 +145,8 @@ const fetchRelay: FetchFunction = async (
if (invalidError) {
throw new InvalidError(
invalidError.message,
invalidError.extensions.field as string ?? "",
invalidError.extensions.cause as string ?? "",
(invalidError.extensions.field as string) ?? "",
(invalidError.extensions.cause as string) ?? "",
);
}

View File

@@ -1,14 +1,6 @@
import {
createBrowserRouter,
Navigate,
redirect,
useRouteError,
} from "react-router";
import { Fragment } from "react";
import {
consoleEnvironment,
UnAuthenticatedError,
} from "./providers/RelayProviders.tsx";
import { createBrowserRouter, redirect, useRouteError } from "react-router";
import { Fragment, lazy } from "react";
import { consoleEnvironment } from "./providers/RelayProviders.tsx";
import { loadQuery } from "react-relay";
import { PageError } from "./components/PageError.tsx";
import { MainLayout } from "/layouts/MainLayout";
@@ -20,7 +12,6 @@ import {
import { OverviewPage } from "/pages/OverviewPage";
import { DocumentsPage } from "/pages/DocumentsPage";
import { SubprocessorsPage } from "/pages/SubprocessorsPage";
import { AccessPage } from "./pages/AccessPage.tsx";
import { TabSkeleton } from "./components/Skeletons/TabSkeleton.tsx";
import { MainSkeleton } from "./components/Skeletons/MainSkeleton.tsx";
import {
@@ -36,14 +27,20 @@ import {
function ErrorBoundary({ error: propsError }: { error?: string }) {
const error = useRouteError() ?? propsError;
if (error instanceof UnAuthenticatedError) {
return <Navigate to="/auth/login" />;
}
return <PageError error={error?.toString()} />;
}
const routes = [
{
path: "/auth",
Component: lazy(() => import("./pages/auth/AuthLayout")),
children: [
{
path: "verify-magic-link",
Component: lazy(() => import("./pages/auth/VerifyMagicLinkPage.tsx")),
},
],
},
{
path: "/",
loader: async () => {
@@ -107,11 +104,6 @@ const routes = [
},
],
},
{
path: "/access",
Component: AccessPage,
ErrorBoundary: ErrorBoundary,
},
// Fallback URL to the NotFound Page
{
path: "*",

View File

@@ -0,0 +1,15 @@
import { matchPath } from "react-router";
export function getPathPrefix() {
const match = matchPath(
{ path: "/trust/:id", caseSensitive: false, end: false },
window.location.pathname,
);
let prefix = "";
if (match) {
prefix = `/trust/${match.params.id}`;
}
return prefix;
}

View File

@@ -37,6 +37,7 @@ export default defineConfig({
"/pages": fileURLToPath(new URL("./src/pages", import.meta.url)),
"/routes": fileURLToPath(new URL("./src/routes", import.meta.url)),
"/providers": fileURLToPath(new URL("./src/providers", import.meta.url)),
"/utils": fileURLToPath(new URL("./src/utils", import.meta.url)),
},
},
});