Plug trust center part 1

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-01-09 19:14:17 +01:00
committed by Bryan Frimin
parent 663400c4f6
commit 7322201dab
41 changed files with 1959 additions and 1033 deletions

View File

@@ -1,11 +1,11 @@
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { Button, Card, IconBlock, IconLock, IconMedal } from "@probo/ui"; import { Button, Card, IconBlock, IconLock, IconMedal } from "@probo/ui";
import type { TrustGraphQuery$data } from "/queries/__generated__/TrustGraphQuery.graphql"; import type { TrustGraphQuery$data } from "/queries/__generated__/TrustGraphQuery.graphql";
import type { PropsWithChildren } from "react"; import { use, type PropsWithChildren } from "react";
import { domain } from "@probo/helpers"; import { domain } from "@probo/helpers";
import { AuditRowAvatar } from "./AuditRow"; import { AuditRowAvatar } from "./AuditRow";
import { RequestAccessDialog } from "./RequestAccessDialog"; import { RequestAccessDialog } from "./RequestAccessDialog";
import { useIsAuthenticated } from "/hooks/useIsAuthenticated"; import { Viewer } from "/providers/Viewer";
export function OrganizationSidebar({ export function OrganizationSidebar({
trustCenter, trustCenter,
@@ -13,7 +13,7 @@ export function OrganizationSidebar({
trustCenter: TrustGraphQuery$data["trustCenterBySlug"]; trustCenter: TrustGraphQuery$data["trustCenterBySlug"];
}) { }) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const isAuthenticated = useIsAuthenticated(); const isAuthenticated = !!use(Viewer);
if (!trustCenter) { if (!trustCenter) {
return null; return null;

View File

@@ -15,9 +15,9 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import { useMutationWithToasts } from "/hooks/useMutationWithToast"; import { useMutationWithToasts } from "/hooks/useMutationWithToast";
import { useTrustCenter } from "/hooks/useTrustCenter"; import { useTrustCenter } from "/hooks/useTrustCenter";
import { type FormEventHandler, type PropsWithChildren } from "react"; import { use, type FormEventHandler, type PropsWithChildren } from "react";
import { useIsAuthenticated } from "/hooks/useIsAuthenticated.ts";
import { InvalidError } from "/providers/RelayProviders"; import { InvalidError } from "/providers/RelayProviders";
import { Viewer } from "/providers/Viewer";
type Props = PropsWithChildren<{ type Props = PropsWithChildren<{
documentId?: string; documentId?: string;
@@ -27,7 +27,7 @@ type Props = PropsWithChildren<{
}>; }>;
const schema = z.object({ const schema = z.object({
name: z.string(), fullName: z.string(),
email: z.string().email(), email: z.string().email(),
}); });
@@ -41,18 +41,28 @@ export function RequestAccessDialog({
const trustCenter = useTrustCenter(); const trustCenter = useTrustCenter();
const { toast } = useToast(); const { toast } = useToast();
const { __ } = useTranslate(); const { __ } = useTranslate();
const { handleSubmit, register, setError, formState } = useFormWithSchema(schema, { const viewer = use(Viewer);
defaultValues: { const { handleSubmit, register, setError, formState } = useFormWithSchema(
name: "", schema,
email: "", {
defaultValues: {
fullName: "",
email: "",
},
}, },
}); );
const isAuthenticated = useIsAuthenticated();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const [commitMutation, isMutating] = useMutation({ documentId, reportId, trustCenterFileId }); const [commitMutation, isMutating] = useMutation({
documentId,
reportId,
trustCenterFileId,
});
const submitCallback = (data: z.infer<typeof schema> | null) => { const submitCallback = (data: z.infer<typeof schema> | null) => {
commitMutation(data) commitMutation({
email: data?.email ?? viewer?.email ?? "",
fullName: data?.fullName ?? viewer?.fullName ?? "",
})
.then(() => { .then(() => {
onSuccess?.(); onSuccess?.();
toast({ toast({
@@ -65,7 +75,7 @@ export function RequestAccessDialog({
.catch((error) => { .catch((error) => {
if (error instanceof InvalidError) { if (error instanceof InvalidError) {
if (error.field === "email") { if (error.field === "email") {
setError(error.field, {message: error.message}) setError(error.field, { message: error.message });
} }
return; return;
} }
@@ -77,7 +87,7 @@ export function RequestAccessDialog({
}); });
}; };
const onSubmit: FormEventHandler<HTMLFormElement> = isAuthenticated const onSubmit: FormEventHandler<HTMLFormElement> = viewer
? (e) => { ? (e) => {
e.preventDefault(); e.preventDefault();
submitCallback(null); submitCallback(null);
@@ -102,12 +112,12 @@ export function RequestAccessDialog({
trustCenter.organization.name, trustCenter.organization.name,
)} )}
</p> </p>
{!isAuthenticated && ( {!viewer && (
<div className="space-y-4"> <div className="space-y-4">
<Field <Field
label={__("Full name")} label={__("Full name")}
placeholder="John Doe" placeholder="John Doe"
{...register("name")} {...register("fullName")}
type="text" type="text"
/> />
<Field <Field
@@ -195,8 +205,10 @@ function useMutation({
useMutationWithToasts(requestDocumentAccessMutation); useMutationWithToasts(requestDocumentAccessMutation);
const [commitRequestReportAccess, isRequestingReportAccess] = const [commitRequestReportAccess, isRequestingReportAccess] =
useMutationWithToasts(requestReportAccessMutation); useMutationWithToasts(requestReportAccessMutation);
const [commitRequestTrustCenterFileAccess, isRequestingTrustCenterFileAccess] = const [
useMutationWithToasts(requestTrustCenterFileAccessMutation); commitRequestTrustCenterFileAccess,
isRequestingTrustCenterFileAccess,
] = useMutationWithToasts(requestTrustCenterFileAccessMutation);
if (trustCenterFileId) { if (trustCenterFileId) {
return [ return [

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<5344ac5d6a506485d45e399361b92299>> * @generated SignedSource<<5113feef2c7f7b13a2d6348202086916>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -11,8 +11,8 @@
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type RequestDocumentAccessInput = { export type RequestDocumentAccessInput = {
documentId: string; documentId: string;
email?: any | null | undefined; email: any;
name?: string | null | undefined; fullName: string;
trustCenterId: string; trustCenterId: string;
}; };
export type RequestAccessDialogDocumentMutation$variables = { export type RequestAccessDialogDocumentMutation$variables = {

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<19ada29ea9b8f557b4fcc39f57bc3aec>> * @generated SignedSource<<294b6a25a178cf3bd9269591ef61381f>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -10,8 +10,8 @@
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type RequestAllAccessesInput = { export type RequestAllAccessesInput = {
email?: any | null | undefined; email: any;
name?: string | null | undefined; fullName: string;
trustCenterId: string; trustCenterId: string;
}; };
export type RequestAccessDialogMutation$variables = { export type RequestAccessDialogMutation$variables = {

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<858effd78b88a123430edcf6fcb100ed>> * @generated SignedSource<<d3209530c326da3c7db6535cef428ded>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -10,8 +10,8 @@
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type RequestReportAccessInput = { export type RequestReportAccessInput = {
email?: any | null | undefined; email: any;
name?: string | null | undefined; fullName: string;
reportId: string; reportId: string;
trustCenterId: string; trustCenterId: string;
}; };

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<32242c5e0b4dc9c9368c0e2f4034d95e>> * @generated SignedSource<<1e744bd11b2d5dd4d982bf1ce8b2a721>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -10,8 +10,8 @@
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type RequestTrustCenterFileAccessInput = { export type RequestTrustCenterFileAccessInput = {
email?: any | null | undefined; email: any;
name?: string | null | undefined; fullName: string;
trustCenterFileId: string; trustCenterFileId: string;
trustCenterId: string; trustCenterId: string;
}; };

View File

@@ -1,6 +0,0 @@
import { useContext } from "react";
import { AuthContext } from "/providers/AuthProvider";
export function useIsAuthenticated(): boolean {
return useContext(AuthContext).isAuthenticated;
}

View File

@@ -6,8 +6,8 @@ import { useTranslate } from "@probo/i18n";
import { OrganizationSidebar } from "/components/OrganizationSidebar"; import { OrganizationSidebar } from "/components/OrganizationSidebar";
import { Outlet } from "react-router"; import { Outlet } from "react-router";
import { NDADialog } from "/components/NDADialog"; import { NDADialog } from "/components/NDADialog";
import { AuthProvider } from "/providers/AuthProvider";
import { TrustCenterProvider } from "/providers/TrustCenterProvider"; import { TrustCenterProvider } from "/providers/TrustCenterProvider";
import { Viewer } from "/providers/Viewer";
type Props = { type Props = {
queryRef: PreloadedQuery<TrustGraphCurrentQuery>; queryRef: PreloadedQuery<TrustGraphCurrentQuery>;
@@ -26,7 +26,7 @@ export function MainLayout(props: Props) {
!trustCenter.hasAcceptedNonDisclosureAgreement && !trustCenter.hasAcceptedNonDisclosureAgreement &&
trustCenter.ndaFileUrl; trustCenter.ndaFileUrl;
return ( return (
<AuthProvider isAuthenticated={trustCenter.isUserAuthenticated}> <Viewer value={data.viewer}>
<TrustCenterProvider trustCenter={trustCenter}> <TrustCenterProvider trustCenter={trustCenter}>
{showNDADialog && ( {showNDADialog && (
<NDADialog <NDADialog
@@ -41,12 +41,8 @@ export function MainLayout(props: Props) {
<main> <main>
<Tabs className="mb-8"> <Tabs className="mb-8">
<TabLink to="/overview">{__("Overview")}</TabLink> <TabLink to="/overview">{__("Overview")}</TabLink>
<TabLink to="/documents"> <TabLink to="/documents">{__("Documents")}</TabLink>
{__("Documents")} <TabLink to="/subprocessors">{__("Subprocessors")}</TabLink>
</TabLink>
<TabLink to="/subprocessors">
{__("Subprocessors")}
</TabLink>
</Tabs> </Tabs>
<Outlet context={{ trustCenter }} /> <Outlet context={{ trustCenter }} />
</main> </main>
@@ -59,6 +55,6 @@ export function MainLayout(props: Props) {
{__("Powered by")} <Logo withPicto className="h-6" /> {__("Powered by")} <Logo withPicto className="h-6" />
</a> </a>
</TrustCenterProvider> </TrustCenterProvider>
</AuthProvider> </Viewer>
); );
} }

View File

@@ -0,0 +1,11 @@
import { createContext } from "react";
interface ViewerContextValue {
fullName: string;
email: string;
}
export const Viewer = createContext<ViewerContextValue | undefined | null>({
email: "",
fullName: "",
});

View File

@@ -82,6 +82,10 @@ export const trustVendorsQuery = graphql`
// Queries for custom domain (subdomain) approach // Queries for custom domain (subdomain) approach
export const currentTrustGraphQuery = graphql` export const currentTrustGraphQuery = graphql`
query TrustGraphCurrentQuery { query TrustGraphCurrentQuery {
viewer {
email
fullName
}
currentTrustCenter { currentTrustCenter {
id id
slug slug

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<ec32dbbc5f596dc323b1b4ff8df21fef>> * @generated SignedSource<<1f5a4da3a77a893daebeceb249612a72>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -37,6 +37,10 @@ export type TrustGraphCurrentQuery$data = {
readonly slug: string; readonly slug: string;
readonly " $fragmentSpreads": FragmentRefs<"OverviewPageFragment">; readonly " $fragmentSpreads": FragmentRefs<"OverviewPageFragment">;
} | null | undefined; } | null | undefined;
readonly viewer: {
readonly email: any;
readonly fullName: string;
} | null | undefined;
}; };
export type TrustGraphCurrentQuery = { export type TrustGraphCurrentQuery = {
response: TrustGraphCurrentQuery$data; response: TrustGraphCurrentQuery$data;
@@ -48,115 +52,122 @@ var v0 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "id", "name": "email",
"storageKey": null "storageKey": null
}, },
v1 = { v1 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "slug", "name": "fullName",
"storageKey": null "storageKey": null
}, },
v2 = { v2 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "isUserAuthenticated", "name": "id",
"storageKey": null "storageKey": null
}, },
v3 = { v3 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "hasAcceptedNonDisclosureAgreement", "name": "slug",
"storageKey": null "storageKey": null
}, },
v4 = { v4 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "ndaFileName", "name": "isUserAuthenticated",
"storageKey": null "storageKey": null
}, },
v5 = { v5 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "ndaFileUrl", "name": "hasAcceptedNonDisclosureAgreement",
"storageKey": null "storageKey": null
}, },
v6 = { v6 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "name", "name": "ndaFileName",
"storageKey": null "storageKey": null
}, },
v7 = { v7 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "description", "name": "ndaFileUrl",
"storageKey": null "storageKey": null
}, },
v8 = { v8 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "websiteUrl", "name": "name",
"storageKey": null "storageKey": null
}, },
v9 = { v9 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "logoUrl", "name": "description",
"storageKey": null "storageKey": null
}, },
v10 = { v10 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "email", "name": "websiteUrl",
"storageKey": null "storageKey": null
}, },
v11 = { v11 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
},
v12 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "headquarterAddress", "name": "headquarterAddress",
"storageKey": null "storageKey": null
}, },
v12 = [ v13 = [
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
"value": 50 "value": 50
} }
], ],
v13 = { v14 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "category", "name": "category",
"storageKey": null "storageKey": null
}, },
v14 = [ v15 = [
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
"value": 5 "value": 5
} }
], ],
v15 = { v16 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "isUserAuthorized", "name": "isUserAuthorized",
"storageKey": null "storageKey": null
}, },
v16 = { v17 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
@@ -170,6 +181,19 @@ return {
"metadata": null, "metadata": null,
"name": "TrustGraphCurrentQuery", "name": "TrustGraphCurrentQuery",
"selections": [ "selections": [
{
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v0/*: any*/),
(v1/*: any*/)
],
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -178,12 +202,12 @@ return {
"name": "currentTrustCenter", "name": "currentTrustCenter",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/), (v2/*: any*/),
(v3/*: any*/), (v3/*: any*/),
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/), (v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -192,12 +216,12 @@ return {
"name": "organization", "name": "organization",
"plural": false, "plural": false,
"selections": [ "selections": [
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/), (v8/*: any*/),
(v9/*: any*/), (v9/*: any*/),
(v10/*: any*/), (v10/*: any*/),
(v11/*: any*/) (v11/*: any*/),
(v0/*: any*/),
(v12/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
@@ -208,7 +232,7 @@ return {
}, },
{ {
"alias": null, "alias": null,
"args": (v12/*: any*/), "args": (v13/*: any*/),
"concreteType": "AuditConnection", "concreteType": "AuditConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "audits", "name": "audits",
@@ -230,7 +254,7 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/), (v2/*: any*/),
{ {
"args": null, "args": null,
"kind": "FragmentSpread", "kind": "FragmentSpread",
@@ -258,6 +282,20 @@ return {
"kind": "Operation", "kind": "Operation",
"name": "TrustGraphCurrentQuery", "name": "TrustGraphCurrentQuery",
"selections": [ "selections": [
{
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/)
],
"storageKey": null
},
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -266,12 +304,12 @@ return {
"name": "currentTrustCenter", "name": "currentTrustCenter",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/), (v2/*: any*/),
(v3/*: any*/), (v3/*: any*/),
(v4/*: any*/), (v4/*: any*/),
(v5/*: any*/), (v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -280,13 +318,13 @@ return {
"name": "organization", "name": "organization",
"plural": false, "plural": false,
"selections": [ "selections": [
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/), (v8/*: any*/),
(v9/*: any*/), (v9/*: any*/),
(v10/*: any*/), (v10/*: any*/),
(v11/*: any*/), (v11/*: any*/),
(v0/*: any*/) (v0/*: any*/),
(v12/*: any*/),
(v2/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
@@ -320,10 +358,10 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/), (v2/*: any*/),
(v6/*: any*/), (v8/*: any*/),
(v9/*: any*/), (v11/*: any*/),
(v8/*: any*/) (v10/*: any*/)
], ],
"storageKey": null "storageKey": null
} }
@@ -363,7 +401,7 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/), (v2/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -371,9 +409,9 @@ return {
"name": "countries", "name": "countries",
"storageKey": null "storageKey": null
}, },
(v6/*: any*/),
(v13/*: any*/),
(v8/*: any*/), (v8/*: any*/),
(v14/*: any*/),
(v10/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -392,7 +430,7 @@ return {
}, },
{ {
"alias": null, "alias": null,
"args": (v14/*: any*/), "args": (v15/*: any*/),
"concreteType": "DocumentConnection", "concreteType": "DocumentConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "documents", "name": "documents",
@@ -414,7 +452,7 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/), (v2/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -422,8 +460,8 @@ return {
"name": "title", "name": "title",
"storageKey": null "storageKey": null
}, },
(v15/*: any*/),
(v16/*: any*/), (v16/*: any*/),
(v17/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -442,7 +480,7 @@ return {
}, },
{ {
"alias": null, "alias": null,
"args": (v14/*: any*/), "args": (v15/*: any*/),
"concreteType": "TrustCenterFileConnection", "concreteType": "TrustCenterFileConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "trustCenterFiles", "name": "trustCenterFiles",
@@ -464,11 +502,11 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/), (v2/*: any*/),
(v13/*: any*/), (v14/*: any*/),
(v6/*: any*/), (v8/*: any*/),
(v15/*: any*/), (v16/*: any*/),
(v16/*: any*/) (v17/*: any*/)
], ],
"storageKey": null "storageKey": null
} }
@@ -480,7 +518,7 @@ return {
}, },
{ {
"alias": null, "alias": null,
"args": (v12/*: any*/), "args": (v13/*: any*/),
"concreteType": "AuditConnection", "concreteType": "AuditConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "audits", "name": "audits",
@@ -502,7 +540,7 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/), (v2/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -511,7 +549,7 @@ return {
"name": "report", "name": "report",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/), (v2/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -519,8 +557,8 @@ return {
"name": "filename", "name": "filename",
"storageKey": null "storageKey": null
}, },
(v15/*: any*/), (v16/*: any*/),
(v16/*: any*/) (v17/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
@@ -532,8 +570,8 @@ return {
"name": "framework", "name": "framework",
"plural": false, "plural": false,
"selections": [ "selections": [
(v0/*: any*/), (v2/*: any*/),
(v6/*: any*/), (v8/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -566,16 +604,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "f098177c07810cbe7609821d63c22b0b", "cacheID": "9d22288e0159409cf43bece191e3dc47",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "TrustGraphCurrentQuery", "name": "TrustGraphCurrentQuery",
"operationKind": "query", "operationKind": "query",
"text": "query TrustGraphCurrentQuery {\n currentTrustCenter {\n id\n slug\n isUserAuthenticated\n hasAcceptedNonDisclosureAgreement\n ndaFileName\n ndaFileUrl\n organization {\n name\n description\n websiteUrl\n logoUrl\n email\n headquarterAddress\n id\n }\n ...OverviewPageFragment\n audits(first: 50) {\n edges {\n node {\n id\n ...AuditRowFragment\n }\n }\n }\n }\n}\n\nfragment AuditRowFragment on Audit {\n report {\n id\n filename\n isUserAuthorized\n hasUserRequestedAccess\n }\n framework {\n id\n name\n lightLogoURL\n darkLogoURL\n }\n}\n\nfragment DocumentRowFragment on Document {\n id\n title\n isUserAuthorized\n hasUserRequestedAccess\n}\n\nfragment OverviewPageFragment on TrustCenter {\n references(first: 14) {\n edges {\n node {\n id\n name\n logoUrl\n websiteUrl\n }\n }\n }\n vendors(first: 3) {\n edges {\n node {\n id\n countries\n ...VendorRowFragment\n }\n }\n }\n documents(first: 5) {\n edges {\n node {\n id\n ...DocumentRowFragment\n documentType\n }\n }\n }\n trustCenterFiles(first: 5) {\n edges {\n node {\n id\n category\n ...TrustCenterFileRowFragment\n }\n }\n }\n}\n\nfragment TrustCenterFileRowFragment on TrustCenterFile {\n id\n name\n isUserAuthorized\n hasUserRequestedAccess\n}\n\nfragment VendorRowFragment on Vendor {\n id\n name\n category\n websiteUrl\n privacyPolicyUrl\n countries\n}\n" "text": "query TrustGraphCurrentQuery {\n viewer {\n email\n fullName\n id\n }\n currentTrustCenter {\n id\n slug\n isUserAuthenticated\n hasAcceptedNonDisclosureAgreement\n ndaFileName\n ndaFileUrl\n organization {\n name\n description\n websiteUrl\n logoUrl\n email\n headquarterAddress\n id\n }\n ...OverviewPageFragment\n audits(first: 50) {\n edges {\n node {\n id\n ...AuditRowFragment\n }\n }\n }\n }\n}\n\nfragment AuditRowFragment on Audit {\n report {\n id\n filename\n isUserAuthorized\n hasUserRequestedAccess\n }\n framework {\n id\n name\n lightLogoURL\n darkLogoURL\n }\n}\n\nfragment DocumentRowFragment on Document {\n id\n title\n isUserAuthorized\n hasUserRequestedAccess\n}\n\nfragment OverviewPageFragment on TrustCenter {\n references(first: 14) {\n edges {\n node {\n id\n name\n logoUrl\n websiteUrl\n }\n }\n }\n vendors(first: 3) {\n edges {\n node {\n id\n countries\n ...VendorRowFragment\n }\n }\n }\n documents(first: 5) {\n edges {\n node {\n id\n ...DocumentRowFragment\n documentType\n }\n }\n }\n trustCenterFiles(first: 5) {\n edges {\n node {\n id\n category\n ...TrustCenterFileRowFragment\n }\n }\n }\n}\n\nfragment TrustCenterFileRowFragment on TrustCenterFile {\n id\n name\n isUserAuthorized\n hasUserRequestedAccess\n}\n\nfragment VendorRowFragment on Vendor {\n id\n name\n category\n websiteUrl\n privacyPolicyUrl\n countries\n}\n"
} }
}; };
})(); })();
(node as any).hash = "2fc7c45c2636a551c13d806f89e53c4a"; (node as any).hash = "dd2040667d5cc7b9af9659e993ba6a5b";
export default node; export default node;

View File

@@ -56,7 +56,7 @@ const routes = [
{ {
path: "/overview", path: "/overview",
loader: loaderFromQueryLoader(() => loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentTrustGraphQuery, {}) loadQuery(consoleEnvironment, currentTrustGraphQuery, {}),
), ),
Component: withQueryRef(MainLayout), Component: withQueryRef(MainLayout),
Fallback: MainSkeleton, Fallback: MainSkeleton,
@@ -72,7 +72,7 @@ const routes = [
{ {
path: "/documents", path: "/documents",
loader: loaderFromQueryLoader(() => loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentTrustGraphQuery, {}) loadQuery(consoleEnvironment, currentTrustGraphQuery, {}),
), ),
Component: withQueryRef(MainLayout), Component: withQueryRef(MainLayout),
Fallback: MainSkeleton, Fallback: MainSkeleton,
@@ -81,7 +81,7 @@ const routes = [
{ {
path: "", path: "",
loader: loaderFromQueryLoader(() => loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentTrustDocumentsQuery, {}) loadQuery(consoleEnvironment, currentTrustDocumentsQuery, {}),
), ),
Fallback: TabSkeleton, Fallback: TabSkeleton,
Component: withQueryRef(DocumentsPage), Component: withQueryRef(DocumentsPage),
@@ -91,7 +91,7 @@ const routes = [
{ {
path: "/subprocessors", path: "/subprocessors",
loader: loaderFromQueryLoader(() => loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentTrustGraphQuery, {}) loadQuery(consoleEnvironment, currentTrustGraphQuery, {}),
), ),
Component: withQueryRef(MainLayout), Component: withQueryRef(MainLayout),
Fallback: MainSkeleton, Fallback: MainSkeleton,
@@ -100,7 +100,7 @@ const routes = [
{ {
path: "", path: "",
loader: loaderFromQueryLoader(() => loader: loaderFromQueryLoader(() =>
loadQuery(consoleEnvironment, currentTrustVendorsQuery, {}) loadQuery(consoleEnvironment, currentTrustVendorsQuery, {}),
), ),
Fallback: TabSkeleton, Fallback: TabSkeleton,
Component: withQueryRef(SubprocessorsPage), Component: withQueryRef(SubprocessorsPage),

View File

@@ -17,13 +17,17 @@ export default defineConfig({
target: "http://localhost:8080", target: "http://localhost:8080",
changeOrigin: true, changeOrigin: true,
}, },
"/trust/YJwjPEJCAAEAFgAAAZsTYtQt-FLmpawO/api": {
target: "http://localhost:8080",
changeOrigin: true,
},
}, },
}, },
resolve: { resolve: {
alias: { alias: {
"/type": fileURLToPath(new URL("./src/type.ts", import.meta.url)), "/type": fileURLToPath(new URL("./src/type.ts", import.meta.url)),
"/components": fileURLToPath( "/components": fileURLToPath(
new URL("./src/components", import.meta.url) new URL("./src/components", import.meta.url),
), ),
"/queries": fileURLToPath(new URL("./src/queries", import.meta.url)), "/queries": fileURLToPath(new URL("./src/queries", import.meta.url)),
"/helpers": fileURLToPath(new URL("./src/helpers", import.meta.url)), "/helpers": fileURLToPath(new URL("./src/helpers", import.meta.url)),

View File

@@ -196,6 +196,23 @@ func RenderTrustCenterDocumentAccessRejected(
return fmt.Sprintf(subjectTrustCenterDocumentAccessRejected, organizationName), textBody, htmlBody, err return fmt.Sprintf(subjectTrustCenterDocumentAccessRejected, organizationName), textBody, htmlBody, err
} }
func RenderMagicLink(baseURL, fullName, magicLinkUrl string, tokenDuration time.Duration) (subject string, textBody string, htmlBody *string, err error) {
data := struct {
FullName string
MagicLinkUrl string
LogoURL string
DurationInMinutes int
}{
FullName: fullName,
MagicLinkUrl: magicLinkUrl,
LogoURL: baseURL + logoURLPath,
DurationInMinutes: int(tokenDuration.Minutes()),
}
textBody, htmlBody, err = renderEmail(trustCenterAccessTextTemplate, trustCenterAccessHTMLTemplate, data)
return subjectTrustCenterAccess, textBody, htmlBody, err
}
func renderEmail(textTemplate *texttemplate.Template, htmlTemplate *htmltemplate.Template, data any) (textBody string, htmlBody *string, err error) { func renderEmail(textTemplate *texttemplate.Template, htmlTemplate *htmltemplate.Template, data any) (textBody string, htmlBody *string, err error) {
var textBuf bytes.Buffer var textBuf bytes.Buffer
if err := textTemplate.Execute(&textBuf, data); err != nil { if err := textTemplate.Execute(&textBuf, data); err != nil {

View File

@@ -1,17 +1,17 @@
import { render } from '@react-email/components'; import { render } from "@react-email/components";
import { copyFile, mkdir, writeFile } from 'node:fs/promises'; import { copyFile, mkdir, writeFile } from "node:fs/promises";
import { dirname, join } from 'node:path'; import { dirname, join } from "node:path";
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from "node:url";
import * as React from 'react'; import * as React from "react";
import ConfirmEmail from '../src/ConfirmEmail'; import ConfirmEmail from "../src/ConfirmEmail";
import DocumentExport from '../src/DocumentExport'; import DocumentExport from "../src/DocumentExport";
import DocumentSigning from '../src/DocumentSigning'; import DocumentSigning from "../src/DocumentSigning";
import FrameworkExport from '../src/FrameworkExport'; import FrameworkExport from "../src/FrameworkExport";
import Invitation from '../src/Invitation'; import Invitation from "../src/Invitation";
import PasswordReset from '../src/PasswordReset'; import PasswordReset from "../src/PasswordReset";
import TrustCenterAccess from '../src/TrustCenterAccess'; import TrustCenterAccess from "../src/TrustCenterAccess";
import TrustCenterDocumentAccessRejected from '../src/TrustCenterDocumentAccessRejected'; import TrustCenterDocumentAccessRejected from "../src/TrustCenterDocumentAccessRejected";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
@@ -23,42 +23,46 @@ type TemplateConfig = {
const templates: TemplateConfig[] = [ const templates: TemplateConfig[] = [
{ {
name: 'confirm-email', name: "confirm-email",
render: () => ConfirmEmail() render: () => ConfirmEmail(),
}, },
{ {
name: 'password-reset', name: "password-reset",
render: () => PasswordReset() render: () => PasswordReset(),
}, },
{ {
name: 'invitation', name: "invitation",
render: () => Invitation() render: () => Invitation(),
}, },
{ {
name: 'document-signing', name: "document-signing",
render: () => DocumentSigning() render: () => DocumentSigning(),
}, },
{ {
name: 'document-export', name: "document-export",
render: () => DocumentExport() render: () => DocumentExport(),
}, },
{ {
name: 'framework-export', name: "framework-export",
render: () => FrameworkExport() render: () => FrameworkExport(),
}, },
{ {
name: 'trust-center-access', name: "trust-center-access",
render: () => TrustCenterAccess() render: () => TrustCenterAccess(),
}, },
{ {
name: 'trust-center-document-access-rejected', name: "trust-center-document-access-rejected",
render: () => TrustCenterDocumentAccessRejected() render: () => TrustCenterDocumentAccessRejected(),
},
{
name: "magic-link",
render: () => TrustCenterAccess(),
}, },
]; ];
async function build() { async function build() {
const outputDir = join(__dirname, '..', 'dist'); const outputDir = join(__dirname, "..", "dist");
const templatesDir = join(__dirname, '..', 'templates'); const templatesDir = join(__dirname, "..", "templates");
await mkdir(outputDir, { recursive: true }); await mkdir(outputDir, { recursive: true });
for (const template of templates) { for (const template of templates) {
@@ -74,6 +78,6 @@ async function build() {
} }
build().catch((err) => { build().catch((err) => {
console.error('Failed to build email templates:', err); console.error("Failed to build email templates:", err);
process.exit(1); process.exit(1);
}); });

View File

@@ -0,0 +1,28 @@
import { Button, Section, Text } from "@react-email/components";
import * as React from "react";
import EmailLayout, {
bodyText,
button,
buttonContainer,
footerText,
} from "./components/EmailLayout";
export const MagicLink = () => {
return (
<EmailLayout subject="Probo Magic Link">
<Text style={bodyText}>Please use this link to connect to Probo:</Text>
<Section style={buttonContainer}>
<Button style={button} href={"{{.MagicLinkURL}}"}>
Connect to Probo
</Button>
</Section>
<Text style={footerText}>
This link will expire in {"{{.DurationInMinutes}}"} minutes.
</Text>
</EmailLayout>
);
};
export default MagicLink;

View File

@@ -1,22 +1,31 @@
import { Button, Section, Text } from '@react-email/components'; import { Button, Section, Text } from "@react-email/components";
import * as React from 'react'; import * as React from "react";
import EmailLayout, { bodyText, button, buttonContainer, footerText } from './components/EmailLayout'; import EmailLayout, {
bodyText,
button,
buttonContainer,
footerText,
} from "./components/EmailLayout";
export const TrustCenterAccess = () => { export const TrustCenterAccess = () => {
return ( return (
<EmailLayout subject={`Trust Center Access Invitation - ${'{{.OrganizationName}}'}`} organizationName={'{{.OrganizationName}}'}> <EmailLayout
subject={`Trust Center Access Invitation - ${"{{.OrganizationName}}"}`}
>
<Text style={bodyText}> <Text style={bodyText}>
You have been granted access to <strong>{'{{.OrganizationName}}'}</strong>'s Trust Center! Click the button below to access it: You have been granted access to{" "}
<strong>{"{{.OrganizationName}}"}</strong>'s Trust Center! Click the
button below to access it:
</Text> </Text>
<Section style={buttonContainer}> <Section style={buttonContainer}>
<Button style={button} href={'{{.AccessUrl}}'}> <Button style={button} href={"{{.AccessUrl}}"}>
Access Trust Center Access Trust Center
</Button> </Button>
</Section> </Section>
<Text style={footerText}> <Text style={footerText}>
This link will expire in {'{{.DurationInDays}}'} days. This link will expire in {"{{.DurationInDays}}"} days.
</Text> </Text>
</EmailLayout> </EmailLayout>
); );

View File

@@ -0,0 +1,11 @@
Probo
Hi {{.FullName}},
Please use this link to connect to Probo:
{{.MagicLinkURL}}
This link will expire in {{.DurationInMinutes}} minutes.
Probo Inc, 490 Post St, STE 640, San Francisco, CA, 94102, US

View File

@@ -1,3 +1,3 @@
module.exports = { module.exports = {
tabWidth: 2, tabWidth: 2,
}; };

View File

@@ -16,6 +16,7 @@ package iam
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"time" "time"
@@ -50,6 +51,11 @@ type (
FullName string FullName string
} }
LoadOrCreateIdentityRequest struct {
Email mail.Addr
FullName string
}
CreateIdentityWithPasswordRequest struct { CreateIdentityWithPasswordRequest struct {
Email mail.Addr Email mail.Addr
Password string Password string
@@ -59,11 +65,16 @@ type (
PasswordResetData struct { PasswordResetData struct {
Email mail.Addr `json:"email"` Email mail.Addr `json:"email"`
} }
MagicLinkData struct {
Email mail.Addr `json:"email"`
}
) )
const ( const (
TokenTypeOrganizationInvitation = "organization_invitation" TokenTypeOrganizationInvitation = "organization_invitation"
TokenTypePasswordReset = "password_reset" TokenTypePasswordReset = "password_reset"
TokenTypeMagicLink = "magic_link"
) )
func NewAuthService(svc *Service) *AuthService { func NewAuthService(svc *Service) *AuthService {
@@ -98,6 +109,14 @@ func (req ChangePasswordRequest) Validate() error {
return v.Error() return v.Error()
} }
func (req LoadOrCreateIdentityRequest) Validate() error {
v := validator.New()
v.Check(req.FullName, "fullName", validator.NotEmpty(), validator.MinLen(1), validator.MaxLen(255))
return v.Error()
}
func (req CreateIdentityWithPasswordRequest) Validate() error { func (req CreateIdentityWithPasswordRequest) Validate() error {
v := validator.New() v := validator.New()
@@ -300,6 +319,51 @@ func (s AuthService) SendPasswordResetInstructionByEmail(
) )
} }
func (s AuthService) LoadOrCreateIdentity(
ctx context.Context,
req *LoadOrCreateIdentityRequest,
) (*coredata.Identity, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
var (
identity *coredata.Identity
now = time.Now()
)
if err := s.pg.WithTx(ctx, func(tx pg.Conn) error {
identity = &coredata.Identity{}
if err := identity.LoadByEmail(ctx, tx, req.Email); err != nil {
if !errors.Is(err, coredata.ErrResourceNotFound) {
return fmt.Errorf("cannot load identity: %w", err)
}
identity = &coredata.Identity{
ID: gid.New(gid.NilTenant, coredata.IdentityEntityType),
EmailAddress: req.Email,
FullName: req.FullName,
HashedPassword: nil,
EmailAddressVerified: false,
CreatedAt: now,
UpdatedAt: now,
}
err = identity.Insert(ctx, tx)
if err != nil {
return fmt.Errorf("cannot insert identity: %w", err)
}
}
return nil
}); err != nil {
return nil, err
}
return identity, nil
}
func (s AuthService) CreateIdentityWithPassword( func (s AuthService) CreateIdentityWithPassword(
ctx context.Context, ctx context.Context,
req *CreateIdentityWithPasswordRequest, req *CreateIdentityWithPasswordRequest,
@@ -476,3 +540,103 @@ func (s AuthService) OpenSessionWithPassword(ctx context.Context, email mail.Add
return identity, session, err return identity, session, err
} }
func (s AuthService) SendMagicLink(ctx context.Context, email mail.Addr) error {
token, err := statelesstoken.NewToken(
s.tokenSecret,
TokenTypeMagicLink,
s.magicLinkTokenValidity,
MagicLinkData{
Email: email,
},
)
if err != nil {
return fmt.Errorf("cannot generate magic link token: %w", err)
}
base, err := baseurl.Parse(s.baseURL)
if err != nil {
return fmt.Errorf("cannot parse base URL: %w", err)
}
magicLinkURL := base.
WithPath("/auth/magic-link").
WithQuery("token", token).
MustString()
return s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
fullName := email.Username()
identity := &coredata.Identity{}
err := identity.LoadByEmail(ctx, tx, email)
if err == nil {
fullName = identity.FullName
} else {
if !errors.Is(err, coredata.ErrResourceNotFound) {
return fmt.Errorf("cannot load identity: %w", err)
}
}
subject, textBody, htmlBody, err := emails.RenderMagicLink(
s.baseURL,
fullName,
magicLinkURL,
s.invitationTokenValidity,
)
if err != nil {
return fmt.Errorf("cannot render magic link email: %w", err)
}
magicLinkEmail := coredata.NewEmail(
fullName,
email,
subject,
textBody,
htmlBody,
)
err = magicLinkEmail.Insert(ctx, tx)
if err != nil {
return fmt.Errorf("cannot insert email: %w", err)
}
return nil
},
)
}
func (s AuthService) OpenSessionWithMagicLink(ctx context.Context, token string) (*coredata.Identity, *coredata.Session, error) {
var (
identity = &coredata.Identity{}
session = &coredata.Session{}
)
payload, err := statelesstoken.ValidateToken[MagicLinkData](s.tokenSecret, TokenTypeMagicLink, token)
if err != nil {
return nil, nil, NewInvalidTokenError()
}
if err := s.pg.WithTx(
ctx,
func(conn pg.Conn) error {
err := identity.LoadByEmail(ctx, conn, payload.Data.Email)
if err != nil {
return fmt.Errorf("cannot load identity by email: %w", err)
}
session = coredata.NewRootSession(identity.ID, coredata.AuthMethodPassword, s.sessionDuration)
err = session.Insert(ctx, conn)
if err != nil {
return fmt.Errorf("cannot insert session: %w", err)
}
return nil
},
); err != nil {
return nil, nil, err
}
return identity, session, err
}

View File

@@ -31,6 +31,7 @@ type (
disableSignup bool disableSignup bool
invitationTokenValidity time.Duration invitationTokenValidity time.Duration
passwordResetTokenValidity time.Duration passwordResetTokenValidity time.Duration
magicLinkTokenValidity time.Duration
sessionDuration time.Duration sessionDuration time.Duration
bucket string bucket string
certificate *x509.Certificate certificate *x509.Certificate
@@ -53,6 +54,7 @@ type (
DisableSignup bool DisableSignup bool
InvitationTokenValidity time.Duration InvitationTokenValidity time.Duration
PasswordResetTokenValidity time.Duration PasswordResetTokenValidity time.Duration
MagicLinkTokenValidity time.Duration
SessionDuration time.Duration SessionDuration time.Duration
Bucket string Bucket string
TokenSecret string TokenSecret string
@@ -99,6 +101,7 @@ func NewService(
disableSignup: cfg.DisableSignup, disableSignup: cfg.DisableSignup,
invitationTokenValidity: cfg.InvitationTokenValidity, invitationTokenValidity: cfg.InvitationTokenValidity,
passwordResetTokenValidity: cfg.PasswordResetTokenValidity, passwordResetTokenValidity: cfg.PasswordResetTokenValidity,
magicLinkTokenValidity: cfg.MagicLinkTokenValidity,
sessionDuration: cfg.SessionDuration, sessionDuration: cfg.SessionDuration,
bucket: cfg.Bucket, bucket: cfg.Bucket,
certificate: cfg.Certificate, certificate: cfg.Certificate,

View File

@@ -16,6 +16,19 @@ func (a Addr) String() string {
return string(a) return string(a)
} }
func (a *Addr) Username() string {
if a == nil || *a == Nil {
return ""
}
parts := strings.Split(a.String(), "@")
if len(parts) != 2 {
return ""
}
return parts[0]
}
func (a *Addr) Domain() string { func (a *Addr) Domain() string {
if a == nil || *a == Nil { if a == nil || *a == Nil {
return "" return ""

View File

@@ -468,3 +468,25 @@ func (s *Service) LoadTrustCenterByID(ctx context.Context, id gid.GID) (*TrustCe
return &info, err return &info, err
} }
func (s *Service) LoadTrustCenterByOrganizationID(ctx context.Context, organizationID gid.GID) (*TrustCenterInfo, error) {
var info TrustCenterInfo
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
scope := coredata.NewScope(organizationID.TenantID())
var trustCenter coredata.TrustCenter
if err := trustCenter.LoadByOrganizationID(ctx, conn, scope, organizationID); err != nil {
return fmt.Errorf("cannot load trust center: %w", err)
}
info.ID = trustCenter.ID
info.OrganizationID = trustCenter.OrganizationID
return nil
},
)
return &info, err
}

View File

@@ -40,7 +40,7 @@ type (
CreateTrustCenterAccessRequest struct { CreateTrustCenterAccessRequest struct {
TrustCenterID gid.GID TrustCenterID gid.GID
Email mail.Addr Email mail.Addr
Name string FullName string
} }
UpdateTrustCenterDocumentAccessRequest struct { UpdateTrustCenterDocumentAccessRequest struct {
@@ -69,7 +69,7 @@ func (ctcar *CreateTrustCenterAccessRequest) Validate() error {
v.Check(ctcar.TrustCenterID, "trust_center_id", validator.Required(), validator.GID(coredata.TrustCenterEntityType)) v.Check(ctcar.TrustCenterID, "trust_center_id", validator.Required(), validator.GID(coredata.TrustCenterEntityType))
v.Check(ctcar.Email, "email", validator.Required(), validator.NotEmpty()) v.Check(ctcar.Email, "email", validator.Required(), validator.NotEmpty())
v.Check(ctcar.Email.Domain(), "email", validator.NotBlacklisted()) v.Check(ctcar.Email.Domain(), "email", validator.NotBlacklisted())
v.Check(ctcar.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength)) v.Check(ctcar.FullName, "name", validator.SafeTextNoNewLine(TitleMaxLength))
return v.Error() return v.Error()
} }
@@ -244,7 +244,7 @@ func (s TrustCenterAccessService) Create(
TenantID: s.svc.scope.GetTenantID(), TenantID: s.svc.scope.GetTenantID(),
TrustCenterID: req.TrustCenterID, TrustCenterID: req.TrustCenterID,
Email: req.Email, Email: req.Email,
Name: req.Name, Name: req.FullName,
Active: false, Active: false,
HasAcceptedNonDisclosureAgreement: false, HasAcceptedNonDisclosureAgreement: false,
CreatedAt: now, CreatedAt: now,

View File

@@ -26,6 +26,7 @@ type (
DisableSignup bool `json:"disable-signup"` DisableSignup bool `json:"disable-signup"`
InvitationConfirmationTokenValidity int `json:"invitation-confirmation-token-validity"` InvitationConfirmationTokenValidity int `json:"invitation-confirmation-token-validity"`
PasswordResetTokenValidity int `json:"password-reset-token-validity"` PasswordResetTokenValidity int `json:"password-reset-token-validity"`
MagicLinkTokenValidity int `json:"magic-link-token-validity"`
SAML samlConfig `json:"saml"` SAML samlConfig `json:"saml"`
} }

View File

@@ -125,6 +125,7 @@ func New() *Implm {
DisableSignup: false, DisableSignup: false,
InvitationConfirmationTokenValidity: 3600, InvitationConfirmationTokenValidity: 3600,
PasswordResetTokenValidity: 3600, PasswordResetTokenValidity: 3600,
MagicLinkTokenValidity: 3600,
SAML: samlConfig{ SAML: samlConfig{
SessionDuration: 604800, SessionDuration: 604800,
CleanupIntervalSeconds: 86400, CleanupIntervalSeconds: 86400,
@@ -318,6 +319,7 @@ func (impl *Implm) Run(
DisableSignup: impl.cfg.Auth.DisableSignup, DisableSignup: impl.cfg.Auth.DisableSignup,
InvitationTokenValidity: time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity) * time.Second, InvitationTokenValidity: time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity) * time.Second,
PasswordResetTokenValidity: time.Duration(impl.cfg.Auth.PasswordResetTokenValidity) * time.Second, PasswordResetTokenValidity: time.Duration(impl.cfg.Auth.PasswordResetTokenValidity) * time.Second,
MagicLinkTokenValidity: time.Duration(impl.cfg.Auth.MagicLinkTokenValidity) * time.Second,
SessionDuration: time.Duration(impl.cfg.Auth.Cookie.Duration) * time.Hour, SessionDuration: time.Duration(impl.cfg.Auth.Cookie.Duration) * time.Hour,
Bucket: impl.cfg.AWS.Bucket, Bucket: impl.cfg.AWS.Bucket,
TokenSecret: impl.cfg.Auth.Cookie.Secret, TokenSecret: impl.cfg.Auth.Cookie.Secret,

View File

@@ -28,6 +28,7 @@ var (
identityContextKey = &ctxKey{name: "identity"} identityContextKey = &ctxKey{name: "identity"}
sessionContextKey = &ctxKey{name: "session"} sessionContextKey = &ctxKey{name: "session"}
apiKeyContextKey = &ctxKey{name: "api_key"} apiKeyContextKey = &ctxKey{name: "api_key"}
TrustCenterKey = &ctxKey{name: "trust_center"}
) )
func SessionFromContext(ctx context.Context) *coredata.Session { func SessionFromContext(ctx context.Context) *coredata.Session {

View File

@@ -359,11 +359,11 @@ func (r *membershipProfileResolver) Permission(ctx context.Context, obj *types.M
func (r *mutationResolver) SignIn(ctx context.Context, input types.SignInInput) (*types.SignInPayload, error) { func (r *mutationResolver) SignIn(ctx context.Context, input types.SignInInput) (*types.SignInPayload, error) {
// TODO: handle existing session to only open child session and chnage root session auth method to PASSWORD // TODO: handle existing session to only open child session and chnage root session auth method to PASSWORD
user, session, err := r.iam.AuthService.OpenSessionWithPassword(ctx, input.Email, input.Password) identity, session, err := r.iam.AuthService.OpenSessionWithPassword(ctx, input.Email, input.Password)
if err != nil { if err != nil {
var errInvalidPassword *iam.ErrInvalidPassword var errInvalidPassword *iam.ErrInvalidPassword
if errors.As(err, &errInvalidPassword) { if errors.As(err, &errInvalidPassword) {
return nil, graphql.ErrorOnPath(ctx, err) return nil, gqlutils.Invalid(ctx, err)
} }
var errInvalidCredentials *iam.ErrInvalidCredentials var errInvalidCredentials *iam.ErrInvalidCredentials
@@ -384,7 +384,7 @@ func (r *mutationResolver) SignIn(ctx context.Context, input types.SignInInput)
r.sessionCookie.Set(w, session) r.sessionCookie.Set(w, session)
return &types.SignInPayload{ return &types.SignInPayload{
Identity: types.NewIdentity(user), Identity: types.NewIdentity(identity),
Session: types.NewSession(session), Session: types.NewSession(session),
}, nil }, nil
} }

View File

@@ -32,6 +32,7 @@ func NewGraphQLHandler(iamSvc *iam.Service, proboSvc *probo.Service, customDomai
probo: proboSvc, probo: proboSvc,
iam: iamSvc, iam: iamSvc,
customDomainCname: customDomainCname, customDomainCname: customDomainCname,
logger: logger,
}, },
} }

View File

@@ -46,6 +46,7 @@ type (
authorize authz.AuthorizeFunc authorize authz.AuthorizeFunc
probo *probo.Service probo *probo.Service
iam *iam.Service iam *iam.Service
logger *log.Logger
customDomainCname string customDomainCname string
} }
) )
@@ -97,7 +98,10 @@ func NewMux(
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(requests) if err := json.NewEncoder(w).Encode(requests); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}, },
) )
@@ -151,7 +155,7 @@ func NewMux(
w.Header().Set("Content-Type", "application/pdf") w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s.pdf\"", uuid.String())) w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s.pdf\"", uuid.String()))
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write(pdfData) _, _ = w.Write(pdfData)
}, },
) )

View File

@@ -60,7 +60,7 @@ func NewAuditEdge(a *coredata.Audit, orderField coredata.AuditOrderField) *Audit
} }
func NewAudit(a *coredata.Audit) *Audit { func NewAudit(a *coredata.Audit) *Audit {
return &Audit{ node := &Audit{
ID: a.ID, ID: a.ID,
Organization: &Organization{ Organization: &Organization{
ID: a.OrganizationID, ID: a.OrganizationID,
@@ -76,4 +76,12 @@ func NewAudit(a *coredata.Audit) *Audit {
CreatedAt: a.CreatedAt, CreatedAt: a.CreatedAt,
UpdatedAt: a.UpdatedAt, UpdatedAt: a.UpdatedAt,
} }
if a.ReportID != nil {
node.Report = &Report{
ID: *a.ReportID,
}
}
return node
} }

View File

@@ -14,6 +14,7 @@ import (
"time" "time"
pgx "github.com/jackc/pgx/v5" pgx "github.com/jackc/pgx/v5"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/iam"
@@ -1674,12 +1675,28 @@ func (r *mutationResolver) CreateTrustCenterAccess(ctx context.Context, input ty
prb := r.ProboService(ctx, input.TrustCenterID.TenantID()) prb := r.ProboService(ctx, input.TrustCenterID.TenantID())
// TODO: when admin/owner creates trust center access, we should have an invite for it instead of directly creating the identity
identity := authn.IdentityFromContext(ctx)
if identity == nil {
var err error
identity, err = r.iam.AuthService.LoadOrCreateIdentity(
ctx,
&iam.LoadOrCreateIdentityRequest{
Email: input.Email,
},
)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot load or create identity", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
}
access, err := prb.TrustCenterAccesses.Create( access, err := prb.TrustCenterAccesses.Create(
ctx, ctx,
&probo.CreateTrustCenterAccessRequest{ &probo.CreateTrustCenterAccessRequest{
TrustCenterID: input.TrustCenterID, TrustCenterID: input.TrustCenterID,
Email: input.Email, Email: identity.EmailAddress,
Name: input.Name, FullName: identity.FullName,
}, },
) )
if err != nil { if err != nil {
@@ -1687,8 +1704,8 @@ func (r *mutationResolver) CreateTrustCenterAccess(ctx context.Context, input ty
return nil, gqlutils.Conflict(ctx, err) return nil, gqlutils.Conflict(ctx, err)
} }
// TODO no panic use gqlutils.InternalError r.logger.ErrorCtx(ctx, "cannot create trust center access", log.Error(err))
panic(fmt.Errorf("cannot create trust center access: %w", err)) return nil, gqlutils.Internal(ctx)
} }
return &types.CreateTrustCenterAccessPayload{ return &types.CreateTrustCenterAccessPayload{

View File

@@ -20,13 +20,16 @@ import (
"context" "context"
"time" "time"
"github.com/99designs/gqlgen/graphql"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"go.gearno.de/kit/log" "go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/securecookie" "go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server/api/authn" "go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/trust/v1/schema" "go.probo.inc/probo/pkg/server/api/trust/v1/schema"
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils" "go.probo.inc/probo/pkg/server/gqlutils"
"go.probo.inc/probo/pkg/trust" "go.probo.inc/probo/pkg/trust"
) )
@@ -45,10 +48,28 @@ type (
} }
Resolver struct { Resolver struct {
trust *trust.Service trust *trust.Service
logger *log.Logger
iam *iam.Service
sessionCookie *authn.Cookie
} }
) )
type ctxKey struct{ name string }
var (
TrustCenterKey = &ctxKey{name: "trust_center"}
)
func TrustCenterFromContext(ctx context.Context) probo.TrustCenterInfo {
trustCenter, _ := ctx.Value(TrustCenterKey).(probo.TrustCenterInfo)
return trustCenter
}
func ContextWithTrustCenter(ctx context.Context, trustCenter probo.TrustCenterInfo) context.Context {
return context.WithValue(ctx, TrustCenterKey, trustCenter)
}
func NewMux( func NewMux(
logger *log.Logger, logger *log.Logger,
iamSvc *iam.Service, iamSvc *iam.Service,
@@ -60,7 +81,25 @@ func NewMux(
sessionMiddleware := authn.NewSessionMiddleware(iamSvc, cookieConfig) sessionMiddleware := authn.NewSessionMiddleware(iamSvc, cookieConfig)
r.Use(sessionMiddleware) r.Use(sessionMiddleware)
config := schema.Config{Resolvers: &Resolver{trust: trustSvc}} config := schema.Config{
Resolvers: &Resolver{
iam: iamSvc,
trust: trustSvc,
logger: logger,
sessionCookie: authn.NewCookie(&cookieConfig),
},
Directives: schema.DirectiveRoot{
MustBeAuthenticated: func(ctx context.Context, obj any, next graphql.Resolver, role *types.Role) (any, error) {
identity := authn.IdentityFromContext(ctx)
if identity == nil {
return nil, gqlutils.Unauthenticatedf(ctx, "authentication required")
}
return next(ctx)
},
},
}
es := schema.NewExecutableSchema(config) es := schema.NewExecutableSchema(config)
graphqlHandler := gqlutils.NewHandler(es, logger) graphqlHandler := gqlutils.NewHandler(es, logger)
@@ -73,14 +112,6 @@ func (r *Resolver) RootTrustService(ctx context.Context) *trust.TenantService {
return r.trust.WithTenant(gid.NewTenantID()) return r.trust.WithTenant(gid.NewTenantID())
} }
func (r *Resolver) PublicTrustService(ctx context.Context, tenantID gid.TenantID) *trust.TenantService { func (r *Resolver) TrustService(ctx context.Context, tenantID gid.TenantID) *trust.TenantService {
return r.trust.WithTenant(tenantID) return r.trust.WithTenant(tenantID)
} }
func (r *Resolver) PrivateTrustService(ctx context.Context, tenantID gid.TenantID) (*trust.TenantService, error) {
// if err := trustauth.ValidateTenantAccess(ctx, r, userTenantContextKey, tenantID); err != nil {
// return nil, fmt.Errorf("cannot access trust center: %w", err)
// }
return r.trust.WithTenant(tenantID), nil
}

View File

@@ -34,6 +34,15 @@ type PageInfo {
endCursor: CursorKey endCursor: CursorKey
} }
type Identity implements Node {
id: ID!
email: EmailAddr!
fullName: String!
emailVerified: Boolean!
createdAt: Datetime!
updatedAt: Datetime!
}
type Organization implements Node { type Organization implements Node {
id: ID! id: ID!
name: String! name: String!
@@ -535,10 +544,18 @@ type TrustCenterAccess implements Node {
updatedAt: Datetime! updatedAt: Datetime!
} }
input SignInWithTokenInput {
token: String!
}
type SignInWithTokenPayload {
success: Boolean!
}
input RequestAllAccessesInput { input RequestAllAccessesInput {
trustCenterId: ID! trustCenterId: ID!
email: EmailAddr email: EmailAddr!
name: String fullName: String!
} }
type RequestAccessesPayload { type RequestAccessesPayload {
@@ -560,22 +577,22 @@ input AcceptNonDisclosureAgreementInput {
input RequestDocumentAccessInput { input RequestDocumentAccessInput {
trustCenterId: ID! trustCenterId: ID!
documentId: ID! documentId: ID!
email: EmailAddr email: EmailAddr!
name: String fullName: String!
} }
input RequestReportAccessInput { input RequestReportAccessInput {
trustCenterId: ID! trustCenterId: ID!
reportId: ID! reportId: ID!
email: EmailAddr email: EmailAddr!
name: String fullName: String!
} }
input RequestTrustCenterFileAccessInput { input RequestTrustCenterFileAccessInput {
trustCenterId: ID! trustCenterId: ID!
trustCenterFileId: ID! trustCenterFileId: ID!
email: EmailAddr email: EmailAddr!
name: String fullName: String!
} }
input ExportTrustCenterFileInput { input ExportTrustCenterFileInput {
@@ -599,12 +616,15 @@ type AcceptNonDisclosureAgreementPayload {
} }
type Query { type Query {
viewer: Identity
node(id: ID!): Node! node(id: ID!): Node!
trustCenterBySlug(slug: String!): TrustCenter @mustBeAuthenticated(role: NONE) trustCenterBySlug(slug: String!): TrustCenter @mustBeAuthenticated(role: NONE)
currentTrustCenter: TrustCenter @mustBeAuthenticated(role: NONE) currentTrustCenter: TrustCenter @mustBeAuthenticated(role: NONE)
} }
type Mutation { type Mutation {
signInWithToken(input: SignInWithTokenInput!): SignInWithTokenPayload!
requestAllAccesses(input: RequestAllAccessesInput!): RequestAccessesPayload! requestAllAccesses(input: RequestAllAccessesInput!): RequestAccessesPayload!
@mustBeAuthenticated(role: NONE) @mustBeAuthenticated(role: NONE)

View File

@@ -120,6 +120,15 @@ type ComplexityRoot struct {
Name func(childComplexity int) int Name func(childComplexity int) int
} }
Identity struct {
CreatedAt func(childComplexity int) int
Email func(childComplexity int) int
EmailVerified func(childComplexity int) int
FullName func(childComplexity int) int
ID func(childComplexity int) int
UpdatedAt func(childComplexity int) int
}
Mutation struct { Mutation struct {
AcceptNonDisclosureAgreement func(childComplexity int, input types.AcceptNonDisclosureAgreementInput) int AcceptNonDisclosureAgreement func(childComplexity int, input types.AcceptNonDisclosureAgreementInput) int
ExportDocumentPDF func(childComplexity int, input types.ExportDocumentPDFInput) int ExportDocumentPDF func(childComplexity int, input types.ExportDocumentPDFInput) int
@@ -129,6 +138,7 @@ type ComplexityRoot struct {
RequestDocumentAccess func(childComplexity int, input types.RequestDocumentAccessInput) int RequestDocumentAccess func(childComplexity int, input types.RequestDocumentAccessInput) int
RequestReportAccess func(childComplexity int, input types.RequestReportAccessInput) int RequestReportAccess func(childComplexity int, input types.RequestReportAccessInput) int
RequestTrustCenterFileAccess func(childComplexity int, input types.RequestTrustCenterFileAccessInput) int RequestTrustCenterFileAccess func(childComplexity int, input types.RequestTrustCenterFileAccessInput) int
SignInWithToken func(childComplexity int, input types.SignInWithTokenInput) int
} }
Organization struct { Organization struct {
@@ -152,6 +162,7 @@ type ComplexityRoot struct {
CurrentTrustCenter func(childComplexity int) int CurrentTrustCenter func(childComplexity int) int
Node func(childComplexity int, id gid.GID) int Node func(childComplexity int, id gid.GID) int
TrustCenterBySlug func(childComplexity int, slug string) int TrustCenterBySlug func(childComplexity int, slug string) int
Viewer func(childComplexity int) int
} }
Report struct { Report struct {
@@ -165,6 +176,10 @@ type ComplexityRoot struct {
TrustCenterAccess func(childComplexity int) int TrustCenterAccess func(childComplexity int) int
} }
SignInWithTokenPayload struct {
Success func(childComplexity int) int
}
TrustCenter struct { TrustCenter struct {
Active func(childComplexity int) int Active func(childComplexity int) int
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
@@ -258,6 +273,7 @@ type FrameworkResolver interface {
DarkLogoURL(ctx context.Context, obj *types.Framework) (*string, error) DarkLogoURL(ctx context.Context, obj *types.Framework) (*string, error)
} }
type MutationResolver interface { type MutationResolver interface {
SignInWithToken(ctx context.Context, input types.SignInWithTokenInput) (*types.SignInWithTokenPayload, error)
RequestAllAccesses(ctx context.Context, input types.RequestAllAccessesInput) (*types.RequestAccessesPayload, error) RequestAllAccesses(ctx context.Context, input types.RequestAllAccessesInput) (*types.RequestAccessesPayload, error)
ExportDocumentPDF(ctx context.Context, input types.ExportDocumentPDFInput) (*types.ExportDocumentPDFPayload, error) ExportDocumentPDF(ctx context.Context, input types.ExportDocumentPDFInput) (*types.ExportDocumentPDFPayload, error)
ExportReportPDF(ctx context.Context, input types.ExportReportPDFInput) (*types.ExportReportPDFPayload, error) ExportReportPDF(ctx context.Context, input types.ExportReportPDFInput) (*types.ExportReportPDFPayload, error)
@@ -271,6 +287,7 @@ type OrganizationResolver interface {
LogoURL(ctx context.Context, obj *types.Organization) (*string, error) LogoURL(ctx context.Context, obj *types.Organization) (*string, error)
} }
type QueryResolver interface { type QueryResolver interface {
Viewer(ctx context.Context) (*types.Identity, error)
Node(ctx context.Context, id gid.GID) (types.Node, error) Node(ctx context.Context, id gid.GID) (types.Node, error)
TrustCenterBySlug(ctx context.Context, slug string) (*types.TrustCenter, error) TrustCenterBySlug(ctx context.Context, slug string) (*types.TrustCenter, error)
CurrentTrustCenter(ctx context.Context) (*types.TrustCenter, error) CurrentTrustCenter(ctx context.Context) (*types.TrustCenter, error)
@@ -472,6 +489,43 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Framework.Name(childComplexity), true return e.complexity.Framework.Name(childComplexity), true
case "Identity.createdAt":
if e.complexity.Identity.CreatedAt == nil {
break
}
return e.complexity.Identity.CreatedAt(childComplexity), true
case "Identity.email":
if e.complexity.Identity.Email == nil {
break
}
return e.complexity.Identity.Email(childComplexity), true
case "Identity.emailVerified":
if e.complexity.Identity.EmailVerified == nil {
break
}
return e.complexity.Identity.EmailVerified(childComplexity), true
case "Identity.fullName":
if e.complexity.Identity.FullName == nil {
break
}
return e.complexity.Identity.FullName(childComplexity), true
case "Identity.id":
if e.complexity.Identity.ID == nil {
break
}
return e.complexity.Identity.ID(childComplexity), true
case "Identity.updatedAt":
if e.complexity.Identity.UpdatedAt == nil {
break
}
return e.complexity.Identity.UpdatedAt(childComplexity), true
case "Mutation.acceptNonDisclosureAgreement": case "Mutation.acceptNonDisclosureAgreement":
if e.complexity.Mutation.AcceptNonDisclosureAgreement == nil { if e.complexity.Mutation.AcceptNonDisclosureAgreement == nil {
break break
@@ -560,6 +614,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
} }
return e.complexity.Mutation.RequestTrustCenterFileAccess(childComplexity, args["input"].(types.RequestTrustCenterFileAccessInput)), true return e.complexity.Mutation.RequestTrustCenterFileAccess(childComplexity, args["input"].(types.RequestTrustCenterFileAccessInput)), true
case "Mutation.signInWithToken":
if e.complexity.Mutation.SignInWithToken == nil {
break
}
args, err := ec.field_Mutation_signInWithToken_args(ctx, rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.SignInWithToken(childComplexity, args["input"].(types.SignInWithTokenInput)), true
case "Organization.description": case "Organization.description":
if e.complexity.Organization.Description == nil { if e.complexity.Organization.Description == nil {
@@ -657,6 +722,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
} }
return e.complexity.Query.TrustCenterBySlug(childComplexity, args["slug"].(string)), true return e.complexity.Query.TrustCenterBySlug(childComplexity, args["slug"].(string)), true
case "Query.viewer":
if e.complexity.Query.Viewer == nil {
break
}
return e.complexity.Query.Viewer(childComplexity), true
case "Report.filename": case "Report.filename":
if e.complexity.Report.Filename == nil { if e.complexity.Report.Filename == nil {
@@ -690,6 +761,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.RequestAccessesPayload.TrustCenterAccess(childComplexity), true return e.complexity.RequestAccessesPayload.TrustCenterAccess(childComplexity), true
case "SignInWithTokenPayload.success":
if e.complexity.SignInWithTokenPayload.Success == nil {
break
}
return e.complexity.SignInWithTokenPayload.Success(childComplexity), true
case "TrustCenter.active": case "TrustCenter.active":
if e.complexity.TrustCenter.Active == nil { if e.complexity.TrustCenter.Active == nil {
break break
@@ -1018,6 +1096,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputRequestDocumentAccessInput, ec.unmarshalInputRequestDocumentAccessInput,
ec.unmarshalInputRequestReportAccessInput, ec.unmarshalInputRequestReportAccessInput,
ec.unmarshalInputRequestTrustCenterFileAccessInput, ec.unmarshalInputRequestTrustCenterFileAccessInput,
ec.unmarshalInputSignInWithTokenInput,
) )
first := true first := true
@@ -1151,6 +1230,15 @@ type PageInfo {
endCursor: CursorKey endCursor: CursorKey
} }
type Identity implements Node {
id: ID!
email: EmailAddr!
fullName: String!
emailVerified: Boolean!
createdAt: Datetime!
updatedAt: Datetime!
}
type Organization implements Node { type Organization implements Node {
id: ID! id: ID!
name: String! name: String!
@@ -1652,10 +1740,18 @@ type TrustCenterAccess implements Node {
updatedAt: Datetime! updatedAt: Datetime!
} }
input SignInWithTokenInput {
token: String!
}
type SignInWithTokenPayload {
success: Boolean!
}
input RequestAllAccessesInput { input RequestAllAccessesInput {
trustCenterId: ID! trustCenterId: ID!
email: EmailAddr email: EmailAddr!
name: String fullName: String!
} }
type RequestAccessesPayload { type RequestAccessesPayload {
@@ -1677,22 +1773,22 @@ input AcceptNonDisclosureAgreementInput {
input RequestDocumentAccessInput { input RequestDocumentAccessInput {
trustCenterId: ID! trustCenterId: ID!
documentId: ID! documentId: ID!
email: EmailAddr email: EmailAddr!
name: String fullName: String!
} }
input RequestReportAccessInput { input RequestReportAccessInput {
trustCenterId: ID! trustCenterId: ID!
reportId: ID! reportId: ID!
email: EmailAddr email: EmailAddr!
name: String fullName: String!
} }
input RequestTrustCenterFileAccessInput { input RequestTrustCenterFileAccessInput {
trustCenterId: ID! trustCenterId: ID!
trustCenterFileId: ID! trustCenterFileId: ID!
email: EmailAddr email: EmailAddr!
name: String fullName: String!
} }
input ExportTrustCenterFileInput { input ExportTrustCenterFileInput {
@@ -1716,12 +1812,15 @@ type AcceptNonDisclosureAgreementPayload {
} }
type Query { type Query {
viewer: Identity
node(id: ID!): Node! node(id: ID!): Node!
trustCenterBySlug(slug: String!): TrustCenter @mustBeAuthenticated(role: NONE) trustCenterBySlug(slug: String!): TrustCenter @mustBeAuthenticated(role: NONE)
currentTrustCenter: TrustCenter @mustBeAuthenticated(role: NONE) currentTrustCenter: TrustCenter @mustBeAuthenticated(role: NONE)
} }
type Mutation { type Mutation {
signInWithToken(input: SignInWithTokenInput!): SignInWithTokenPayload!
requestAllAccesses(input: RequestAllAccessesInput!): RequestAccessesPayload! requestAllAccesses(input: RequestAllAccessesInput!): RequestAccessesPayload!
@mustBeAuthenticated(role: NONE) @mustBeAuthenticated(role: NONE)
@@ -1858,6 +1957,17 @@ func (ec *executionContext) field_Mutation_requestTrustCenterFileAccess_args(ctx
return args, nil return args, nil
} }
func (ec *executionContext) field_Mutation_signInWithToken_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNSignInWithTokenInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSignInWithTokenInput)
if err != nil {
return nil, err
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error var err error
args := map[string]any{} args := map[string]any{}
@@ -2841,6 +2951,225 @@ func (ec *executionContext) fieldContext_Framework_darkLogoURL(_ context.Context
return fc, nil return fc, nil
} }
func (ec *executionContext) _Identity_id(ctx context.Context, field graphql.CollectedField, obj *types.Identity) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Identity_id,
func(ctx context.Context) (any, error) {
return obj.ID, nil
},
nil,
ec.marshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID,
true,
true,
)
}
func (ec *executionContext) fieldContext_Identity_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Identity",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type ID does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Identity_email(ctx context.Context, field graphql.CollectedField, obj *types.Identity) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Identity_email,
func(ctx context.Context) (any, error) {
return obj.Email, nil
},
nil,
ec.marshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr,
true,
true,
)
}
func (ec *executionContext) fieldContext_Identity_email(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Identity",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type EmailAddr does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Identity_fullName(ctx context.Context, field graphql.CollectedField, obj *types.Identity) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Identity_fullName,
func(ctx context.Context) (any, error) {
return obj.FullName, nil
},
nil,
ec.marshalNString2string,
true,
true,
)
}
func (ec *executionContext) fieldContext_Identity_fullName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Identity",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Identity_emailVerified(ctx context.Context, field graphql.CollectedField, obj *types.Identity) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Identity_emailVerified,
func(ctx context.Context) (any, error) {
return obj.EmailVerified, nil
},
nil,
ec.marshalNBoolean2bool,
true,
true,
)
}
func (ec *executionContext) fieldContext_Identity_emailVerified(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Identity",
Field: field,
IsMethod: false,
IsResolver: false,
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) _Identity_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Identity) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Identity_createdAt,
func(ctx context.Context) (any, error) {
return obj.CreatedAt, nil
},
nil,
ec.marshalNDatetime2timeᚐTime,
true,
true,
)
}
func (ec *executionContext) fieldContext_Identity_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Identity",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Datetime does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Identity_updatedAt(ctx context.Context, field graphql.CollectedField, obj *types.Identity) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Identity_updatedAt,
func(ctx context.Context) (any, error) {
return obj.UpdatedAt, nil
},
nil,
ec.marshalNDatetime2timeᚐTime,
true,
true,
)
}
func (ec *executionContext) fieldContext_Identity_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Identity",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Datetime does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Mutation_signInWithToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Mutation_signInWithToken,
func(ctx context.Context) (any, error) {
fc := graphql.GetFieldContext(ctx)
return ec.resolvers.Mutation().SignInWithToken(ctx, fc.Args["input"].(types.SignInWithTokenInput))
},
nil,
ec.marshalNSignInWithTokenPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSignInWithTokenPayload,
true,
true,
)
}
func (ec *executionContext) fieldContext_Mutation_signInWithToken(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Mutation",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "success":
return ec.fieldContext_SignInWithTokenPayload_success(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type SignInWithTokenPayload", field.Name)
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_signInWithToken_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
return fc, nil
}
func (ec *executionContext) _Mutation_requestAllAccesses(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Mutation_requestAllAccesses(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
return graphql.ResolveField( return graphql.ResolveField(
ctx, ctx,
@@ -3664,6 +3993,49 @@ func (ec *executionContext) fieldContext_PageInfo_endCursor(_ context.Context, f
return fc, nil return fc, nil
} }
func (ec *executionContext) _Query_viewer(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Query_viewer,
func(ctx context.Context) (any, error) {
return ec.resolvers.Query().Viewer(ctx)
},
nil,
ec.marshalOIdentity2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐIdentity,
true,
false,
)
}
func (ec *executionContext) fieldContext_Query_viewer(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Query",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "id":
return ec.fieldContext_Identity_id(ctx, field)
case "email":
return ec.fieldContext_Identity_email(ctx, field)
case "fullName":
return ec.fieldContext_Identity_fullName(ctx, field)
case "emailVerified":
return ec.fieldContext_Identity_emailVerified(ctx, field)
case "createdAt":
return ec.fieldContext_Identity_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_Identity_updatedAt(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Identity", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _Query_node(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Query_node(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
return graphql.ResolveField( return graphql.ResolveField(
ctx, ctx,
@@ -4132,6 +4504,35 @@ func (ec *executionContext) fieldContext_RequestAccessesPayload_trustCenterAcces
return fc, nil return fc, nil
} }
func (ec *executionContext) _SignInWithTokenPayload_success(ctx context.Context, field graphql.CollectedField, obj *types.SignInWithTokenPayload) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_SignInWithTokenPayload_success,
func(ctx context.Context) (any, error) {
return obj.Success, nil
},
nil,
ec.marshalNBoolean2bool,
true,
true,
)
}
func (ec *executionContext) fieldContext_SignInWithTokenPayload_success(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "SignInWithTokenPayload",
Field: field,
IsMethod: false,
IsResolver: false,
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_id(ctx context.Context, field graphql.CollectedField, obj *types.TrustCenter) (ret graphql.Marshaler) { func (ec *executionContext) _TrustCenter_id(ctx context.Context, field graphql.CollectedField, obj *types.TrustCenter) (ret graphql.Marshaler) {
return graphql.ResolveField( return graphql.ResolveField(
ctx, ctx,
@@ -7219,7 +7620,7 @@ func (ec *executionContext) unmarshalInputRequestAllAccessesInput(ctx context.Co
asMap[k] = v asMap[k] = v
} }
fieldsInOrder := [...]string{"trustCenterId", "email", "name"} fieldsInOrder := [...]string{"trustCenterId", "email", "fullName"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@@ -7235,18 +7636,18 @@ func (ec *executionContext) unmarshalInputRequestAllAccessesInput(ctx context.Co
it.TrustCenterID = data it.TrustCenterID = data
case "email": case "email":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email"))
data, err := ec.unmarshalOEmailAddr2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) data, err := ec.unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v)
if err != nil { if err != nil {
return it, err return it, err
} }
it.Email = data it.Email = data
case "name": case "fullName":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName"))
data, err := ec.unmarshalOString2ᚖstring(ctx, v) data, err := ec.unmarshalNString2string(ctx, v)
if err != nil { if err != nil {
return it, err return it, err
} }
it.Name = data it.FullName = data
} }
} }
@@ -7260,7 +7661,7 @@ func (ec *executionContext) unmarshalInputRequestDocumentAccessInput(ctx context
asMap[k] = v asMap[k] = v
} }
fieldsInOrder := [...]string{"trustCenterId", "documentId", "email", "name"} fieldsInOrder := [...]string{"trustCenterId", "documentId", "email", "fullName"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@@ -7283,18 +7684,18 @@ func (ec *executionContext) unmarshalInputRequestDocumentAccessInput(ctx context
it.DocumentID = data it.DocumentID = data
case "email": case "email":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email"))
data, err := ec.unmarshalOEmailAddr2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) data, err := ec.unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v)
if err != nil { if err != nil {
return it, err return it, err
} }
it.Email = data it.Email = data
case "name": case "fullName":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName"))
data, err := ec.unmarshalOString2ᚖstring(ctx, v) data, err := ec.unmarshalNString2string(ctx, v)
if err != nil { if err != nil {
return it, err return it, err
} }
it.Name = data it.FullName = data
} }
} }
@@ -7308,7 +7709,7 @@ func (ec *executionContext) unmarshalInputRequestReportAccessInput(ctx context.C
asMap[k] = v asMap[k] = v
} }
fieldsInOrder := [...]string{"trustCenterId", "reportId", "email", "name"} fieldsInOrder := [...]string{"trustCenterId", "reportId", "email", "fullName"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@@ -7331,18 +7732,18 @@ func (ec *executionContext) unmarshalInputRequestReportAccessInput(ctx context.C
it.ReportID = data it.ReportID = data
case "email": case "email":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email"))
data, err := ec.unmarshalOEmailAddr2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) data, err := ec.unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v)
if err != nil { if err != nil {
return it, err return it, err
} }
it.Email = data it.Email = data
case "name": case "fullName":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName"))
data, err := ec.unmarshalOString2ᚖstring(ctx, v) data, err := ec.unmarshalNString2string(ctx, v)
if err != nil { if err != nil {
return it, err return it, err
} }
it.Name = data it.FullName = data
} }
} }
@@ -7356,7 +7757,7 @@ func (ec *executionContext) unmarshalInputRequestTrustCenterFileAccessInput(ctx
asMap[k] = v asMap[k] = v
} }
fieldsInOrder := [...]string{"trustCenterId", "trustCenterFileId", "email", "name"} fieldsInOrder := [...]string{"trustCenterId", "trustCenterFileId", "email", "fullName"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@@ -7379,18 +7780,45 @@ func (ec *executionContext) unmarshalInputRequestTrustCenterFileAccessInput(ctx
it.TrustCenterFileID = data it.TrustCenterFileID = data
case "email": case "email":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email"))
data, err := ec.unmarshalOEmailAddr2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v) data, err := ec.unmarshalNEmailAddr2goᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx, v)
if err != nil { if err != nil {
return it, err return it, err
} }
it.Email = data it.Email = data
case "name": case "fullName":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("fullName"))
data, err := ec.unmarshalOString2ᚖstring(ctx, v) data, err := ec.unmarshalNString2string(ctx, v)
if err != nil { if err != nil {
return it, err return it, err
} }
it.Name = data it.FullName = data
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputSignInWithTokenInput(ctx context.Context, obj any) (types.SignInWithTokenInput, error) {
var it types.SignInWithTokenInput
asMap := map[string]any{}
for k, v := range obj.(map[string]any) {
asMap[k] = v
}
fieldsInOrder := [...]string{"token"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
case "token":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("token"))
data, err := ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
it.Token = data
} }
} }
@@ -7454,6 +7882,13 @@ func (ec *executionContext) _Node(ctx context.Context, sel ast.SelectionSet, obj
return graphql.Null return graphql.Null
} }
return ec._Organization(ctx, sel, obj) return ec._Organization(ctx, sel, obj)
case types.Identity:
return ec._Identity(ctx, sel, &obj)
case *types.Identity:
if obj == nil {
return graphql.Null
}
return ec._Identity(ctx, sel, obj)
case types.Framework: case types.Framework:
return ec._Framework(ctx, sel, &obj) return ec._Framework(ctx, sel, &obj)
case *types.Framework: case *types.Framework:
@@ -8155,6 +8590,70 @@ func (ec *executionContext) _Framework(ctx context.Context, sel ast.SelectionSet
return out return out
} }
var identityImplementors = []string{"Identity", "Node"}
func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, obj *types.Identity) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, identityImplementors)
out := graphql.NewFieldSet(fields)
deferred := make(map[string]*graphql.FieldSet)
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("Identity")
case "id":
out.Values[i] = ec._Identity_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "email":
out.Values[i] = ec._Identity_email(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "fullName":
out.Values[i] = ec._Identity_fullName(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "emailVerified":
out.Values[i] = ec._Identity_emailVerified(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "createdAt":
out.Values[i] = ec._Identity_createdAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "updatedAt":
out.Values[i] = ec._Identity_updatedAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch(ctx)
if out.Invalids > 0 {
return graphql.Null
}
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
for label, dfs := range deferred {
ec.processDeferredGroup(graphql.DeferredGroup{
Label: label,
Path: graphql.GetPath(ctx),
FieldSet: dfs,
Context: ctx,
})
}
return out
}
var mutationImplementors = []string{"Mutation"} var mutationImplementors = []string{"Mutation"}
func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
@@ -8174,6 +8673,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
switch field.Name { switch field.Name {
case "__typename": case "__typename":
out.Values[i] = graphql.MarshalString("Mutation") out.Values[i] = graphql.MarshalString("Mutation")
case "signInWithToken":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_signInWithToken(ctx, field)
})
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "requestAllAccesses": case "requestAllAccesses":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_requestAllAccesses(ctx, field) return ec._Mutation_requestAllAccesses(ctx, field)
@@ -8405,6 +8911,25 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
switch field.Name { switch field.Name {
case "__typename": case "__typename":
out.Values[i] = graphql.MarshalString("Query") out.Values[i] = graphql.MarshalString("Query")
case "viewer":
field := field
innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Query_viewer(ctx, field)
return res
}
rrm := func(ctx context.Context) graphql.Marshaler {
return ec.OperationContext.RootResolverMiddleware(ctx,
func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
}
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
case "node": case "node":
field := field field := field
@@ -8651,6 +9176,45 @@ func (ec *executionContext) _RequestAccessesPayload(ctx context.Context, sel ast
return out return out
} }
var signInWithTokenPayloadImplementors = []string{"SignInWithTokenPayload"}
func (ec *executionContext) _SignInWithTokenPayload(ctx context.Context, sel ast.SelectionSet, obj *types.SignInWithTokenPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, signInWithTokenPayloadImplementors)
out := graphql.NewFieldSet(fields)
deferred := make(map[string]*graphql.FieldSet)
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("SignInWithTokenPayload")
case "success":
out.Values[i] = ec._SignInWithTokenPayload_success(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch(ctx)
if out.Invalids > 0 {
return graphql.Null
}
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
for label, dfs := range deferred {
ec.processDeferredGroup(graphql.DeferredGroup{
Label: label,
Path: graphql.GetPath(ctx),
FieldSet: dfs,
Context: ctx,
})
}
return out
}
var trustCenterImplementors = []string{"TrustCenter", "Node"} var trustCenterImplementors = []string{"TrustCenter", "Node"}
func (ec *executionContext) _TrustCenter(ctx context.Context, sel ast.SelectionSet, obj *types.TrustCenter) graphql.Marshaler { func (ec *executionContext) _TrustCenter(ctx context.Context, sel ast.SelectionSet, obj *types.TrustCenter) graphql.Marshaler {
@@ -10954,6 +11518,25 @@ func (ec *executionContext) unmarshalNRequestTrustCenterFileAccessInput2goᚗpro
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)
} }
func (ec *executionContext) unmarshalNSignInWithTokenInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSignInWithTokenInput(ctx context.Context, v any) (types.SignInWithTokenInput, error) {
res, err := ec.unmarshalInputSignInWithTokenInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNSignInWithTokenPayload2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSignInWithTokenPayload(ctx context.Context, sel ast.SelectionSet, v types.SignInWithTokenPayload) graphql.Marshaler {
return ec._SignInWithTokenPayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNSignInWithTokenPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSignInWithTokenPayload(ctx context.Context, sel ast.SelectionSet, v *types.SignInWithTokenPayload) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow")
}
return graphql.Null
}
return ec._SignInWithTokenPayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNString2string(ctx context.Context, v any) (string, error) { func (ec *executionContext) unmarshalNString2string(ctx context.Context, v any) (string, error) {
res, err := graphql.UnmarshalString(v) res, err := graphql.UnmarshalString(v)
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)
@@ -11583,22 +12166,11 @@ func (ec *executionContext) marshalOCursorKey2ᚖgoᚗproboᚗincᚋproboᚋpkg
return res return res
} }
func (ec *executionContext) unmarshalOEmailAddr2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx context.Context, v any) (*mail.Addr, error) { func (ec *executionContext) marshalOIdentity2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐIdentity(ctx context.Context, sel ast.SelectionSet, v *types.Identity) graphql.Marshaler {
if v == nil {
return nil, nil
}
res, err := mail1.UnmarshalAddrScalar(v)
return &res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalOEmailAddr2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋmailᚐAddr(ctx context.Context, sel ast.SelectionSet, v *mail.Addr) graphql.Marshaler {
if v == nil { if v == nil {
return graphql.Null return graphql.Null
} }
_ = sel return ec._Identity(ctx, sel, v)
_ = ctx
res := mail1.MarshalAddrScalar(*v)
return res
} }
func (ec *executionContext) unmarshalOInt2ᚖint(ctx context.Context, v any) (*int, error) { func (ec *executionContext) unmarshalOInt2ᚖint(ctx context.Context, v any) (*int, error) {

View File

@@ -1,139 +0,0 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package trust_v1
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"go.gearno.de/kit/httpserver"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/statelesstoken"
"go.probo.inc/probo/pkg/trust"
)
type ctxKey struct {
name string
}
var (
CustomDomainOrganizationIDKey = &ctxKey{name: "custom_domain_organization_id"}
)
func GetCustomDomainOrganizationID(ctx context.Context) (gid.GID, bool) {
organizationID, ok := ctx.Value(CustomDomainOrganizationIDKey).(gid.GID)
return organizationID, ok
}
type (
AuthTokenRequest struct {
Token string `json:"token"`
}
AuthTokenResponse struct {
Success bool `json:"success"`
TrustCenterID string `json:"trust_center_id,omitempty"`
Message string `json:"message,omitempty"`
}
)
func authTokenHandler(trustSvc *trust.Service, trustAuthCfg TrustAuthConfig) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req AuthTokenRequest
// Limit request body size to 1KB to prevent DoS attacks
limitedReader := http.MaxBytesReader(w, r.Body, 1024)
if err := json.NewDecoder(limitedReader).Decode(&req); err != nil {
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err))
return
}
if req.Token == "" {
httpserver.RenderJSON(w, http.StatusBadRequest, AuthTokenResponse{
Success: false,
Message: "Token is required",
})
return
}
accessData, err := validateTrustCenterAccessToken(r.Context(), trustSvc, trustAuthCfg, req.Token)
if err != nil {
httpserver.RenderJSON(w, http.StatusUnauthorized, AuthTokenResponse{
Success: false,
Message: "Invalid or expired token",
})
return
}
tokenString, err := statelesstoken.NewToken(
trustAuthCfg.TokenSecret,
trustAuthCfg.TokenType,
trustAuthCfg.TokenDuration,
*accessData,
)
if err != nil {
httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("cannot create token: %w", err))
return
}
// Determine cookie domain: use custom domain if present, otherwise use configured domain
cookieDomain := trustAuthCfg.CookieDomain
if _, ok := GetCustomDomainOrganizationID(r.Context()); ok {
// On custom domain, use the request host
if r.TLS != nil && r.TLS.ServerName != "" {
cookieDomain = r.TLS.ServerName
}
}
cookie := &http.Cookie{
Name: trustAuthCfg.CookieName,
Value: tokenString,
Domain: cookieDomain,
Path: "/",
MaxAge: int(trustAuthCfg.CookieDuration / time.Second),
Secure: trustAuthCfg.CookieSecure,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
}
http.SetCookie(w, cookie)
httpserver.RenderJSON(w, http.StatusOK, AuthTokenResponse{
Success: true,
TrustCenterID: accessData.TrustCenterID.String(),
Message: "Authentication successful",
})
}
}
func validateTrustCenterAccessToken(ctx context.Context, trustSvc *trust.Service, trustAuthCfg TrustAuthConfig, tokenString string) (*probo.TrustCenterAccessData, error) {
token, err := statelesstoken.ValidateToken[probo.TrustCenterAccessData](
trustSvc.GetTokenSecret(),
trustAuthCfg.TokenType,
tokenString,
)
if err != nil {
return nil, fmt.Errorf("cannot validate trust center access token: %w", err)
}
tenantSvc := trustSvc.WithTenant(token.Data.TrustCenterID.TenantID())
if err := tenantSvc.TrustCenterAccesses.ValidateToken(ctx, token.Data.TrustCenterID, token.Data.Email); err != nil {
return nil, fmt.Errorf("cannot validate trust center access token: %w", err)
}
return &token.Data, nil
}

View File

@@ -102,6 +102,18 @@ type Framework struct {
func (Framework) IsNode() {} func (Framework) IsNode() {}
func (this Framework) GetID() gid.GID { return this.ID } func (this Framework) GetID() gid.GID { return this.ID }
type Identity struct {
ID gid.GID `json:"id"`
Email mail.Addr `json:"email"`
FullName string `json:"fullName"`
EmailVerified bool `json:"emailVerified"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (Identity) IsNode() {}
func (this Identity) GetID() gid.GID { return this.ID }
type Mutation struct { type Mutation struct {
} }
@@ -143,30 +155,38 @@ type RequestAccessesPayload struct {
} }
type RequestAllAccessesInput struct { type RequestAllAccessesInput struct {
TrustCenterID gid.GID `json:"trustCenterId"` TrustCenterID gid.GID `json:"trustCenterId"`
Email *mail.Addr `json:"email,omitempty"` Email mail.Addr `json:"email"`
Name *string `json:"name,omitempty"` FullName string `json:"fullName"`
} }
type RequestDocumentAccessInput struct { type RequestDocumentAccessInput struct {
TrustCenterID gid.GID `json:"trustCenterId"` TrustCenterID gid.GID `json:"trustCenterId"`
DocumentID gid.GID `json:"documentId"` DocumentID gid.GID `json:"documentId"`
Email *mail.Addr `json:"email,omitempty"` Email mail.Addr `json:"email"`
Name *string `json:"name,omitempty"` FullName string `json:"fullName"`
} }
type RequestReportAccessInput struct { type RequestReportAccessInput struct {
TrustCenterID gid.GID `json:"trustCenterId"` TrustCenterID gid.GID `json:"trustCenterId"`
ReportID gid.GID `json:"reportId"` ReportID gid.GID `json:"reportId"`
Email *mail.Addr `json:"email,omitempty"` Email mail.Addr `json:"email"`
Name *string `json:"name,omitempty"` FullName string `json:"fullName"`
} }
type RequestTrustCenterFileAccessInput struct { type RequestTrustCenterFileAccessInput struct {
TrustCenterID gid.GID `json:"trustCenterId"` TrustCenterID gid.GID `json:"trustCenterId"`
TrustCenterFileID gid.GID `json:"trustCenterFileId"` TrustCenterFileID gid.GID `json:"trustCenterFileId"`
Email *mail.Addr `json:"email,omitempty"` Email mail.Addr `json:"email"`
Name *string `json:"name,omitempty"` FullName string `json:"fullName"`
}
type SignInWithTokenInput struct {
Token string `json:"token"`
}
type SignInWithTokenPayload struct {
Success bool `json:"success"`
} }
type TrustCenter struct { type TrustCenter struct {

File diff suppressed because it is too large Load Diff

View File

@@ -58,6 +58,10 @@ func Forbidden(ctx context.Context, err error) *gqlerror.Error {
} }
} }
func Forbiddenf(ctx context.Context, format string, a ...any) *gqlerror.Error {
return Forbidden(ctx, fmt.Errorf(format, a...))
}
func NotFound(ctx context.Context, err error) *gqlerror.Error { func NotFound(ctx context.Context, err error) *gqlerror.Error {
return &gqlerror.Error{ return &gqlerror.Error{
Message: err.Error(), Message: err.Error(),
@@ -68,6 +72,10 @@ func NotFound(ctx context.Context, err error) *gqlerror.Error {
} }
} }
func NotFoundf(ctx context.Context, format string, a ...any) *gqlerror.Error {
return NotFound(ctx, fmt.Errorf(format, a...))
}
func Conflict(ctx context.Context, err error) *gqlerror.Error { func Conflict(ctx context.Context, err error) *gqlerror.Error {
return &gqlerror.Error{ return &gqlerror.Error{
Message: err.Error(), Message: err.Error(),

View File

@@ -15,7 +15,6 @@
package server package server
import ( import (
"context"
"errors" "errors"
"net/http" "net/http"
"strings" "strings"
@@ -193,7 +192,7 @@ func (s *Server) loadTrustCenterBySlugOrID(next http.Handler) http.Handler {
) )
} }
ctx = s.addTrustCenterToContext(ctx, trustCenter.ID.TenantID(), trustCenter.OrganizationID) ctx = trust_v1.ContextWithTrustCenter(ctx, *trustCenter)
next.ServeHTTP(w, r.WithContext(ctx)) next.ServeHTTP(w, r.WithContext(ctx))
}) })
} }
@@ -235,16 +234,20 @@ func (s *Server) loadTrustCenterByDomain(next http.Handler) http.Handler {
log.String("organization_id", organizationID.String()), log.String("organization_id", organizationID.String()),
) )
ctx = s.addTrustCenterToContext(ctx, organizationID.TenantID(), organizationID) trustCenter, err := s.proboService.LoadTrustCenterByOrganizationID(ctx, organizationID)
if err != nil {
s.logger.WarnCtx(ctx, "trust center not found",
log.Error(err),
)
http.Error(w, "Trust center not found", http.StatusNotFound)
return
}
ctx = trust_v1.ContextWithTrustCenter(ctx, *trustCenter)
next.ServeHTTP(w, r.WithContext(ctx)) next.ServeHTTP(w, r.WithContext(ctx))
}) })
} }
func (s *Server) addTrustCenterToContext(ctx context.Context, tenantID, organizationID interface{}) context.Context {
ctx = context.WithValue(ctx, trust_v1.CustomDomainOrganizationIDKey, organizationID)
return ctx
}
func (s *Server) stripTrustPrefix(next http.Handler) http.Handler { func (s *Server) stripTrustPrefix(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
slugOrId := chi.URLParam(r, "slugOrId") slugOrId := chi.URLParam(r, "slugOrId")

View File

@@ -44,7 +44,7 @@ type (
TrustCenterAccessRequest struct { TrustCenterAccessRequest struct {
TrustCenterID gid.GID TrustCenterID gid.GID
Email mail.Addr Email mail.Addr
Name string FullName string
DocumentIDs []gid.GID DocumentIDs []gid.GID
ReportIDs []gid.GID ReportIDs []gid.GID
TrustCenterFileIDs []gid.GID TrustCenterFileIDs []gid.GID
@@ -63,26 +63,6 @@ func (tcar *TrustCenterAccessRequest) Validate() error {
return v.Error() return v.Error()
} }
func (s TrustCenterAccessService) ValidateToken(
ctx context.Context,
trustCenterID gid.GID,
email mail.Addr,
) error {
return s.svc.pg.WithConn(ctx, func(conn pg.Conn) error {
access := &coredata.TrustCenterAccess{}
err := access.LoadByTrustCenterIDAndEmail(ctx, conn, s.svc.scope, trustCenterID, email)
if err != nil {
return fmt.Errorf("cannot load trust center access: %w", err)
}
if !access.Active {
return fmt.Errorf("trust center access is not active")
}
return nil
})
}
func (s TrustCenterAccessService) Request( func (s TrustCenterAccessService) Request(
ctx context.Context, ctx context.Context,
req *TrustCenterAccessRequest, req *TrustCenterAccessRequest,
@@ -170,7 +150,7 @@ func (s TrustCenterAccessService) Request(
TenantID: s.svc.scope.GetTenantID(), TenantID: s.svc.scope.GetTenantID(),
TrustCenterID: req.TrustCenterID, TrustCenterID: req.TrustCenterID,
Email: req.Email, Email: req.Email,
Name: req.Name, Name: req.FullName,
Active: false, Active: false,
HasAcceptedNonDisclosureAgreement: false, HasAcceptedNonDisclosureAgreement: false,
CreatedAt: now, CreatedAt: now,