Add compliance page mailing list base

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-02-25 14:59:00 +01:00
parent c0a69100e8
commit 4d2cb793b6
50 changed files with 6365 additions and 110 deletions

View File

@@ -0,0 +1,132 @@
/**
* @generated SignedSource<<72b5471e12ed01f059b4192ee5827a1a>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type DeleteMailingListSubscriberInput = {
id: string;
};
export type CompliancePageMailingListDeleteMutation$variables = {
connections: ReadonlyArray<string>;
input: DeleteMailingListSubscriberInput;
};
export type CompliancePageMailingListDeleteMutation$data = {
readonly deleteMailingListSubscriber: {
readonly deletedMailingListSubscriberId: string;
};
};
export type CompliancePageMailingListDeleteMutation = {
response: CompliancePageMailingListDeleteMutation$data;
variables: CompliancePageMailingListDeleteMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "connections"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
},
v2 = [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "deletedMailingListSubscriberId",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "CompliancePageMailingListDeleteMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "DeleteMailingListSubscriberPayload",
"kind": "LinkedField",
"name": "deleteMailingListSubscriber",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "CompliancePageMailingListDeleteMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "DeleteMailingListSubscriberPayload",
"kind": "LinkedField",
"name": "deleteMailingListSubscriber",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "deleteEdge",
"key": "",
"kind": "ScalarHandle",
"name": "deletedMailingListSubscriberId",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "65de43d2f00cd2c553aa72568f81eac2",
"id": null,
"metadata": {},
"name": "CompliancePageMailingListDeleteMutation",
"operationKind": "mutation",
"text": "mutation CompliancePageMailingListDeleteMutation(\n $input: DeleteMailingListSubscriberInput!\n) {\n deleteMailingListSubscriber(input: $input) {\n deletedMailingListSubscriberId\n }\n}\n"
}
};
})();
(node as any).hash = "bb627bfeb68e7b72c337c40133ab63a8";
export default node;

View File

@@ -0,0 +1,234 @@
/**
* @generated SignedSource<<e9dd50980011bde2eb30f8c1f7fd5344>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type MailingListSubscriberStatus = "CONFIRMED" | "PENDING";
import { FragmentRefs } from "relay-runtime";
export type CompliancePageMailingListFragment$data = {
readonly id: string;
readonly mailingList: {
readonly id: string;
readonly subscribers: {
readonly __id: string;
readonly edges: ReadonlyArray<{
readonly node: {
readonly createdAt: string;
readonly email: string;
readonly fullName: string;
readonly id: string;
readonly status: MailingListSubscriberStatus;
};
}>;
readonly pageInfo: {
readonly endCursor: string | null | undefined;
readonly hasNextPage: boolean;
};
};
} | null | undefined;
readonly " $fragmentType": "CompliancePageMailingListFragment";
};
export type CompliancePageMailingListFragment$key = {
readonly " $data"?: CompliancePageMailingListFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"CompliancePageMailingListFragment">;
};
import CompliancePageMailingListQuery_graphql from './CompliancePageMailingListQuery.graphql';
const node: ReaderFragment = (function(){
var v0 = [
"mailingList",
"subscribers"
],
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"argumentDefinitions": [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "after"
},
{
"defaultValue": 20,
"kind": "LocalArgument",
"name": "first"
}
],
"kind": "Fragment",
"metadata": {
"connection": [
{
"count": "first",
"cursor": "after",
"direction": "forward",
"path": (v0/*: any*/)
}
],
"refetch": {
"connection": {
"forward": {
"count": "first",
"cursor": "after"
},
"backward": null,
"path": (v0/*: any*/)
},
"fragmentPathInResult": [
"node"
],
"operation": CompliancePageMailingListQuery_graphql,
"identifierInfo": {
"identifierField": "id",
"identifierQueryVariableName": "id"
}
}
},
"name": "CompliancePageMailingListFragment",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "MailingList",
"kind": "LinkedField",
"name": "mailingList",
"plural": false,
"selections": [
(v1/*: any*/),
{
"alias": "subscribers",
"args": null,
"concreteType": "MailingListSubscriberConnection",
"kind": "LinkedField",
"name": "__CompliancePageMailingList_subscribers_connection",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "MailingListSubscriberEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "MailingListSubscriber",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v1/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
"storageKey": null
}
],
"storageKey": null
},
(v1/*: any*/)
],
"type": "TrustCenter",
"abstractKey": null
};
})();
(node as any).hash = "b1828ed7ded38bb3e3e150b0875ea0a6";
export default node;

View File

@@ -0,0 +1,327 @@
/**
* @generated SignedSource<<7e30e59b53799ccae49199d7b46f0320>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type CompliancePageMailingListPageQuery$variables = {
organizationId: string;
};
export type CompliancePageMailingListPageQuery$data = {
readonly organization: {
readonly __typename: "Organization";
readonly compliancePage: {
readonly id: string;
readonly mailingList: {
readonly id: string;
readonly replyTo: string | null | undefined;
} | null | undefined;
readonly " $fragmentSpreads": FragmentRefs<"CompliancePageMailingListFragment">;
};
} | {
// This will never be '%other', but we need some
// value in case none of the concrete values match.
readonly __typename: "%other";
};
};
export type CompliancePageMailingListPageQuery = {
response: CompliancePageMailingListPageQuery$data;
variables: CompliancePageMailingListPageQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "replyTo",
"storageKey": null
},
v5 = [
{
"kind": "Literal",
"name": "first",
"value": 20
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "CompliancePageMailingListPageQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": "compliancePage",
"args": null,
"concreteType": "TrustCenter",
"kind": "LinkedField",
"name": "trustCenter",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "MailingList",
"kind": "LinkedField",
"name": "mailingList",
"plural": false,
"selections": [
(v3/*: any*/),
(v4/*: any*/)
],
"storageKey": null
},
{
"args": null,
"kind": "FragmentSpread",
"name": "CompliancePageMailingListFragment"
}
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "CompliancePageMailingListPageQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": "compliancePage",
"args": null,
"concreteType": "TrustCenter",
"kind": "LinkedField",
"name": "trustCenter",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "MailingList",
"kind": "LinkedField",
"name": "mailingList",
"plural": false,
"selections": [
(v3/*: any*/),
(v4/*: any*/),
{
"alias": null,
"args": (v5/*: any*/),
"concreteType": "MailingListSubscriberConnection",
"kind": "LinkedField",
"name": "subscribers",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "MailingListSubscriberEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "MailingListSubscriber",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
(v2/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
"storageKey": "subscribers(first:20)"
},
{
"alias": null,
"args": (v5/*: any*/),
"filters": null,
"handle": "connection",
"key": "CompliancePageMailingList_subscribers",
"kind": "LinkedHandle",
"name": "subscribers"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"type": "Organization",
"abstractKey": null
},
(v3/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "8eb57a5cf8f8b1294da6aee3228176f3",
"id": null,
"metadata": {},
"name": "CompliancePageMailingListPageQuery",
"operationKind": "query",
"text": "query CompliancePageMailingListPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n compliancePage: trustCenter {\n id\n mailingList {\n id\n replyTo\n }\n ...CompliancePageMailingListFragment\n }\n }\n id\n }\n}\n\nfragment CompliancePageMailingListFragment on TrustCenter {\n mailingList {\n id\n subscribers(first: 20) {\n pageInfo {\n hasNextPage\n endCursor\n }\n edges {\n node {\n id\n fullName\n email\n status\n createdAt\n __typename\n }\n cursor\n }\n }\n }\n id\n}\n"
}
};
})();
(node as any).hash = "0df7b2a8b9f0d2f421f354bd56773e43";
export default node;

View File

@@ -0,0 +1,114 @@
/**
* @generated SignedSource<<c5e3b54834d5bcf46746de91d32e964a>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type UpdateMailingListInput = {
id: string;
replyTo?: string | null | undefined;
};
export type CompliancePageMailingListPage_updateMailingListMutation$variables = {
input: UpdateMailingListInput;
};
export type CompliancePageMailingListPage_updateMailingListMutation$data = {
readonly updateMailingList: {
readonly mailingList: {
readonly id: string;
readonly replyTo: string | null | undefined;
};
};
};
export type CompliancePageMailingListPage_updateMailingListMutation = {
response: CompliancePageMailingListPage_updateMailingListMutation$data;
variables: CompliancePageMailingListPage_updateMailingListMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "UpdateMailingListPayload",
"kind": "LinkedField",
"name": "updateMailingList",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "MailingList",
"kind": "LinkedField",
"name": "mailingList",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "replyTo",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "CompliancePageMailingListPage_updateMailingListMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "CompliancePageMailingListPage_updateMailingListMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "30b67540b107297075df95ee044714ea",
"id": null,
"metadata": {},
"name": "CompliancePageMailingListPage_updateMailingListMutation",
"operationKind": "mutation",
"text": "mutation CompliancePageMailingListPage_updateMailingListMutation(\n $input: UpdateMailingListInput!\n) {\n updateMailingList(input: $input) {\n mailingList {\n id\n replyTo\n }\n }\n}\n"
}
};
})();
(node as any).hash = "0a1e6524623210147ba5db2affec456c";
export default node;

View File

@@ -0,0 +1,275 @@
/**
* @generated SignedSource<<88648e469580db301e2d4105dae6a072>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type CompliancePageMailingListQuery$variables = {
after?: string | null | undefined;
first?: number | null | undefined;
id: string;
};
export type CompliancePageMailingListQuery$data = {
readonly node: {
readonly " $fragmentSpreads": FragmentRefs<"CompliancePageMailingListFragment">;
};
};
export type CompliancePageMailingListQuery = {
response: CompliancePageMailingListQuery$data;
variables: CompliancePageMailingListQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "after"
},
{
"defaultValue": 20,
"kind": "LocalArgument",
"name": "first"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "id"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "id"
}
],
v2 = [
{
"kind": "Variable",
"name": "after",
"variableName": "after"
},
{
"kind": "Variable",
"name": "first",
"variableName": "first"
}
],
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "CompliancePageMailingListQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"args": (v2/*: any*/),
"kind": "FragmentSpread",
"name": "CompliancePageMailingListFragment"
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "CompliancePageMailingListQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
(v4/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "MailingList",
"kind": "LinkedField",
"name": "mailingList",
"plural": false,
"selections": [
(v4/*: any*/),
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "MailingListSubscriberConnection",
"kind": "LinkedField",
"name": "subscribers",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "MailingListSubscriberEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "MailingListSubscriber",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v4/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
(v3/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
"storageKey": null
},
{
"alias": null,
"args": (v2/*: any*/),
"filters": null,
"handle": "connection",
"key": "CompliancePageMailingList_subscribers",
"kind": "LinkedHandle",
"name": "subscribers"
}
],
"storageKey": null
}
],
"type": "TrustCenter",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "409500a631f678d3b173a2feae512f71",
"id": null,
"metadata": {},
"name": "CompliancePageMailingListQuery",
"operationKind": "query",
"text": "query CompliancePageMailingListQuery(\n $after: CursorKey = null\n $first: Int = 20\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...CompliancePageMailingListFragment_2HEEH6\n id\n }\n}\n\nfragment CompliancePageMailingListFragment_2HEEH6 on TrustCenter {\n mailingList {\n id\n subscribers(first: $first, after: $after) {\n pageInfo {\n hasNextPage\n endCursor\n }\n edges {\n node {\n id\n fullName\n email\n status\n createdAt\n __typename\n }\n cursor\n }\n }\n }\n id\n}\n"
}
};
})();
(node as any).hash = "b1828ed7ded38bb3e3e150b0875ea0a6";
export default node;

View File

@@ -0,0 +1,201 @@
/**
* @generated SignedSource<<61ab320d87e6fb43d245dc070871a160>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type MailingListSubscriberStatus = "CONFIRMED" | "PENDING";
export type CreateMailingListSubscriberInput = {
email: string;
fullName: string;
mailingListId: string;
};
export type NewCompliancePageSubscriberDialogMutation$variables = {
connections: ReadonlyArray<string>;
input: CreateMailingListSubscriberInput;
};
export type NewCompliancePageSubscriberDialogMutation$data = {
readonly createMailingListSubscriber: {
readonly mailingListSubscriberEdge: {
readonly cursor: string;
readonly node: {
readonly createdAt: string;
readonly email: string;
readonly fullName: string;
readonly id: string;
readonly status: MailingListSubscriberStatus;
};
};
};
};
export type NewCompliancePageSubscriberDialogMutation = {
response: NewCompliancePageSubscriberDialogMutation$data;
variables: NewCompliancePageSubscriberDialogMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "connections"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
},
v2 = [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
v3 = {
"alias": null,
"args": null,
"concreteType": "MailingListSubscriberEdge",
"kind": "LinkedField",
"name": "mailingListSubscriberEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "MailingListSubscriber",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "NewCompliancePageSubscriberDialogMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateMailingListSubscriberPayload",
"kind": "LinkedField",
"name": "createMailingListSubscriber",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "NewCompliancePageSubscriberDialogMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateMailingListSubscriberPayload",
"kind": "LinkedField",
"name": "createMailingListSubscriber",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "prependEdge",
"key": "",
"kind": "LinkedHandle",
"name": "mailingListSubscriberEdge",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "6029b34056e1e0477db8bec0080b4ad1",
"id": null,
"metadata": {},
"name": "NewCompliancePageSubscriberDialogMutation",
"operationKind": "mutation",
"text": "mutation NewCompliancePageSubscriberDialogMutation(\n $input: CreateMailingListSubscriberInput!\n) {\n createMailingListSubscriber(input: $input) {\n mailingListSubscriberEdge {\n cursor\n node {\n id\n fullName\n email\n status\n createdAt\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "fae262a9df20589ff2923f631692a6ce";
export default node;

View File

@@ -1,6 +1,6 @@
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Badge, IconCheckmark1, IconFolder2, IconMedal, IconPageTextLine, IconPencil, IconPeopleAdd, IconSettingsGear2, IconStore, PageHeader, TabLink, Tabs } from "@probo/ui";
import { Badge, IconBell2, IconCheckmark1, IconFolder2, IconMedal, IconPageTextLine, IconPencil, IconPeopleAdd, IconSettingsGear2, IconStore, PageHeader, TabLink, Tabs } from "@probo/ui";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { Outlet } from "react-router";
import { graphql } from "relay-runtime";
@@ -84,6 +84,10 @@ export function CompliancePageLayout(props: { queryRef: PreloadedQuery<Complianc
<IconPeopleAdd className="size-4" />
{__("Access")}
</TabLink>
<TabLink to={`/organizations/${organizationId}/compliance-page/mailing-list`}>
<IconBell2 className="size-4" />
{__("Mailing List")}
</TabLink>
</Tabs>
<Outlet />

View File

@@ -0,0 +1,147 @@
import { useTranslate } from "@probo/i18n";
import { Button, Card, Field, IconPlusLarge, Spinner, useDialogRef } from "@probo/ui";
import { useState } from "react";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { ConnectionHandler, graphql } from "relay-runtime";
import type { CompliancePageMailingListPage_updateMailingListMutation } from "#/__generated__/core/CompliancePageMailingListPage_updateMailingListMutation.graphql";
import type { CompliancePageMailingListPageQuery } from "#/__generated__/core/CompliancePageMailingListPageQuery.graphql";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { CompliancePageMailingList } from "./_components/CompliancePageMailingList";
import { NewCompliancePageSubscriberDialog } from "./_components/NewCompliancePageSubscriberDialog";
export const compliancePageMailingListPageQuery = graphql`
query CompliancePageMailingListPageQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
__typename
... on Organization {
compliancePage: trustCenter @required(action: THROW) {
id
mailingList {
id
replyTo
}
...CompliancePageMailingListFragment
}
}
}
}
`;
const updateMailingListMutation = graphql`
mutation CompliancePageMailingListPage_updateMailingListMutation($input: UpdateMailingListInput!) {
updateMailingList(input: $input) {
mailingList {
id
replyTo
}
}
}
`;
export function CompliancePageMailingListPage(props: {
queryRef: PreloadedQuery<CompliancePageMailingListPageQuery>;
}) {
const { queryRef } = props;
const { __ } = useTranslate();
const dialogRef = useDialogRef();
const { organization } = usePreloadedQuery<CompliancePageMailingListPageQuery>(
compliancePageMailingListPageQuery,
queryRef,
);
if (organization.__typename !== "Organization") {
throw new Error("invalid type for node");
}
const mailingList = organization.compliancePage.mailingList;
const mailingListId = mailingList?.id;
const connectionId = mailingListId
? ConnectionHandler.getConnectionID(mailingListId, "CompliancePageMailingList_subscribers")
: null;
const [replyTo, setReplyTo] = useState(mailingList?.replyTo ?? "");
const [updateMailingList, isUpdating] = useMutationWithToasts<CompliancePageMailingListPage_updateMailingListMutation>(
updateMailingListMutation,
{
successMessage: __("Mailing list updated successfully"),
errorMessage: __("Failed to update mailing list"),
},
);
const handleSaveReplyTo = () => {
if (!mailingListId) return;
void updateMailingList({
variables: {
input: {
id: mailingListId,
replyTo: replyTo.trim() || null,
},
},
});
};
return (
<div className="space-y-6">
{mailingListId && (
<Card className="p-6 space-y-4">
<div>
<h3 className="text-base font-medium">{__("Settings")}</h3>
<p className="text-sm text-txt-tertiary">
{__("Configure how your mailing list behaves")}
</p>
</div>
<div className="flex items-end gap-3">
<div className="flex-1">
<Field
label={__("Reply-to email")}
type="email"
placeholder={__("security@example.com")}
value={replyTo}
onChange={e => setReplyTo(e.target.value)}
/>
</div>
<Button
onClick={handleSaveReplyTo}
disabled={isUpdating}
className="shrink-0"
>
{isUpdating && <Spinner />}
{__("Save")}
</Button>
</div>
</Card>
)}
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-base font-medium">{__("Subscribers")}</h3>
<p className="text-sm text-txt-tertiary">
{__("People subscribed to receive security and compliance updates")}
</p>
</div>
{mailingListId && (
<Button icon={IconPlusLarge} onClick={() => dialogRef.current?.open()}>
{__("Add Subscriber")}
</Button>
)}
</div>
<CompliancePageMailingList fragmentRef={organization.compliancePage} />
</div>
{mailingListId && connectionId && (
<NewCompliancePageSubscriberDialog
ref={dialogRef}
mailingListId={mailingListId}
connectionId={connectionId}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,37 @@
import { useEffect } from "react";
import { useQueryLoader } from "react-relay";
import type { CompliancePageMailingListPageQuery } from "#/__generated__/core/CompliancePageMailingListPageQuery.graphql";
import { LinkCardSkeleton } from "#/components/skeletons/LinkCardSkeleton";
import { useOrganizationId } from "#/hooks/useOrganizationId";
import { CoreRelayProvider } from "#/providers/CoreRelayProvider";
import {
CompliancePageMailingListPage,
compliancePageMailingListPageQuery,
} from "./CompliancePageMailingListPage";
function CompliancePageMailingListPageQueryLoader() {
const organizationId = useOrganizationId();
const [queryRef, loadQuery] = useQueryLoader<CompliancePageMailingListPageQuery>(
compliancePageMailingListPageQuery,
);
useEffect(() => {
if (!queryRef) {
loadQuery({ organizationId });
}
});
if (!queryRef) return <LinkCardSkeleton />;
return <CompliancePageMailingListPage queryRef={queryRef} />;
}
export default function CompliancePageMailingListPageLoader() {
return (
<CoreRelayProvider>
<CompliancePageMailingListPageQueryLoader />
</CoreRelayProvider>
);
}

View File

@@ -0,0 +1,160 @@
import { useTranslate } from "@probo/i18n";
import { Badge, Button, IconChevronDown, IconTrashCan, Spinner, Table, Tbody, Td, Th, Thead, Tr } from "@probo/ui";
import { usePaginationFragment } from "react-relay";
import { graphql } from "relay-runtime";
import type { CompliancePageMailingListDeleteMutation } from "#/__generated__/core/CompliancePageMailingListDeleteMutation.graphql";
import type { CompliancePageMailingListFragment$key } from "#/__generated__/core/CompliancePageMailingListFragment.graphql";
import type { CompliancePageMailingListQuery } from "#/__generated__/core/CompliancePageMailingListQuery.graphql";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
const deleteMutation = graphql`
mutation CompliancePageMailingListDeleteMutation(
$input: DeleteMailingListSubscriberInput!
$connections: [ID!]!
) {
deleteMailingListSubscriber(input: $input) {
deletedMailingListSubscriberId @deleteEdge(connections: $connections)
}
}
`;
const fragment = graphql`
fragment CompliancePageMailingListFragment on TrustCenter
@argumentDefinitions(
first: { type: Int, defaultValue: 20 }
after: { type: CursorKey, defaultValue: null }
)
@refetchable(queryName: "CompliancePageMailingListQuery") {
mailingList {
id
subscribers(
first: $first
after: $after
) @connection(key: "CompliancePageMailingList_subscribers") {
__id
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
fullName
email
status
createdAt
}
}
}
}
}
`;
export function CompliancePageMailingList(props: {
fragmentRef: CompliancePageMailingListFragment$key;
}) {
const { fragmentRef } = props;
const { __ } = useTranslate();
const {
data,
hasNext,
loadNext,
isLoadingNext,
} = usePaginationFragment<CompliancePageMailingListQuery, CompliancePageMailingListFragment$key>(
fragment,
fragmentRef,
);
const subscribers = data.mailingList?.subscribers;
const [deleteSubscriber, isDeleting] = useMutationWithToasts<CompliancePageMailingListDeleteMutation>(
deleteMutation,
{
successMessage: __("Subscriber removed successfully"),
errorMessage: __("Failed to delete subscriber"),
},
);
const handleDelete = (id: string) => {
if (!subscribers) return;
void deleteSubscriber({
variables: {
input: { id },
connections: [subscribers.__id],
},
});
};
return (
<>
{!subscribers || subscribers.edges.length === 0
? (
<Table>
<Tbody>
<Tr>
<Td className="text-center text-txt-tertiary py-8">
{__("No mailing list subscribers yet")}
</Td>
</Tr>
</Tbody>
</Table>
)
: (
<>
<Table>
<Thead>
<Tr>
<Th>{__("Name")}</Th>
<Th>{__("Email")}</Th>
<Th>{__("Status")}</Th>
<Th>{__("Subscribed on")}</Th>
<Th />
</Tr>
</Thead>
<Tbody>
{subscribers.edges.map(({ node: subscriber }) => (
<Tr key={subscriber.id}>
<Td>{subscriber.fullName}</Td>
<Td>{subscriber.email}</Td>
<Td>
<Badge
variant={subscriber.status === "CONFIRMED" ? "success" : "warning"}
>
{subscriber.status === "CONFIRMED" ? __("Confirmed") : __("Pending")}
</Badge>
</Td>
<Td className="text-txt-tertiary text-sm">
{new Date(subscriber.createdAt).toLocaleDateString()}
</Td>
<Td className="w-10">
<Button
variant="tertiary"
icon={IconTrashCan}
disabled={isDeleting}
onClick={() => handleDelete(subscriber.id)}
aria-label={__("Delete subscriber")}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
{hasNext && (
<Button
variant="tertiary"
onClick={() => loadNext(10)}
disabled={isLoadingNext}
className="mt-3 mx-auto"
icon={IconChevronDown}
>
{isLoadingNext && <Spinner />}
{__("Show More")}
</Button>
)}
</>
)}
</>
);
}

View File

@@ -0,0 +1,114 @@
import { useTranslate } from "@probo/i18n";
import { Button, Dialog, DialogContent, DialogFooter, type DialogRef, Field, Spinner } from "@probo/ui";
import { type DataID, graphql } from "relay-runtime";
import { z } from "zod";
import type { NewCompliancePageSubscriberDialogMutation } from "#/__generated__/core/NewCompliancePageSubscriberDialogMutation.graphql";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
const createSubscriberMutation = graphql`
mutation NewCompliancePageSubscriberDialogMutation(
$input: CreateMailingListSubscriberInput!
$connections: [ID!]!
) {
createMailingListSubscriber(input: $input) {
mailingListSubscriberEdge @prependEdge(connections: $connections) {
cursor
node {
id
fullName
email
status
createdAt
}
}
}
}
`;
export function NewCompliancePageSubscriberDialog(props: {
mailingListId: string;
connectionId: DataID;
ref: DialogRef;
}) {
const { mailingListId, connectionId, ref } = props;
const { __ } = useTranslate();
const schema = z.object({
fullName: z.string().min(1, __("Full name is required")).trim(),
email: z
.string()
.min(1, __("Email is required"))
.trim()
.email(__("Please enter a valid email address")),
});
const form = useFormWithSchema(schema, {
defaultValues: { fullName: "", email: "" },
});
const [createSubscriber, isCreating] = useMutationWithToasts<NewCompliancePageSubscriberDialogMutation>(
createSubscriberMutation,
{
successMessage: __("Subscriber added successfully"),
errorMessage: __("Failed to add subscriber"),
},
);
const handleSubmit = async (data: z.infer<typeof schema>) => {
await createSubscriber({
variables: {
input: {
mailingListId,
fullName: data.fullName.trim(),
email: data.email.trim(),
},
connections: connectionId ? [connectionId] : [],
},
onCompleted: (_, errors) => {
if (errors?.length) return;
setTimeout(() => {
form.reset();
ref.current?.close();
}, 50);
setTimeout(() => {
form.reset();
}, 300);
},
});
};
return (
<Dialog ref={ref} title={__("Add Subscriber")}>
<form onSubmit={e => void form.handleSubmit(handleSubmit)(e)}>
<DialogContent padded className="space-y-6">
<p className="text-txt-secondary text-sm">
{__("Add a person to receive security and compliance updates")}
</p>
<Field
label={__("Full Name")}
required
error={form.formState.errors.fullName?.message}
{...form.register("fullName")}
placeholder={__("John Doe")}
/>
<Field
label={__("Email Address")}
required
error={form.formState.errors.email?.message}
type="email"
{...form.register("email")}
placeholder={__("john@example.com")}
/>
</DialogContent>
<DialogFooter>
<Button type="submit" disabled={isCreating}>
{isCreating && <Spinner />}
{__("Add Subscriber")}
</Button>
</DialogFooter>
</form>
</Dialog>
);
}

View File

@@ -57,6 +57,11 @@ export const compliancePageRoutes = [
Fallback: LinkCardSkeleton,
Component: lazy(() => import("#/pages/organizations/compliance-page/access/CompliancePageAccessPageLoader")),
},
{
path: "mailing-list",
Fallback: LinkCardSkeleton,
Component: lazy(() => import("#/pages/organizations/compliance-page/mailing-list/CompliancePageMailingListPageLoader")),
},
],
},
];

View File

@@ -5,6 +5,7 @@ import { UnAuthenticatedError } from "@probo/relay";
import {
Button,
Card,
IconBell2,
IconBlock,
IconLock,
IconMedal,
@@ -20,6 +21,8 @@ import type { TrustGraphCurrentQuery$data } from "#/queries/__generated__/TrustG
import { getPathPrefix } from "#/utils/pathPrefix";
import type { OrganizationSidebar_requestAllAccessesMutation } from "./__generated__/OrganizationSidebar_requestAllAccessesMutation.graphql";
import type { OrganizationSidebar_subscribeToMailingListMutation } from "./__generated__/OrganizationSidebar_subscribeToMailingListMutation.graphql";
import type { OrganizationSidebar_unsubscribeFromMailingListMutation } from "./__generated__/OrganizationSidebar_unsubscribeFromMailingListMutation.graphql";
import { FrameworkBadge } from "./FrameworkBadge";
const requestAllAccessesMutation = graphql`
@@ -32,11 +35,35 @@ const requestAllAccessesMutation = graphql`
}
`;
const subscribeToMailingListMutation = graphql`
mutation OrganizationSidebar_subscribeToMailingListMutation {
subscribeToMailingList {
subscription {
id
email
createdAt
updatedAt
}
}
}
`;
const unsubscribeFromMailingListMutation = graphql`
mutation OrganizationSidebar_unsubscribeFromMailingListMutation {
unsubscribeFromMailingList {
deletedMailingListSubscriberId @deleteRecord
}
}
`;
export function OrganizationSidebar({
trustCenter,
isAuthenticated,
}: {
trustCenter: TrustGraphCurrentQuery$data["currentTrustCenter"];
isAuthenticated: boolean;
}) {
const trustCenterId = trustCenter?.id;
const { __ } = useTranslate();
const { toast } = useToast();
const theme = useSystemTheme();
@@ -51,6 +78,16 @@ export function OrganizationSidebar({
requestAllAccessesMutation,
);
const [subscribeToMailingList, isSubscribing]
= useMutation<OrganizationSidebar_subscribeToMailingListMutation>(
subscribeToMailingListMutation,
);
const [unsubscribeFromMailingList, isUnsubscribing]
= useMutation<OrganizationSidebar_unsubscribeFromMailingListMutation>(
unsubscribeFromMailingListMutation,
);
const handleRequestAllAccesses = () => {
requestAllAccesses({
variables: {},
@@ -93,6 +130,71 @@ export function OrganizationSidebar({
});
};
const handleSubscribe = () => {
subscribeToMailingList({
variables: {},
updater: (store, data) => {
const subscription = data?.subscribeToMailingList?.subscription;
if (!subscription?.id || !trustCenterId) return;
const trustCenterRecord = store.get(trustCenterId);
if (!trustCenterRecord) return;
const subscriptionRecord = store.get(subscription.id);
if (!subscriptionRecord) return;
trustCenterRecord.setLinkedRecord(subscriptionRecord, "viewerSubscription");
},
onCompleted: (_, errors) => {
if (errors?.length) {
toast({
title: __("Error"),
description: formatError(__("Cannot subscribe"), errors),
variant: "error",
});
return;
}
toast({
title: __("Subscribed"),
description: __("You will be notified of security updates."),
variant: "success",
});
},
onError: (error) => {
toast({
title: __("Error"),
description: error.message ?? __("Cannot subscribe"),
variant: "error",
});
},
});
};
const handleUnsubscribe = () => {
unsubscribeFromMailingList({
variables: {},
onCompleted: (_, errors) => {
if (errors?.length) {
toast({
title: __("Error"),
description: formatError(__("Cannot unsubscribe"), errors),
variant: "error",
});
return;
}
toast({
title: __("Unsubscribed"),
description: __("You will no longer receive security updates."),
variant: "success",
});
},
onError: (error) => {
toast({
title: __("Error"),
description: error.message ?? __("Cannot unsubscribe"),
variant: "error",
});
},
});
};
if (!trustCenter) {
return null;
}
@@ -188,15 +290,43 @@ export function OrganizationSidebar({
</>
)}
<Button
disabled={isRequestingAccess}
variant="primary"
icon={IconLock}
className="w-full h-10"
onClick={handleRequestAllAccesses}
>
{__("Request access")}
</Button>
{/* Actions */}
<div className="space-y-2">
<Button
disabled={isRequestingAccess}
variant="primary"
icon={IconLock}
className="w-full h-10"
onClick={handleRequestAllAccesses}
>
{__("Request access")}
</Button>
{isAuthenticated && (
trustCenter.viewerSubscription
? (
<Button
disabled={isUnsubscribing}
variant="secondary"
icon={IconBell2}
className="w-full h-10"
onClick={handleUnsubscribe}
>
{__("Unsubscribe from updates")}
</Button>
)
: (
<Button
disabled={isSubscribing}
variant="secondary"
icon={IconBell2}
className="w-full h-10"
onClick={handleSubscribe}
>
{__("Subscribe to updates")}
</Button>
)
)}
</div>
</div>
</Card>
);

View File

@@ -0,0 +1,111 @@
/**
* @generated SignedSource<<80e6e1e3558d126472e7affb5b7b6cbd>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type OrganizationSidebar_subscribeToMailingListMutation$variables = Record<PropertyKey, never>;
export type OrganizationSidebar_subscribeToMailingListMutation$data = {
readonly subscribeToMailingList: {
readonly subscription: {
readonly createdAt: any;
readonly email: any;
readonly id: string;
readonly updatedAt: any;
};
};
};
export type OrganizationSidebar_subscribeToMailingListMutation = {
response: OrganizationSidebar_subscribeToMailingListMutation$data;
variables: OrganizationSidebar_subscribeToMailingListMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"alias": null,
"args": null,
"concreteType": "SubscribeToMailingListPayload",
"kind": "LinkedField",
"name": "subscribeToMailingList",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "MailingListSubscriber",
"kind": "LinkedField",
"name": "subscription",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "OrganizationSidebar_subscribeToMailingListMutation",
"selections": (v0/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [],
"kind": "Operation",
"name": "OrganizationSidebar_subscribeToMailingListMutation",
"selections": (v0/*: any*/)
},
"params": {
"cacheID": "07d99ad1645ea1e8752678fd9b5e6748",
"id": null,
"metadata": {},
"name": "OrganizationSidebar_subscribeToMailingListMutation",
"operationKind": "mutation",
"text": "mutation OrganizationSidebar_subscribeToMailingListMutation {\n subscribeToMailingList {\n subscription {\n id\n email\n createdAt\n updatedAt\n }\n }\n}\n"
}
};
})();
(node as any).hash = "80d3e85f4a55a7de7b066959f4a457c8";
export default node;

View File

@@ -0,0 +1,96 @@
/**
* @generated SignedSource<<8c1211258e4480dcaa6fdf14364cb49b>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type OrganizationSidebar_unsubscribeFromMailingListMutation$variables = Record<PropertyKey, never>;
export type OrganizationSidebar_unsubscribeFromMailingListMutation$data = {
readonly unsubscribeFromMailingList: {
readonly deletedMailingListSubscriberId: string | null | undefined;
};
};
export type OrganizationSidebar_unsubscribeFromMailingListMutation = {
response: OrganizationSidebar_unsubscribeFromMailingListMutation$data;
variables: OrganizationSidebar_unsubscribeFromMailingListMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "deletedMailingListSubscriberId",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "OrganizationSidebar_unsubscribeFromMailingListMutation",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "UnsubscribeFromMailingListPayload",
"kind": "LinkedField",
"name": "unsubscribeFromMailingList",
"plural": false,
"selections": [
(v0/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [],
"kind": "Operation",
"name": "OrganizationSidebar_unsubscribeFromMailingListMutation",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "UnsubscribeFromMailingListPayload",
"kind": "LinkedField",
"name": "unsubscribeFromMailingList",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "deleteRecord",
"key": "",
"kind": "ScalarHandle",
"name": "deletedMailingListSubscriberId"
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "1c6706f94348a5426c1e57d5fe99ab82",
"id": null,
"metadata": {},
"name": "OrganizationSidebar_unsubscribeFromMailingListMutation",
"operationKind": "mutation",
"text": "mutation OrganizationSidebar_unsubscribeFromMailingListMutation {\n unsubscribeFromMailingList {\n deletedMailingListSubscriberId\n }\n}\n"
}
};
})();
(node as any).hash = "7f3949f63166fc3bd23838a44fb94993";
export default node;

View File

@@ -18,6 +18,7 @@ export function MainLayout(props: Props) {
const { __ } = useTranslate();
const data = usePreloadedQuery(currentTrustGraphQuery, props.queryRef);
const trustCenter = data.currentTrustCenter;
const isAuthenticated = data.viewer != null;
const theme = useSystemTheme();
@@ -31,7 +32,7 @@ export function MainLayout(props: Props) {
return (
<TrustCenterProvider trustCenter={trustCenter}>
<div className="grid grid-cols-1 max-w-[1280px] mx-4 pt-6 gap-4 lg:mx-auto lg:gap-10 lg:pt-20 lg:grid-cols-[400px_1fr] lg:items-start ">
<OrganizationSidebar trustCenter={trustCenter} />
<OrganizationSidebar trustCenter={trustCenter} isAuthenticated={isAuthenticated} />
<main>
<Tabs className="mb-8">
<TabLink to="/overview">{__("Overview")}</TabLink>

View File

@@ -3,9 +3,18 @@ import { graphql } from "relay-runtime";
// Queries for custom domain (subdomain) approach
export const currentTrustGraphQuery = graphql`
query TrustGraphCurrentQuery {
viewer {
id
}
currentTrustCenter @required(action: THROW) {
id
slug
viewerSubscription {
id
email
createdAt
updatedAt
}
logoFileUrl
darkLogoFileUrl
nonDisclosureAgreement {

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<74757bf9a32552f6353eb888c9f663cd>>
* @generated SignedSource<<5b0657c3423f38cbd689de2dae810831>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -62,8 +62,17 @@ export type TrustGraphCurrentQuery$data = {
readonly vendorInfo: {
readonly totalCount: number;
};
readonly viewerSubscription: {
readonly createdAt: any;
readonly email: any;
readonly id: string;
readonly updatedAt: any;
} | null | undefined;
readonly " $fragmentSpreads": FragmentRefs<"OverviewPageFragment">;
};
readonly viewer: {
readonly id: string;
} | null | undefined;
};
export type TrustGraphCurrentQuery = {
response: TrustGraphCurrentQuery$data;
@@ -81,81 +90,120 @@ var v0 = {
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "slug",
"concreteType": "Identity",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v0/*: any*/)
],
"storageKey": null
},
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoFileUrl",
"name": "slug",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "darkLogoFileUrl",
"name": "email",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fileName",
"concreteType": "MailingListSubscriber",
"kind": "LinkedField",
"name": "viewerSubscription",
"plural": false,
"selections": [
(v0/*: any*/),
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
}
],
"storageKey": null
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fileUrl",
"name": "logoFileUrl",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"name": "darkLogoFileUrl",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"name": "fileName",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"name": "fileUrl",
"storageKey": null
},
v9 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "websiteUrl",
"name": "status",
"storageKey": null
},
v10 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"name": "name",
"storageKey": null
},
v11 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "headquarterAddress",
"name": "description",
"storageKey": null
},
v12 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "websiteUrl",
"storageKey": null
},
v13 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "headquarterAddress",
"storageKey": null
},
v14 = {
"alias": null,
"args": [
{
@@ -186,7 +234,7 @@ v12 = {
"plural": false,
"selections": [
(v0/*: any*/),
(v7/*: any*/),
(v10/*: any*/),
{
"alias": null,
"args": null,
@@ -203,7 +251,7 @@ v12 = {
],
"storageKey": "externalUrls(first:20)"
},
v13 = {
v15 = {
"alias": "vendorInfo",
"args": [
{
@@ -227,28 +275,28 @@ v13 = {
],
"storageKey": "vendors(first:0)"
},
v14 = [
v16 = [
{
"kind": "Literal",
"name": "first",
"value": 50
}
],
v15 = [
v17 = [
{
"kind": "Literal",
"name": "first",
"value": 5
}
],
v16 = {
v18 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "isUserAuthorized",
"storageKey": null
},
v17 = {
v19 = {
"alias": null,
"args": null,
"concreteType": "DocumentAccess",
@@ -257,11 +305,11 @@ v17 = {
"plural": false,
"selections": [
(v0/*: any*/),
(v6/*: any*/)
(v9/*: any*/)
],
"storageKey": null
},
v18 = {
v20 = {
"alias": null,
"args": null,
"concreteType": "Framework",
@@ -270,7 +318,7 @@ v18 = {
"plural": false,
"selections": [
(v0/*: any*/),
(v7/*: any*/),
(v10/*: any*/),
{
"alias": null,
"args": null,
@@ -295,6 +343,7 @@ return {
"metadata": null,
"name": "TrustGraphCurrentQuery",
"selections": [
(v1/*: any*/),
{
"kind": "RequiredField",
"field": {
@@ -306,9 +355,10 @@ return {
"plural": false,
"selections": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
{
"alias": null,
"args": null,
@@ -317,8 +367,8 @@ return {
"name": "nonDisclosureAgreement",
"plural": false,
"selections": [
(v4/*: any*/),
(v5/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
{
"alias": null,
"args": null,
@@ -327,7 +377,7 @@ return {
"name": "viewerSignature",
"plural": false,
"selections": [
(v6/*: any*/)
(v9/*: any*/)
],
"storageKey": null
}
@@ -342,24 +392,24 @@ return {
"name": "organization",
"plural": false,
"selections": [
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
(v11/*: any*/)
(v11/*: any*/),
(v12/*: any*/),
(v3/*: any*/),
(v13/*: any*/)
],
"storageKey": null
},
(v12/*: any*/),
(v14/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "OverviewPageFragment"
},
(v13/*: any*/),
(v15/*: any*/),
{
"alias": null,
"args": (v14/*: any*/),
"args": (v16/*: any*/),
"concreteType": "AuditConnection",
"kind": "LinkedField",
"name": "audits",
@@ -398,7 +448,7 @@ return {
},
{
"alias": null,
"args": (v14/*: any*/),
"args": (v16/*: any*/),
"concreteType": "ComplianceFrameworkConnection",
"kind": "LinkedField",
"name": "complianceFrameworks",
@@ -461,6 +511,7 @@ return {
"kind": "Operation",
"name": "TrustGraphCurrentQuery",
"selections": [
(v1/*: any*/),
{
"alias": null,
"args": null,
@@ -470,9 +521,10 @@ return {
"plural": false,
"selections": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
{
"alias": null,
"args": null,
@@ -481,8 +533,8 @@ return {
"name": "nonDisclosureAgreement",
"plural": false,
"selections": [
(v4/*: any*/),
(v5/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
{
"alias": null,
"args": null,
@@ -491,7 +543,7 @@ return {
"name": "viewerSignature",
"plural": false,
"selections": [
(v6/*: any*/),
(v9/*: any*/),
(v0/*: any*/)
],
"storageKey": null
@@ -507,16 +559,16 @@ return {
"name": "organization",
"plural": false,
"selections": [
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
(v11/*: any*/),
(v12/*: any*/),
(v3/*: any*/),
(v13/*: any*/),
(v0/*: any*/)
],
"storageKey": null
},
(v12/*: any*/),
(v14/*: any*/),
{
"alias": null,
"args": [
@@ -548,7 +600,7 @@ return {
"plural": false,
"selections": [
(v0/*: any*/),
(v7/*: any*/),
(v10/*: any*/),
{
"alias": null,
"args": null,
@@ -556,7 +608,7 @@ return {
"name": "logoUrl",
"storageKey": null
},
(v9/*: any*/)
(v12/*: any*/)
],
"storageKey": null
}
@@ -604,9 +656,9 @@ return {
"name": "countries",
"storageKey": null
},
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/)
(v10/*: any*/),
(v11/*: any*/),
(v12/*: any*/)
],
"storageKey": null
}
@@ -618,7 +670,7 @@ return {
},
{
"alias": null,
"args": (v15/*: any*/),
"args": (v17/*: any*/),
"concreteType": "DocumentConnection",
"kind": "LinkedField",
"name": "documents",
@@ -648,8 +700,8 @@ return {
"name": "title",
"storageKey": null
},
(v16/*: any*/),
(v17/*: any*/),
(v18/*: any*/),
(v19/*: any*/),
{
"alias": null,
"args": null,
@@ -668,7 +720,7 @@ return {
},
{
"alias": null,
"args": (v15/*: any*/),
"args": (v17/*: any*/),
"concreteType": "TrustCenterFileConnection",
"kind": "LinkedField",
"name": "trustCenterFiles",
@@ -698,9 +750,9 @@ return {
"name": "category",
"storageKey": null
},
(v7/*: any*/),
(v16/*: any*/),
(v17/*: any*/)
(v10/*: any*/),
(v18/*: any*/),
(v19/*: any*/)
],
"storageKey": null
}
@@ -710,10 +762,10 @@ return {
],
"storageKey": "trustCenterFiles(first:5)"
},
(v13/*: any*/),
(v15/*: any*/),
{
"alias": null,
"args": (v14/*: any*/),
"args": (v16/*: any*/),
"concreteType": "AuditConnection",
"kind": "LinkedField",
"name": "audits",
@@ -736,7 +788,7 @@ return {
"plural": false,
"selections": [
(v0/*: any*/),
(v7/*: any*/),
(v10/*: any*/),
{
"alias": null,
"args": null,
@@ -753,12 +805,12 @@ return {
"name": "filename",
"storageKey": null
},
(v16/*: any*/),
(v17/*: any*/)
(v18/*: any*/),
(v19/*: any*/)
],
"storageKey": null
},
(v18/*: any*/)
(v20/*: any*/)
],
"storageKey": null
}
@@ -770,7 +822,7 @@ return {
},
{
"alias": null,
"args": (v14/*: any*/),
"args": (v16/*: any*/),
"concreteType": "ComplianceFrameworkConnection",
"kind": "LinkedField",
"name": "complianceFrameworks",
@@ -793,7 +845,7 @@ return {
"plural": false,
"selections": [
(v0/*: any*/),
(v18/*: any*/)
(v20/*: any*/)
],
"storageKey": null
}
@@ -809,16 +861,16 @@ return {
]
},
"params": {
"cacheID": "96e75937f385e17b760440e001cefc47",
"cacheID": "80334c766999a3d299eaf40fd32235b7",
"id": null,
"metadata": {},
"name": "TrustGraphCurrentQuery",
"operationKind": "query",
"text": "query TrustGraphCurrentQuery {\n currentTrustCenter {\n id\n slug\n logoFileUrl\n darkLogoFileUrl\n nonDisclosureAgreement {\n fileName\n fileUrl\n viewerSignature {\n status\n id\n }\n }\n organization {\n name\n description\n websiteUrl\n email\n headquarterAddress\n id\n }\n externalUrls(first: 20) {\n edges {\n node {\n id\n name\n url\n }\n }\n }\n ...OverviewPageFragment\n vendorInfo: vendors(first: 0) {\n totalCount\n }\n audits(first: 50) {\n edges {\n node {\n id\n ...AuditRowFragment\n }\n }\n }\n complianceFrameworks(first: 50) {\n edges {\n node {\n id\n framework {\n ...FrameworkBadgeFragment\n id\n }\n }\n }\n }\n }\n}\n\nfragment AuditRowFragment on Audit {\n name\n report {\n id\n filename\n isUserAuthorized\n access {\n id\n status\n }\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 access {\n id\n status\n }\n}\n\nfragment FrameworkBadgeFragment on Framework {\n id\n name\n lightLogoURL\n darkLogoURL\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 access {\n id\n status\n }\n}\n\nfragment VendorRowFragment on Vendor {\n name\n description\n websiteUrl\n countries\n}\n"
"text": "query TrustGraphCurrentQuery {\n viewer {\n id\n }\n currentTrustCenter {\n id\n slug\n viewerSubscription {\n id\n email\n createdAt\n updatedAt\n }\n logoFileUrl\n darkLogoFileUrl\n nonDisclosureAgreement {\n fileName\n fileUrl\n viewerSignature {\n status\n id\n }\n }\n organization {\n name\n description\n websiteUrl\n email\n headquarterAddress\n id\n }\n externalUrls(first: 20) {\n edges {\n node {\n id\n name\n url\n }\n }\n }\n ...OverviewPageFragment\n vendorInfo: vendors(first: 0) {\n totalCount\n }\n audits(first: 50) {\n edges {\n node {\n id\n ...AuditRowFragment\n }\n }\n }\n complianceFrameworks(first: 50) {\n edges {\n node {\n id\n framework {\n ...FrameworkBadgeFragment\n id\n }\n }\n }\n }\n }\n}\n\nfragment AuditRowFragment on Audit {\n name\n report {\n id\n filename\n isUserAuthorized\n access {\n id\n status\n }\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 access {\n id\n status\n }\n}\n\nfragment FrameworkBadgeFragment on Framework {\n id\n name\n lightLogoURL\n darkLogoURL\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 access {\n id\n status\n }\n}\n\nfragment VendorRowFragment on Vendor {\n name\n description\n websiteUrl\n countries\n}\n"
}
};
})();
(node as any).hash = "ca6f04fecf9bce92407f33e8c0e4bd03";
(node as any).hash = "d38cdfff47370448fff4d9c6c22ad1bd";
export default node;

View File

@@ -87,6 +87,8 @@ const (
EmailAttachmentEntityType uint16 = 61
ComplianceFrameworkEntityType uint16 = 62
ComplianceExternalURLEntityType uint16 = 63
MailingListEntityType uint16 = 64
MailingListSubscriberEntityType uint16 = 65
)
func NewEntityFromID(id gid.GID) (any, bool) {
@@ -215,6 +217,10 @@ func NewEntityFromID(id gid.GID) (any, bool) {
return &ComplianceFramework{ID: id}, true
case ComplianceExternalURLEntityType:
return &ComplianceExternalURL{ID: id}, true
case MailingListEntityType:
return &MailingList{ID: id}, true
case MailingListSubscriberEntityType:
return &MailingListSubscriber{ID: id}, true
default:
return nil, false
}

View File

@@ -0,0 +1,170 @@
// 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 coredata
import (
"context"
"errors"
"fmt"
"maps"
"time"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/mail"
)
type MailingList struct {
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
ReplyTo *mail.Addr `db:"reply_to"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func (ml *MailingList) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) {
q := `SELECT organization_id FROM mailing_lists WHERE id = $1 LIMIT 1;`
var organizationID gid.GID
if err := conn.QueryRow(ctx, q, ml.ID).Scan(&organizationID); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrResourceNotFound
}
return nil, fmt.Errorf("cannot query mailing list authorization attributes: %w", err)
}
return map[string]string{"organization_id": organizationID.String()}, nil
}
func (ml *MailingList) LoadByID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
id gid.GID,
) error {
q := `
SELECT
id,
organization_id,
reply_to,
created_at,
updated_at
FROM
mailing_lists
WHERE
%s
AND id = @mailing_list_id
LIMIT 1;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"mailing_list_id": id}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query mailing list: %w", err)
}
result, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[MailingList])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ErrResourceNotFound
}
return fmt.Errorf("cannot collect mailing list: %w", err)
}
*ml = result
return nil
}
func (ml *MailingList) Update(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
UPDATE mailing_lists
SET
reply_to = @reply_to,
updated_at = @updated_at
WHERE
%s
AND id = @mailing_list_id;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"mailing_list_id": ml.ID,
"reply_to": ml.ReplyTo,
"updated_at": ml.UpdatedAt,
}
maps.Copy(args, scope.SQLArguments())
tag, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot update mailing list: %w", err)
}
if tag.RowsAffected() == 0 {
return ErrResourceNotFound
}
return nil
}
func (ml *MailingList) Insert(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
INSERT INTO mailing_lists (
id,
tenant_id,
organization_id,
reply_to,
created_at,
updated_at
)
VALUES (
@mailing_list_id,
@tenant_id,
@organization_id,
@reply_to,
@created_at,
@updated_at
);
`
args := pgx.StrictNamedArgs{
"mailing_list_id": ml.ID,
"tenant_id": scope.GetTenantID(),
"organization_id": ml.OrganizationID,
"reply_to": ml.ReplyTo,
"created_at": ml.CreatedAt,
"updated_at": ml.UpdatedAt,
}
if _, err := conn.Exec(ctx, q, args); err != nil {
return fmt.Errorf("cannot insert mailing list: %w", err)
}
return nil
}

View File

@@ -0,0 +1,316 @@
// 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 coredata
import (
"context"
"errors"
"fmt"
"maps"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/mail"
"go.probo.inc/probo/pkg/page"
)
type (
MailingListSubscriber struct {
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
MailingListID gid.GID `db:"mailing_list_id"`
FullName string `db:"full_name"`
Email mail.Addr `db:"email"`
Status MailingListSubscriberStatus `db:"status"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
MailingListSubscribers []*MailingListSubscriber
)
func (cns *MailingListSubscriber) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) {
q := `SELECT organization_id FROM mailing_list_subscribers WHERE id = $1 LIMIT 1;`
var organizationID gid.GID
if err := conn.QueryRow(ctx, q, cns.ID).Scan(&organizationID); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrResourceNotFound
}
return nil, fmt.Errorf("cannot query mailing list subscriber authorization attributes: %w", err)
}
return map[string]string{"organization_id": organizationID.String()}, nil
}
func (cns *MailingListSubscriber) CursorKey(orderBy MailingListSubscriberOrderField) page.CursorKey {
switch orderBy {
case MailingListSubscriberOrderFieldCreatedAt:
return page.NewCursorKey(cns.ID, cns.CreatedAt)
}
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
}
func (cns *MailingListSubscriber) LoadByMailingListIDAndEmail(
ctx context.Context,
conn pg.Conn,
scope Scoper,
mailingListID gid.GID,
email mail.Addr,
) error {
q := `
SELECT
id,
organization_id,
mailing_list_id,
full_name,
email,
status,
created_at,
updated_at
FROM
mailing_list_subscribers
WHERE
%s
AND mailing_list_id = @mailing_list_id
AND email = @email
LIMIT 1;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"mailing_list_id": mailingListID,
"email": email,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query mailing list subscriber: %w", err)
}
subscriber, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[MailingListSubscriber])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ErrResourceNotFound
}
return fmt.Errorf("cannot collect mailing list subscriber: %w", err)
}
*cns = subscriber
return nil
}
func (cns *MailingListSubscriber) Insert(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
INSERT INTO mailing_list_subscribers (
id,
tenant_id,
organization_id,
mailing_list_id,
full_name,
email,
status,
created_at,
updated_at
)
VALUES (
@mailing_list_subscriber_id,
@tenant_id,
@organization_id,
@mailing_list_id,
@full_name,
@email,
@status,
@created_at,
@updated_at
);
`
args := pgx.StrictNamedArgs{
"mailing_list_subscriber_id": cns.ID,
"tenant_id": scope.GetTenantID(),
"organization_id": cns.OrganizationID,
"mailing_list_id": cns.MailingListID,
"full_name": cns.FullName,
"email": cns.Email,
"status": cns.Status,
"created_at": cns.CreatedAt,
"updated_at": cns.UpdatedAt,
}
if _, err := conn.Exec(ctx, q, args); err != nil {
if pgErr, ok := errors.AsType[*pgconn.PgError](err); ok && pgErr.Code == "23505" {
return ErrResourceAlreadyExists
}
return fmt.Errorf("cannot insert mailing list subscriber: %w", err)
}
return nil
}
func (cns *MailingListSubscriber) Update(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
UPDATE mailing_list_subscribers
SET
status = @status,
updated_at = @updated_at
WHERE
%s
AND id = @mailing_list_subscriber_id;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"mailing_list_subscriber_id": cns.ID,
"status": cns.Status,
"updated_at": cns.UpdatedAt,
}
maps.Copy(args, scope.SQLArguments())
tag, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot update mailing list subscriber: %w", err)
}
if tag.RowsAffected() == 0 {
return ErrResourceNotFound
}
return nil
}
func (cns *MailingListSubscriber) Delete(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
DELETE FROM
mailing_list_subscribers
WHERE
%s
AND id = @mailing_list_subscriber_id;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"mailing_list_subscriber_id": cns.ID}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot delete mailing list subscriber: %w", err)
}
return nil
}
func (cnss *MailingListSubscribers) CountByMailingListID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
mailingListID gid.GID,
) (int, error) {
q := `
SELECT
COUNT(id)
FROM
mailing_list_subscribers
WHERE
%s
AND mailing_list_id = @mailing_list_id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"mailing_list_id": mailingListID}
maps.Copy(args, scope.SQLArguments())
row := conn.QueryRow(ctx, q, args)
var count int
err := row.Scan(&count)
if err != nil {
return 0, fmt.Errorf("cannot count mailing list subscribers: %w", err)
}
return count, nil
}
func (cnss *MailingListSubscribers) LoadByMailingListID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
mailingListID gid.GID,
cursor *page.Cursor[MailingListSubscriberOrderField],
) error {
q := `
SELECT
id,
organization_id,
mailing_list_id,
full_name,
email,
status,
created_at,
updated_at
FROM
mailing_list_subscribers
WHERE
%s
AND mailing_list_id = @mailing_list_id
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.StrictNamedArgs{
"mailing_list_id": mailingListID,
}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query mailing list subscribers: %w", err)
}
subscribers, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[MailingListSubscriber])
if err != nil {
return fmt.Errorf("cannot collect mailing list subscribers: %w", err)
}
*cnss = subscribers
return nil
}

View File

@@ -0,0 +1,36 @@
// 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 coredata
import "fmt"
type MailingListSubscriberOrderField string
const (
MailingListSubscriberOrderFieldCreatedAt MailingListSubscriberOrderField = "CREATED_AT"
)
func (f MailingListSubscriberOrderField) String() string {
return string(f)
}
func (f MailingListSubscriberOrderField) Column() string {
switch f {
case MailingListSubscriberOrderFieldCreatedAt:
return "created_at"
}
panic(fmt.Sprintf("unsupported order by: %s", f))
}

View File

@@ -0,0 +1,58 @@
// 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 coredata
import (
"database/sql/driver"
"fmt"
)
type MailingListSubscriberStatus string
const (
MailingListSubscriberStatusPending MailingListSubscriberStatus = "PENDING"
MailingListSubscriberStatusConfirmed MailingListSubscriberStatus = "CONFIRMED"
)
func (s MailingListSubscriberStatus) String() string {
return string(s)
}
func (s *MailingListSubscriberStatus) Scan(value any) error {
var str string
switch v := value.(type) {
case string:
str = v
case []byte:
str = string(v)
default:
return fmt.Errorf("unsupported type for MailingListSubscriberStatus: %T", value)
}
switch str {
case "PENDING":
*s = MailingListSubscriberStatusPending
case "CONFIRMED":
*s = MailingListSubscriberStatusConfirmed
default:
return fmt.Errorf("invalid MailingListSubscriberStatus value: %q", str)
}
return nil
}
func (s MailingListSubscriberStatus) Value() (driver.Value, error) {
return s.String(), nil
}

View File

@@ -0,0 +1,50 @@
CREATE TABLE mailing_lists (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
organization_id TEXT NOT NULL REFERENCES organizations(id) ON UPDATE CASCADE ON DELETE CASCADE,
reply_to CITEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
UNIQUE (organization_id)
);
CREATE TYPE mailing_list_subscriber_status AS ENUM (
'PENDING',
'CONFIRMED'
);
CREATE TABLE mailing_list_subscribers (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
organization_id TEXT NOT NULL REFERENCES organizations(id) ON UPDATE CASCADE ON DELETE CASCADE,
mailing_list_id TEXT NOT NULL REFERENCES mailing_lists(id) ON UPDATE CASCADE ON DELETE CASCADE,
full_name TEXT NOT NULL,
email CITEXT NOT NULL,
status mailing_list_subscriber_status NOT NULL DEFAULT 'PENDING',
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
UNIQUE (mailing_list_id, email)
);
ALTER TABLE trust_centers
ADD COLUMN mailing_list_id TEXT REFERENCES mailing_lists(id) ON UPDATE CASCADE ON DELETE RESTRICT;
INSERT INTO mailing_lists (
id,
tenant_id,
organization_id,
created_at,
updated_at
)
SELECT
generate_gid(decode_base64_unpadded(tc.tenant_id), 64),
tc.tenant_id,
tc.organization_id,
NOW(),
NOW()
FROM trust_centers tc;
UPDATE trust_centers tc
SET mailing_list_id = ml.id
FROM mailing_lists ml
WHERE ml.organization_id = tc.organization_id;

View File

@@ -35,6 +35,7 @@ type (
TenantID gid.TenantID `db:"tenant_id"`
Active bool `db:"active"`
Slug string `db:"slug"`
MailingListID *gid.GID `db:"mailing_list_id"`
LogoFileID *gid.GID `db:"logo_file_id"`
DarkLogoFileID *gid.GID `db:"dark_logo_file_id"`
NonDisclosureAgreementFileID *gid.GID `db:"non_disclosure_agreement_file_id"`
@@ -79,6 +80,7 @@ SELECT
id,
organization_id,
tenant_id,
mailing_list_id,
logo_file_id,
dark_logo_file_id,
active,
@@ -129,6 +131,7 @@ SELECT
id,
organization_id,
tenant_id,
mailing_list_id,
logo_file_id,
dark_logo_file_id,
active,
@@ -179,6 +182,7 @@ SELECT
id,
organization_id,
tenant_id,
mailing_list_id,
logo_file_id,
dark_logo_file_id,
active,
@@ -224,6 +228,7 @@ INSERT INTO trust_centers (
id,
organization_id,
tenant_id,
mailing_list_id,
logo_file_id,
dark_logo_file_id,
active,
@@ -235,6 +240,7 @@ INSERT INTO trust_centers (
@id,
@organization_id,
@tenant_id,
@mailing_list_id,
@logo_file_id,
@dark_logo_file_id,
@active,
@@ -249,6 +255,7 @@ INSERT INTO trust_centers (
"id": tc.ID,
"organization_id": tc.OrganizationID,
"tenant_id": tc.TenantID,
"mailing_list_id": tc.MailingListID,
"logo_file_id": tc.LogoFileID,
"dark_logo_file_id": tc.DarkLogoFileID,
"active": tc.Active,

View File

@@ -480,12 +480,20 @@ func (s *OrganizationService) CreateOrganization(
UpdatedAt: now,
}
mailingList = &coredata.MailingList{
ID: gid.New(tenantID, coredata.MailingListEntityType),
OrganizationID: organization.ID,
CreatedAt: now,
UpdatedAt: now,
}
trustCenter = &coredata.TrustCenter{
ID: gid.New(tenantID, coredata.TrustCenterEntityType),
OrganizationID: organization.ID,
TenantID: organization.TenantID,
Active: false,
Slug: slug.Make(organization.Name),
MailingListID: &mailingList.ID,
CreatedAt: now,
UpdatedAt: now,
}
@@ -616,6 +624,10 @@ func (s *OrganizationService) CreateOrganization(
return fmt.Errorf("cannot insert organization context: %w", err)
}
if err := mailingList.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert mailing list: %w", err)
}
if err := trustCenter.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert trust center: %w", err)
}

23
pkg/mailman/errors.go Normal file
View File

@@ -0,0 +1,23 @@
// 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 mailman
import "errors"
var (
ErrMailingListNotFound = errors.New("mailing list not found")
ErrSubscriberNotFound = errors.New("mailing list subscriber not found")
ErrSubscriberAlreadyExist = errors.New("mailing list subscriber already exists")
)

218
pkg/mailman/service.go Normal file
View File

@@ -0,0 +1,218 @@
// 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 mailman
import (
"context"
"errors"
"fmt"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/mail"
"go.probo.inc/probo/pkg/page"
)
type Service struct {
pg *pg.Client
}
func NewService(pgClient *pg.Client) *Service {
return &Service{pg: pgClient}
}
func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
return &TenantService{pg: s.pg, scope: coredata.NewScope(tenantID)}
}
type TenantService struct {
pg *pg.Client
scope coredata.Scoper
}
func (s *TenantService) UpdateMailingList(
ctx context.Context,
id gid.GID,
replyTo *mail.Addr,
) (*coredata.MailingList, error) {
var ml coredata.MailingList
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := ml.LoadByID(ctx, conn, s.scope, id); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return ErrMailingListNotFound
}
return fmt.Errorf("cannot load mailing list: %w", err)
}
ml.ReplyTo = replyTo
ml.UpdatedAt = time.Now()
if err := ml.Update(ctx, conn, s.scope); err != nil {
return fmt.Errorf("cannot update mailing list: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return &ml, nil
}
func (s *TenantService) GetSubscriber(
ctx context.Context,
mailingListID gid.GID,
email mail.Addr,
) (*coredata.MailingListSubscriber, error) {
var subscriber coredata.MailingListSubscriber
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := subscriber.LoadByMailingListIDAndEmail(ctx, conn, s.scope, mailingListID, email); err != nil {
return fmt.Errorf("cannot load mailing list subscriber: %w", err)
}
return nil
},
)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, nil
}
return nil, err
}
return &subscriber, nil
}
func (s *TenantService) CreateSubscriber(
ctx context.Context,
mailingListID gid.GID,
email mail.Addr,
fullName string,
) (*coredata.MailingListSubscriber, error) {
now := time.Now()
subscriber := &coredata.MailingListSubscriber{
ID: gid.New(s.scope.GetTenantID(), coredata.MailingListSubscriberEntityType),
MailingListID: mailingListID,
FullName: fullName,
Email: email,
Status: coredata.MailingListSubscriberStatusPending,
CreatedAt: now,
UpdatedAt: now,
}
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
var ml coredata.MailingList
if err := ml.LoadByID(ctx, conn, s.scope, mailingListID); err != nil {
return fmt.Errorf("cannot load mailing list: %w", err)
}
subscriber.OrganizationID = ml.OrganizationID
if err := subscriber.Insert(ctx, conn, s.scope); err != nil {
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
return ErrSubscriberAlreadyExist
}
return fmt.Errorf("cannot insert mailing list subscriber: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return subscriber, nil
}
func (s *TenantService) DeleteSubscriber(
ctx context.Context,
id gid.GID,
) error {
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
subscriber := coredata.MailingListSubscriber{ID: id}
if err := subscriber.Delete(ctx, conn, s.scope); err != nil {
return fmt.Errorf("cannot delete mailing list subscriber: %w", err)
}
return nil
},
)
if err != nil {
return err
}
return nil
}
func (s *TenantService) CountSubscribers(
ctx context.Context,
mailingListID gid.GID,
) (int, error) {
var count int
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) (err error) {
subscribers := coredata.MailingListSubscribers{}
count, err = subscribers.CountByMailingListID(ctx, conn, s.scope, mailingListID)
if err != nil {
return fmt.Errorf("cannot count mailing list subscribers: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s *TenantService) ListSubscribers(
ctx context.Context,
mailingListID gid.GID,
cursor *page.Cursor[coredata.MailingListSubscriberOrderField],
) (*page.Page[*coredata.MailingListSubscriber, coredata.MailingListSubscriberOrderField], error) {
var subscribers coredata.MailingListSubscribers
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := subscribers.LoadByMailingListID(ctx, conn, s.scope, mailingListID, cursor); err != nil {
return fmt.Errorf("cannot load mailing list subscribers: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(subscribers, cursor), nil
}

View File

@@ -40,6 +40,14 @@ const (
ActionTrustCenterAccessUpdate = "core:trust-center-access:update"
ActionTrustCenterAccessDelete = "core:trust-center-access:delete"
// MailingList actions
ActionMailingListUpdate = "core:mailing-list:update"
// MailingListSubscriber actions
ActionMailingListSubscriberList = "core:mailing-list-subscriber:list"
ActionMailingListSubscriberCreate = "core:mailing-list-subscriber:create"
ActionMailingListSubscriberDelete = "core:mailing-list-subscriber:delete"
// TrustCenterReference actions
ActionTrustCenterReferenceList = "core:trust-center-reference:list"
ActionTrustCenterReferenceGetLogoUrl = "core:trust-center-reference:get-logo-url"

View File

@@ -673,3 +673,36 @@ func (s *TrustCenterService) EmailPresenterConfig(ctx context.Context, complianc
return emailPresenterCfg, nil
}
func (s *TrustCenterService) GetMailingList(
ctx context.Context,
trustCenterID gid.GID,
) (*coredata.MailingList, error) {
var mailingList *coredata.MailingList
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
trustCenter := &coredata.TrustCenter{}
if err := trustCenter.LoadByID(ctx, conn, s.svc.scope, trustCenterID); err != nil {
return fmt.Errorf("cannot load trust center: %w", err)
}
if trustCenter.MailingListID == nil {
return nil
}
mailingList = &coredata.MailingList{}
if err := mailingList.LoadByID(ctx, conn, s.svc.scope, *trustCenter.MailingListID); err != nil {
return fmt.Errorf("cannot load mailing list: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return mailingList, nil
}

View File

@@ -56,6 +56,7 @@ import (
"go.probo.inc/probo/pkg/html2pdf"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailer"
"go.probo.inc/probo/pkg/mailman"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server"
@@ -471,6 +472,8 @@ func (impl *Implm) Run(
slackService,
)
mailmanService := mailman.NewService(pgClient)
serverHandler, err := server.NewServer(
server.Config{
AllowedOrigins: impl.cfg.Api.Cors.AllowedOrigins,
@@ -479,6 +482,7 @@ func (impl *Implm) Run(
IAM: iamService,
Trust: trustService,
ESign: esignService,
Mailman: mailmanService,
Slack: slackService,
ConnectorRegistry: defaultConnectorRegistry,
BaseURL: baseURL,

View File

@@ -27,6 +27,7 @@ import (
"go.probo.inc/probo/pkg/connector"
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailman"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/securecookie"
connect_v1 "go.probo.inc/probo/pkg/server/api/connect/v1"
@@ -47,6 +48,7 @@ type (
Trust *trust.Service
ESign *esign.Service
Slack *slack.Service
Mailman *mailman.Service
Cookie securecookie.Config
TokenSecret string
ConnectorRegistry *connector.ConnectorRegistry
@@ -120,7 +122,9 @@ func NewServer(cfg Config) (*Server, error) {
cfg.IAM,
cfg.Trust,
cfg.ESign,
cfg.Mailman,
cfg.Cookie,
cfg.TokenSecret,
cfg.BaseURL,
),
consoleHandler: console_v1.NewMux(
@@ -128,6 +132,7 @@ func NewServer(cfg Config) (*Server, error) {
cfg.Probo,
cfg.IAM,
cfg.ESign,
cfg.Mailman,
cfg.Cookie,
cfg.TokenSecret,
cfg.ConnectorRegistry,

View File

@@ -20,19 +20,21 @@ import (
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailman"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/server/api/authz"
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
"go.probo.inc/probo/pkg/server/gqlutils"
)
func NewGraphQLHandler(iamSvc *iam.Service, proboSvc *probo.Service, esignSvc *esign.Service, customDomainCname string, logger *log.Logger) http.Handler {
func NewGraphQLHandler(iamSvc *iam.Service, proboSvc *probo.Service, esignSvc *esign.Service, mailmanSvc *mailman.Service, customDomainCname string, logger *log.Logger) http.Handler {
config := schema.Config{
Resolvers: &Resolver{
authorize: authz.NewAuthorizeFunc(iamSvc, logger),
probo: proboSvc,
iam: iamSvc,
esign: esignSvc,
mailman: mailmanSvc,
customDomainCname: customDomainCname,
logger: logger,
},

View File

@@ -31,6 +31,7 @@ import (
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailman"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/saferedirect"
"go.probo.inc/probo/pkg/securecookie"
@@ -45,6 +46,7 @@ type (
probo *probo.Service
iam *iam.Service
esign *esign.Service
mailman *mailman.Service
logger *log.Logger
customDomainCname string
}
@@ -55,6 +57,7 @@ func NewMux(
proboSvc *probo.Service,
iamSvc *iam.Service,
esignSvc *esign.Service,
mailmanSvc *mailman.Service,
cookieConfig securecookie.Config,
tokenSecret string,
connectorRegistry *connector.ConnectorRegistry,
@@ -65,7 +68,7 @@ func NewMux(
safeRedirect := &saferedirect.SafeRedirect{AllowedHost: baseURL.Host()}
graphqlHandler := NewGraphQLHandler(iamSvc, proboSvc, esignSvc, customDomainCname, logger)
graphqlHandler := NewGraphQLHandler(iamSvc, proboSvc, esignSvc, mailmanSvc, customDomainCname, logger)
r.Group(func(r chi.Router) {
r.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig))
@@ -199,6 +202,10 @@ func (r *Resolver) ProboService(ctx context.Context, tenantID gid.TenantID) *pro
return r.probo.WithTenant(tenantID)
}
func (r *Resolver) MailmanService(ctx context.Context, tenantID gid.TenantID) *mailman.TenantService {
return r.mailman.WithTenant(tenantID)
}
func (r *Resolver) Permission(ctx context.Context, obj types.Node, action string) (bool, error) {
return r.authorize(ctx, obj.GetID(), action) == nil, nil
}

View File

@@ -1634,7 +1634,10 @@ input VendorFilter {
}
# Core Types
type TrustCenter implements Node {
type TrustCenter implements Node
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.TrustCenter"
) {
id: ID!
active: Boolean!
logoFileUrl: String @goField(forceResolver: true)
@@ -1677,9 +1680,60 @@ type TrustCenter implements Node {
orderBy: ComplianceExternalURLOrder
): ComplianceExternalURLConnection! @goField(forceResolver: true)
mailingList: MailingList @goField(forceResolver: true)
permission(action: String!): Boolean! @goField(forceResolver: true)
}
type MailingList implements Node {
id: ID!
replyTo: EmailAddr
subscribers(
first: Int
after: CursorKey
last: Int
before: CursorKey
): MailingListSubscriberConnection! @goField(forceResolver: true)
}
enum MailingListSubscriberStatus
@goModel(
model: "go.probo.inc/probo/pkg/coredata.MailingListSubscriberStatus"
) {
PENDING
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MailingListSubscriberStatusPending"
)
CONFIRMED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MailingListSubscriberStatusConfirmed"
)
}
type MailingListSubscriber implements Node {
id: ID!
fullName: String!
email: EmailAddr!
status: MailingListSubscriberStatus!
createdAt: Datetime!
updatedAt: Datetime!
}
type MailingListSubscriberConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.MailingListSubscriberConnection"
) {
edges: [MailingListSubscriberEdge!]!
pageInfo: PageInfo!
totalCount: Int! @goField(forceResolver: true)
}
type MailingListSubscriberEdge {
cursor: CursorKey!
node: MailingListSubscriber!
}
type Organization implements Node {
id: ID!
name: String!
@@ -3353,6 +3407,17 @@ type Mutation {
deleteTrustCenterAccess(
input: DeleteTrustCenterAccessInput!
): DeleteTrustCenterAccessPayload!
# Mailing List mutations
updateMailingList(
input: UpdateMailingListInput!
): UpdateMailingListPayload!
# Mailing List Subscriber mutations
createMailingListSubscriber(
input: CreateMailingListSubscriberInput!
): CreateMailingListSubscriberPayload!
deleteMailingListSubscriber(
input: DeleteMailingListSubscriberInput!
): DeleteMailingListSubscriberPayload!
# Trust Center Reference mutations
createTrustCenterReference(
input: CreateTrustCenterReferenceInput!
@@ -3755,6 +3820,25 @@ input DeleteTrustCenterAccessInput {
id: ID!
}
input UpdateMailingListInput {
id: ID!
replyTo: EmailAddr
}
type UpdateMailingListPayload {
mailingList: MailingList!
}
input CreateMailingListSubscriberInput {
mailingListId: ID!
fullName: String!
email: EmailAddr!
}
input DeleteMailingListSubscriberInput {
id: ID!
}
input CreateTrustCenterReferenceInput {
trustCenterId: ID!
name: String!
@@ -4606,6 +4690,14 @@ type DeleteTrustCenterAccessPayload {
deletedTrustCenterAccessId: ID!
}
type CreateMailingListSubscriberPayload {
mailingListSubscriberEdge: MailingListSubscriberEdge!
}
type DeleteMailingListSubscriberPayload {
deletedMailingListSubscriberId: ID!
}
type CreateTrustCenterReferencePayload {
trustCenterReferenceEdge: TrustCenterReferenceEdge!
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
// 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 types
import (
"go.probo.inc/probo/pkg/coredata"
)
func NewMailingList(ml *coredata.MailingList) *MailingList {
return &MailingList{
ID: ml.ID,
ReplyTo: ml.ReplyTo,
}
}

View File

@@ -0,0 +1,71 @@
// 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 types
import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
MailingListSubscriberOrderBy OrderBy[coredata.MailingListSubscriberOrderField]
MailingListSubscriberConnection struct {
TotalCount int
Edges []*MailingListSubscriberEdge
PageInfo *PageInfo
Resolver any
ParentID gid.GID
}
)
func NewMailingListSubscriber(s *coredata.MailingListSubscriber) *MailingListSubscriber {
return &MailingListSubscriber{
ID: s.ID,
FullName: s.FullName,
Email: s.Email,
Status: s.Status,
CreatedAt: s.CreatedAt,
UpdatedAt: s.UpdatedAt,
}
}
func NewMailingListSubscriberEdge(s *coredata.MailingListSubscriber, orderBy coredata.MailingListSubscriberOrderField) *MailingListSubscriberEdge {
return &MailingListSubscriberEdge{
Cursor: s.CursorKey(orderBy),
Node: NewMailingListSubscriber(s),
}
}
func NewMailingListSubscriberConnection(
p *page.Page[*coredata.MailingListSubscriber, coredata.MailingListSubscriberOrderField],
resolver any,
mailingListID gid.GID,
) *MailingListSubscriberConnection {
var edges = make([]*MailingListSubscriberEdge, len(p.Data))
for i := range edges {
edges[i] = NewMailingListSubscriberEdge(p.Data[i], p.Cursor.OrderBy.Field)
}
return &MailingListSubscriberConnection{
Edges: edges,
PageInfo: NewPageInfo(p),
Resolver: resolver,
ParentID: mailingListID,
}
}

View File

@@ -15,9 +15,33 @@
package types
import (
"time"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
)
type TrustCenter struct {
ID gid.GID `json:"id"`
Active bool `json:"active"`
LogoFileURL *string `json:"logoFileUrl,omitempty"`
DarkLogoFileURL *string `json:"darkLogoFileUrl,omitempty"`
NdaFileName *string `json:"ndaFileName,omitempty"`
NdaFileURL *string `json:"ndaFileUrl,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Organization *Organization `json:"organization"`
Accesses *TrustCenterAccessConnection `json:"accesses"`
References *TrustCenterReferenceConnection `json:"references"`
ComplianceFrameworks *ComplianceFrameworkConnection `json:"complianceFrameworks"`
ExternalUrls *ComplianceExternalURLConnection `json:"externalUrls"`
MailingList *MailingList `json:"mailingList,omitempty"`
Permission bool `json:"permission"`
}
func (TrustCenter) IsNode() {}
func (t TrustCenter) GetID() gid.GID { return t.ID }
func NewTrustCenter(tc *coredata.TrustCenter, file *coredata.File) *TrustCenter {
var ndaFileName *string
if file != nil {

View File

@@ -436,6 +436,16 @@ type CreateFrameworkPayload struct {
FrameworkEdge *FrameworkEdge `json:"frameworkEdge"`
}
type CreateMailingListSubscriberInput struct {
MailingListID gid.GID `json:"mailingListId"`
FullName string `json:"fullName"`
Email mail.Addr `json:"email"`
}
type CreateMailingListSubscriberPayload struct {
MailingListSubscriberEdge *MailingListSubscriberEdge `json:"mailingListSubscriberEdge"`
}
type CreateMeasureInput struct {
OrganizationID gid.GID `json:"organizationId"`
Name string `json:"name"`
@@ -959,6 +969,14 @@ type DeleteFrameworkPayload struct {
DeletedFrameworkID gid.GID `json:"deletedFrameworkId"`
}
type DeleteMailingListSubscriberInput struct {
ID gid.GID `json:"id"`
}
type DeleteMailingListSubscriberPayload struct {
DeletedMailingListSubscriberID gid.GID `json:"deletedMailingListSubscriberId"`
}
type DeleteMeasureInput struct {
MeasureID gid.GID `json:"measureId"`
}
@@ -1431,6 +1449,32 @@ type ImportMeasurePayload struct {
MeasureEdges []*MeasureEdge `json:"measureEdges"`
}
type MailingList struct {
ID gid.GID `json:"id"`
ReplyTo *mail.Addr `json:"replyTo,omitempty"`
Subscribers *MailingListSubscriberConnection `json:"subscribers"`
}
func (MailingList) IsNode() {}
func (this MailingList) GetID() gid.GID { return this.ID }
type MailingListSubscriber struct {
ID gid.GID `json:"id"`
FullName string `json:"fullName"`
Email mail.Addr `json:"email"`
Status coredata.MailingListSubscriberStatus `json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (MailingListSubscriber) IsNode() {}
func (this MailingListSubscriber) GetID() gid.GID { return this.ID }
type MailingListSubscriberEdge struct {
Cursor page.CursorKey `json:"cursor"`
Node *MailingListSubscriber `json:"node"`
}
type Measure struct {
ID gid.GID `json:"id"`
Category string `json:"category"`
@@ -1899,26 +1943,6 @@ type TransferImpactAssessmentFilter struct {
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
}
type TrustCenter struct {
ID gid.GID `json:"id"`
Active bool `json:"active"`
LogoFileURL *string `json:"logoFileUrl,omitempty"`
DarkLogoFileURL *string `json:"darkLogoFileUrl,omitempty"`
NdaFileName *string `json:"ndaFileName,omitempty"`
NdaFileURL *string `json:"ndaFileUrl,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Organization *Organization `json:"organization"`
Accesses *TrustCenterAccessConnection `json:"accesses"`
References *TrustCenterReferenceConnection `json:"references"`
ComplianceFrameworks *ComplianceFrameworkConnection `json:"complianceFrameworks"`
ExternalUrls *ComplianceExternalURLConnection `json:"externalUrls"`
Permission bool `json:"permission"`
}
func (TrustCenter) IsNode() {}
func (this TrustCenter) GetID() gid.GID { return this.ID }
type TrustCenterAccess struct {
ID gid.GID `json:"id"`
NdaSignature *ElectronicSignature `json:"ndaSignature,omitempty"`
@@ -2148,6 +2172,15 @@ type UpdateFrameworkPayload struct {
Framework *Framework `json:"framework"`
}
type UpdateMailingListInput struct {
ID gid.GID `json:"id"`
ReplyTo *mail.Addr `json:"replyTo,omitempty"`
}
type UpdateMailingListPayload struct {
MailingList *MailingList `json:"mailingList"`
}
type UpdateMeasureInput struct {
ID gid.GID `json:"id"`
Name *string `json:"name,omitempty"`

View File

@@ -1524,6 +1524,47 @@ func (r *frameworkConnectionResolver) TotalCount(ctx context.Context, obj *types
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
}
// Subscribers is the resolver for the subscribers field on MailingList.
func (r *mailingListResolver) Subscribers(ctx context.Context, obj *types.MailingList, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.MailingListSubscriberConnection, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionMailingListSubscriberList); err != nil {
return nil, err
}
pageOrderBy := page.OrderBy[coredata.MailingListSubscriberOrderField]{
Field: coredata.MailingListSubscriberOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
result, err := r.MailmanService(ctx, obj.ID.TenantID()).ListSubscribers(ctx, obj.ID, cursor)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list mailing list subscribers", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewMailingListSubscriberConnection(result, r, obj.ID), nil
}
// TotalCount is the resolver for the totalCount field.
func (r *mailingListSubscriberConnectionResolver) TotalCount(ctx context.Context, obj *types.MailingListSubscriberConnection) (int, error) {
if err := r.authorize(ctx, obj.ParentID, probo.ActionMailingListSubscriberList); err != nil {
return 0, err
}
switch obj.Resolver.(type) {
case *mailingListResolver:
count, err := r.MailmanService(ctx, obj.ParentID.TenantID()).CountSubscribers(ctx, obj.ParentID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count mailing list subscribers", log.Error(err))
return 0, gqlutils.Internal(ctx)
}
return count, nil
}
panic(fmt.Errorf("not implemented: TotalCount for parent type %T", obj.Resolver))
}
// Evidences is the resolver for the evidences field.
func (r *measureResolver) Evidences(ctx context.Context, obj *types.Measure, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.EvidenceOrderBy) (*types.EvidenceConnection, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionEvidenceList); err != nil {
@@ -1987,6 +2028,56 @@ func (r *mutationResolver) DeleteTrustCenterAccess(ctx context.Context, input ty
}, nil
}
// UpdateMailingList is the resolver for the updateMailingList field.
func (r *mutationResolver) UpdateMailingList(ctx context.Context, input types.UpdateMailingListInput) (*types.UpdateMailingListPayload, error) {
if err := r.authorize(ctx, input.ID, probo.ActionMailingListUpdate); err != nil {
return nil, err
}
ml, err := r.MailmanService(ctx, input.ID.TenantID()).UpdateMailingList(ctx, input.ID, input.ReplyTo)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot update mailing list", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.UpdateMailingListPayload{
MailingList: types.NewMailingList(ml),
}, nil
}
// CreateMailingListSubscriber is the resolver for the createMailingListSubscriber field.
func (r *mutationResolver) CreateMailingListSubscriber(ctx context.Context, input types.CreateMailingListSubscriberInput) (*types.CreateMailingListSubscriberPayload, error) {
if err := r.authorize(ctx, input.MailingListID, probo.ActionMailingListSubscriberCreate); err != nil {
return nil, err
}
subscriber, err := r.MailmanService(ctx, input.MailingListID.TenantID()).CreateSubscriber(ctx, input.MailingListID, input.Email, input.FullName)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot create mailing list subscriber", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.CreateMailingListSubscriberPayload{
MailingListSubscriberEdge: types.NewMailingListSubscriberEdge(subscriber, coredata.MailingListSubscriberOrderFieldCreatedAt),
}, nil
}
// DeleteMailingListSubscriber is the resolver for the deleteMailingListSubscriber field.
func (r *mutationResolver) DeleteMailingListSubscriber(ctx context.Context, input types.DeleteMailingListSubscriberInput) (*types.DeleteMailingListSubscriberPayload, error) {
if err := r.authorize(ctx, input.ID, probo.ActionMailingListSubscriberDelete); err != nil {
return nil, err
}
if err := r.MailmanService(ctx, input.ID.TenantID()).DeleteSubscriber(ctx, input.ID); err != nil {
r.logger.ErrorCtx(ctx, "cannot delete mailing list subscriber", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.DeleteMailingListSubscriberPayload{
DeletedMailingListSubscriberID: input.ID,
}, nil
}
// CreateTrustCenterReference is the resolver for the createTrustCenterReference field.
func (r *mutationResolver) CreateTrustCenterReference(ctx context.Context, input types.CreateTrustCenterReferenceInput) (*types.CreateTrustCenterReferencePayload, error) {
if err := r.authorize(ctx, input.TrustCenterID, probo.ActionTrustCenterReferenceCreate); err != nil {
@@ -7990,6 +8081,31 @@ func (r *trustCenterResolver) ExternalUrls(ctx context.Context, obj *types.Trust
return types.NewComplianceExternalURLConnection(result), nil
}
// MailingList is the resolver for the mailingList field.
func (r *trustCenterResolver) MailingList(ctx context.Context, obj *types.TrustCenter) (*types.MailingList, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionMailingListSubscriberList); err != nil {
return nil, err
}
if obj.MailingList != nil {
return obj.MailingList, nil
}
prb := r.ProboService(ctx, obj.ID.TenantID())
ml, err := prb.TrustCenters.GetMailingList(ctx, obj.ID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get mailing list for trust center", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
if ml == nil {
return nil, nil
}
return types.NewMailingList(ml), nil
}
// Permission is the resolver for the permission field.
func (r *trustCenterResolver) Permission(ctx context.Context, obj *types.TrustCenter, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
@@ -9072,6 +9188,14 @@ func (r *Resolver) FrameworkConnection() schema.FrameworkConnectionResolver {
return &frameworkConnectionResolver{r}
}
// MailingList returns schema.MailingListResolver implementation.
func (r *Resolver) MailingList() schema.MailingListResolver { return &mailingListResolver{r} }
// MailingListSubscriberConnection returns schema.MailingListSubscriberConnectionResolver implementation.
func (r *Resolver) MailingListSubscriberConnection() schema.MailingListSubscriberConnectionResolver {
return &mailingListSubscriberConnectionResolver{r}
}
// Measure returns schema.MeasureResolver implementation.
func (r *Resolver) Measure() schema.MeasureResolver { return &measureResolver{r} }
@@ -9306,6 +9430,8 @@ type evidenceConnectionResolver struct{ *Resolver }
type fileResolver struct{ *Resolver }
type frameworkResolver struct{ *Resolver }
type frameworkConnectionResolver struct{ *Resolver }
type mailingListResolver struct{ *Resolver }
type mailingListSubscriberConnectionResolver struct{ *Resolver }
type measureResolver struct{ *Resolver }
type measureConnectionResolver struct{ *Resolver }
type meetingResolver struct{ *Resolver }

View File

@@ -21,6 +21,7 @@ import (
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailman"
"go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/trust/v1/schema"
@@ -29,12 +30,13 @@ import (
"go.probo.inc/probo/pkg/trust"
)
func NewGraphQLHandler(iamSvc *iam.Service, trustSvc *trust.Service, esignSvc *esign.Service, logger *log.Logger, baseURL *baseurl.BaseURL, cookieConfig securecookie.Config) http.Handler {
func NewGraphQLHandler(iamSvc *iam.Service, trustSvc *trust.Service, esignSvc *esign.Service, mailmanSvc *mailman.Service, logger *log.Logger, baseURL *baseurl.BaseURL, cookieConfig securecookie.Config, tokenSecret string) http.Handler {
config := schema.Config{
Resolvers: &Resolver{
iam: iamSvc,
trust: trustSvc,
esign: esignSvc,
mailman: mailmanSvc,
logger: logger,
baseURL: baseURL,
sessionCookie: authn.NewCookie(&cookieConfig),

View File

@@ -26,6 +26,7 @@ import (
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailman"
"go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/compliancepage"
@@ -39,7 +40,6 @@ type (
CookieDuration time.Duration
TokenDuration time.Duration
ReportURLDuration time.Duration
TokenSecret string
Scope string
TokenType string
CookieSecure bool
@@ -48,6 +48,7 @@ type (
Resolver struct {
trust *trust.Service
esign *esign.Service
mailman *mailman.Service
logger *log.Logger
iam *iam.Service
sessionCookie *authn.Cookie
@@ -78,7 +79,9 @@ func NewMux(
iamSvc *iam.Service,
trustSvc *trust.Service,
esignSvc *esign.Service,
mailmanSvc *mailman.Service,
cookieConfig securecookie.Config,
tokenSecret string,
baseURL *baseurl.BaseURL,
) *chi.Mux {
r := chi.NewMux()
@@ -86,17 +89,17 @@ func NewMux(
r.Use(compliancepage.NewCompliancePagePresenceMiddleware())
r.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig))
graphqlHandler := NewGraphQLHandler(iamSvc, trustSvc, esignSvc, logger, baseURL, cookieConfig)
graphqlHandler := NewGraphQLHandler(iamSvc, trustSvc, esignSvc, mailmanSvc, logger, baseURL, cookieConfig, tokenSecret)
r.Handle("/graphql", graphqlHandler)
return r
}
func (r *Resolver) RootTrustService(ctx context.Context) *trust.TenantService {
return r.trust.WithTenant(gid.NewTenantID())
}
func (r *Resolver) TrustService(ctx context.Context, tenantID gid.TenantID) *trust.TenantService {
return r.trust.WithTenant(tenantID)
}
func (r *Resolver) MailmanService(ctx context.Context, tenantID gid.TenantID) *mailman.TenantService {
return r.mailman.WithTenant(tenantID)
}

View File

@@ -530,6 +530,7 @@ type TrustCenter implements Node {
nonDisclosureAgreement: NonDisclosureAgreement @goField(forceResolver: true)
viewerSubscription: MailingListSubscriber @goField(forceResolver: true)
organization: Organization! @goField(forceResolver: true)
documents(
@@ -892,4 +893,41 @@ type Mutation {
recordSigningEvent(
input: RecordSigningEventInput!
): RecordSigningEventPayload @session(required: PRESENT)
subscribeToMailingList: SubscribeToMailingListPayload! @session(required: PRESENT)
unsubscribeFromMailingList: UnsubscribeFromMailingListPayload! @session(required: PRESENT)
}
type SubscribeToMailingListPayload {
subscription: MailingListSubscriber!
}
type UnsubscribeFromMailingListPayload {
deletedMailingListSubscriberId: ID
}
enum MailingListSubscriberStatus
@goModel(
model: "go.probo.inc/probo/pkg/coredata.MailingListSubscriberStatus"
) {
PENDING
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MailingListSubscriberStatusPending"
)
CONFIRMED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MailingListSubscriberStatusConfirmed"
)
}
type MailingListSubscriber implements Node
@goModel(
model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.MailingListSubscriber"
) {
id: ID!
email: EmailAddr!
status: MailingListSubscriberStatus!
createdAt: Datetime!
updatedAt: Datetime!
}

View File

@@ -172,6 +172,14 @@ type ComplexityRoot struct {
UpdatedAt func(childComplexity int) int
}
MailingListSubscriber struct {
CreatedAt func(childComplexity int) int
Email func(childComplexity int) int
ID func(childComplexity int) int
Status func(childComplexity int) int
UpdatedAt func(childComplexity int) int
}
Mutation struct {
AcceptElectronicSignature func(childComplexity int, input types.AcceptElectronicSignatureInput) int
ExportDocumentPDF func(childComplexity int, input types.ExportDocumentPDFInput) int
@@ -183,6 +191,8 @@ type ComplexityRoot struct {
RequestReportAccess func(childComplexity int, input types.RequestReportAccessInput) int
RequestTrustCenterFileAccess func(childComplexity int, input types.RequestTrustCenterFileAccessInput) int
SendMagicLink func(childComplexity int, input types.SendMagicLinkInput) int
SubscribeToMailingList func(childComplexity int) int
UnsubscribeFromMailingList func(childComplexity int) int
UpdateFullName func(childComplexity int, input types.UpdateFullNameInput) int
VerifyMagicLink func(childComplexity int, input types.VerifyMagicLinkInput) int
}
@@ -247,6 +257,10 @@ type ComplexityRoot struct {
Success func(childComplexity int) int
}
SubscribeToMailingListPayload struct {
Subscription func(childComplexity int) int
}
TrustCenter struct {
Active func(childComplexity int) int
Audits func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
@@ -262,6 +276,7 @@ type ComplexityRoot struct {
Slug func(childComplexity int) int
TrustCenterFiles func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
Vendors func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
ViewerSubscription func(childComplexity int) int
}
TrustCenterAccess struct {
@@ -308,6 +323,10 @@ type ComplexityRoot struct {
Node func(childComplexity int) int
}
UnsubscribeFromMailingListPayload struct {
DeletedMailingListSubscriberID func(childComplexity int) int
}
UpdateFullNamePayload struct {
Success func(childComplexity int) int
}
@@ -366,6 +385,8 @@ type MutationResolver interface {
RequestTrustCenterFileAccess(ctx context.Context, input types.RequestTrustCenterFileAccessInput) (*types.RequestFileAccessPayload, error)
AcceptElectronicSignature(ctx context.Context, input types.AcceptElectronicSignatureInput) (*types.AcceptElectronicSignaturePayload, error)
RecordSigningEvent(ctx context.Context, input types.RecordSigningEventInput) (*types.RecordSigningEventPayload, error)
SubscribeToMailingList(ctx context.Context) (*types.SubscribeToMailingListPayload, error)
UnsubscribeFromMailingList(ctx context.Context) (*types.UnsubscribeFromMailingListPayload, error)
}
type NonDisclosureAgreementResolver interface {
FileURL(ctx context.Context, obj *types.NonDisclosureAgreement) (string, error)
@@ -387,6 +408,7 @@ type TrustCenterResolver interface {
LogoFileURL(ctx context.Context, obj *types.TrustCenter) (*string, error)
DarkLogoFileURL(ctx context.Context, obj *types.TrustCenter) (*string, error)
NonDisclosureAgreement(ctx context.Context, obj *types.TrustCenter) (*types.NonDisclosureAgreement, error)
ViewerSubscription(ctx context.Context, obj *types.TrustCenter) (*types.MailingListSubscriber, error)
Organization(ctx context.Context, obj *types.TrustCenter) (*types.Organization, error)
Documents(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.DocumentConnection, error)
Audits(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.AuditConnection, error)
@@ -771,6 +793,37 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.ComplexityRoot.Identity.UpdatedAt(childComplexity), true
case "MailingListSubscriber.createdAt":
if e.ComplexityRoot.MailingListSubscriber.CreatedAt == nil {
break
}
return e.ComplexityRoot.MailingListSubscriber.CreatedAt(childComplexity), true
case "MailingListSubscriber.email":
if e.ComplexityRoot.MailingListSubscriber.Email == nil {
break
}
return e.ComplexityRoot.MailingListSubscriber.Email(childComplexity), true
case "MailingListSubscriber.id":
if e.ComplexityRoot.MailingListSubscriber.ID == nil {
break
}
return e.ComplexityRoot.MailingListSubscriber.ID(childComplexity), true
case "MailingListSubscriber.status":
if e.ComplexityRoot.MailingListSubscriber.Status == nil {
break
}
return e.ComplexityRoot.MailingListSubscriber.Status(childComplexity), true
case "MailingListSubscriber.updatedAt":
if e.ComplexityRoot.MailingListSubscriber.UpdatedAt == nil {
break
}
return e.ComplexityRoot.MailingListSubscriber.UpdatedAt(childComplexity), true
case "Mutation.acceptElectronicSignature":
if e.ComplexityRoot.Mutation.AcceptElectronicSignature == nil {
break
@@ -876,6 +929,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
}
return e.ComplexityRoot.Mutation.SendMagicLink(childComplexity, args["input"].(types.SendMagicLinkInput)), true
case "Mutation.subscribeToMailingList":
if e.ComplexityRoot.Mutation.SubscribeToMailingList == nil {
break
}
return e.ComplexityRoot.Mutation.SubscribeToMailingList(childComplexity), true
case "Mutation.unsubscribeFromMailingList":
if e.ComplexityRoot.Mutation.UnsubscribeFromMailingList == nil {
break
}
return e.ComplexityRoot.Mutation.UnsubscribeFromMailingList(childComplexity), true
case "Mutation.updateFullName":
if e.ComplexityRoot.Mutation.UpdateFullName == nil {
break
@@ -1078,6 +1143,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.ComplexityRoot.SendMagicLinkPayload.Success(childComplexity), true
case "SubscribeToMailingListPayload.subscription":
if e.ComplexityRoot.SubscribeToMailingListPayload.Subscription == nil {
break
}
return e.ComplexityRoot.SubscribeToMailingListPayload.Subscription(childComplexity), true
case "TrustCenter.active":
if e.ComplexityRoot.TrustCenter.Active == nil {
break
@@ -1197,6 +1269,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
}
return e.ComplexityRoot.TrustCenter.Vendors(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey)), true
case "TrustCenter.viewerSubscription":
if e.ComplexityRoot.TrustCenter.ViewerSubscription == nil {
break
}
return e.ComplexityRoot.TrustCenter.ViewerSubscription(childComplexity), true
case "TrustCenterAccess.createdAt":
if e.ComplexityRoot.TrustCenterAccess.CreatedAt == nil {
@@ -1343,6 +1421,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.ComplexityRoot.TrustCenterReferenceEdge.Node(childComplexity), true
case "UnsubscribeFromMailingListPayload.deletedMailingListSubscriberId":
if e.ComplexityRoot.UnsubscribeFromMailingListPayload.DeletedMailingListSubscriberID == nil {
break
}
return e.ComplexityRoot.UnsubscribeFromMailingListPayload.DeletedMailingListSubscriberID(childComplexity), true
case "UpdateFullNamePayload.success":
if e.ComplexityRoot.UpdateFullNamePayload.Success == nil {
break
@@ -2058,6 +2143,7 @@ type TrustCenter implements Node {
nonDisclosureAgreement: NonDisclosureAgreement @goField(forceResolver: true)
viewerSubscription: MailingListSubscriber @goField(forceResolver: true)
organization: Organization! @goField(forceResolver: true)
documents(
@@ -2420,6 +2506,43 @@ type Mutation {
recordSigningEvent(
input: RecordSigningEventInput!
): RecordSigningEventPayload @session(required: PRESENT)
subscribeToMailingList: SubscribeToMailingListPayload! @session(required: PRESENT)
unsubscribeFromMailingList: UnsubscribeFromMailingListPayload! @session(required: PRESENT)
}
type SubscribeToMailingListPayload {
subscription: MailingListSubscriber!
}
type UnsubscribeFromMailingListPayload {
deletedMailingListSubscriberId: ID
}
enum MailingListSubscriberStatus
@goModel(
model: "go.probo.inc/probo/pkg/coredata.MailingListSubscriberStatus"
) {
PENDING
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MailingListSubscriberStatusPending"
)
CONFIRMED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.MailingListSubscriberStatusConfirmed"
)
}
type MailingListSubscriber implements Node
@goModel(
model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.MailingListSubscriber"
) {
id: ID!
email: EmailAddr!
status: MailingListSubscriberStatus!
createdAt: Datetime!
updatedAt: Datetime!
}
`, BuiltIn: false},
{Name: "../../../../gqlutils/directives/session/schema.graphql", Input: `# Session directive for GraphQL APIs
@@ -4698,6 +4821,151 @@ func (ec *executionContext) fieldContext_Identity_updatedAt(_ context.Context, f
return fc, nil
}
func (ec *executionContext) _MailingListSubscriber_id(ctx context.Context, field graphql.CollectedField, obj *types.MailingListSubscriber) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_MailingListSubscriber_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_MailingListSubscriber_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "MailingListSubscriber",
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) _MailingListSubscriber_email(ctx context.Context, field graphql.CollectedField, obj *types.MailingListSubscriber) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_MailingListSubscriber_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_MailingListSubscriber_email(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "MailingListSubscriber",
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) _MailingListSubscriber_status(ctx context.Context, field graphql.CollectedField, obj *types.MailingListSubscriber) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_MailingListSubscriber_status,
func(ctx context.Context) (any, error) {
return obj.Status, nil
},
nil,
ec.marshalNMailingListSubscriberStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMailingListSubscriberStatus,
true,
true,
)
}
func (ec *executionContext) fieldContext_MailingListSubscriber_status(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "MailingListSubscriber",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type MailingListSubscriberStatus does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _MailingListSubscriber_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.MailingListSubscriber) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_MailingListSubscriber_createdAt,
func(ctx context.Context) (any, error) {
return obj.CreatedAt, nil
},
nil,
ec.marshalNDatetime2timeᚐTime,
true,
true,
)
}
func (ec *executionContext) fieldContext_MailingListSubscriber_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "MailingListSubscriber",
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) _MailingListSubscriber_updatedAt(ctx context.Context, field graphql.CollectedField, obj *types.MailingListSubscriber) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_MailingListSubscriber_updatedAt,
func(ctx context.Context) (any, error) {
return obj.UpdatedAt, nil
},
nil,
ec.marshalNDatetime2timeᚐTime,
true,
true,
)
}
func (ec *executionContext) fieldContext_MailingListSubscriber_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "MailingListSubscriber",
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_sendMagicLink(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
@@ -5491,6 +5759,108 @@ func (ec *executionContext) fieldContext_Mutation_recordSigningEvent(ctx context
return fc, nil
}
func (ec *executionContext) _Mutation_subscribeToMailingList(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Mutation_subscribeToMailingList,
func(ctx context.Context) (any, error) {
return ec.Resolvers.Mutation().SubscribeToMailingList(ctx)
},
func(ctx context.Context, next graphql.Resolver) graphql.Resolver {
directive0 := next
directive1 := func(ctx context.Context) (any, error) {
required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx, "PRESENT")
if err != nil {
var zeroVal *types.SubscribeToMailingListPayload
return zeroVal, err
}
if ec.Directives.Session == nil {
var zeroVal *types.SubscribeToMailingListPayload
return zeroVal, errors.New("directive session is not implemented")
}
return ec.Directives.Session(ctx, nil, directive0, required)
}
next = directive1
return next
},
ec.marshalNSubscribeToMailingListPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSubscribeToMailingListPayload,
true,
true,
)
}
func (ec *executionContext) fieldContext_Mutation_subscribeToMailingList(_ 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 "subscription":
return ec.fieldContext_SubscribeToMailingListPayload_subscription(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type SubscribeToMailingListPayload", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _Mutation_unsubscribeFromMailingList(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Mutation_unsubscribeFromMailingList,
func(ctx context.Context) (any, error) {
return ec.Resolvers.Mutation().UnsubscribeFromMailingList(ctx)
},
func(ctx context.Context, next graphql.Resolver) graphql.Resolver {
directive0 := next
directive1 := func(ctx context.Context) (any, error) {
required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋgqlutilsᚋdirectivesᚋsessionᚐSessionRequirement(ctx, "PRESENT")
if err != nil {
var zeroVal *types.UnsubscribeFromMailingListPayload
return zeroVal, err
}
if ec.Directives.Session == nil {
var zeroVal *types.UnsubscribeFromMailingListPayload
return zeroVal, errors.New("directive session is not implemented")
}
return ec.Directives.Session(ctx, nil, directive0, required)
}
next = directive1
return next
},
ec.marshalNUnsubscribeFromMailingListPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐUnsubscribeFromMailingListPayload,
true,
true,
)
}
func (ec *executionContext) fieldContext_Mutation_unsubscribeFromMailingList(_ 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 "deletedMailingListSubscriberId":
return ec.fieldContext_UnsubscribeFromMailingListPayload_deletedMailingListSubscriberId(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type UnsubscribeFromMailingListPayload", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _NonDisclosureAgreement_fileName(ctx context.Context, field graphql.CollectedField, obj *types.NonDisclosureAgreement) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
@@ -6035,6 +6405,8 @@ func (ec *executionContext) fieldContext_Query_currentTrustCenter(_ context.Cont
return ec.fieldContext_TrustCenter_darkLogoFileUrl(ctx, field)
case "nonDisclosureAgreement":
return ec.fieldContext_TrustCenter_nonDisclosureAgreement(ctx, field)
case "viewerSubscription":
return ec.fieldContext_TrustCenter_viewerSubscription(ctx, field)
case "organization":
return ec.fieldContext_TrustCenter_organization(ctx, field)
case "documents":
@@ -6547,6 +6919,47 @@ func (ec *executionContext) fieldContext_SendMagicLinkPayload_success(_ context.
return fc, nil
}
func (ec *executionContext) _SubscribeToMailingListPayload_subscription(ctx context.Context, field graphql.CollectedField, obj *types.SubscribeToMailingListPayload) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_SubscribeToMailingListPayload_subscription,
func(ctx context.Context) (any, error) {
return obj.Subscription, nil
},
nil,
ec.marshalNMailingListSubscriber2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐMailingListSubscriber,
true,
true,
)
}
func (ec *executionContext) fieldContext_SubscribeToMailingListPayload_subscription(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "SubscribeToMailingListPayload",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "id":
return ec.fieldContext_MailingListSubscriber_id(ctx, field)
case "email":
return ec.fieldContext_MailingListSubscriber_email(ctx, field)
case "status":
return ec.fieldContext_MailingListSubscriber_status(ctx, field)
case "createdAt":
return ec.fieldContext_MailingListSubscriber_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_MailingListSubscriber_updatedAt(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type MailingListSubscriber", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _TrustCenter_id(ctx context.Context, field graphql.CollectedField, obj *types.TrustCenter) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
@@ -6729,6 +7142,47 @@ func (ec *executionContext) fieldContext_TrustCenter_nonDisclosureAgreement(_ co
return fc, nil
}
func (ec *executionContext) _TrustCenter_viewerSubscription(ctx context.Context, field graphql.CollectedField, obj *types.TrustCenter) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_TrustCenter_viewerSubscription,
func(ctx context.Context) (any, error) {
return ec.Resolvers.TrustCenter().ViewerSubscription(ctx, obj)
},
nil,
ec.marshalOMailingListSubscriber2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐMailingListSubscriber,
true,
false,
)
}
func (ec *executionContext) fieldContext_TrustCenter_viewerSubscription(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "TrustCenter",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "id":
return ec.fieldContext_MailingListSubscriber_id(ctx, field)
case "email":
return ec.fieldContext_MailingListSubscriber_email(ctx, field)
case "status":
return ec.fieldContext_MailingListSubscriber_status(ctx, field)
case "createdAt":
return ec.fieldContext_MailingListSubscriber_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_MailingListSubscriber_updatedAt(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type MailingListSubscriber", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _TrustCenter_organization(ctx context.Context, field graphql.CollectedField, obj *types.TrustCenter) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
@@ -7951,6 +8405,35 @@ func (ec *executionContext) fieldContext_TrustCenterReferenceEdge_node(_ context
return fc, nil
}
func (ec *executionContext) _UnsubscribeFromMailingListPayload_deletedMailingListSubscriberId(ctx context.Context, field graphql.CollectedField, obj *types.UnsubscribeFromMailingListPayload) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_UnsubscribeFromMailingListPayload_deletedMailingListSubscriberId,
func(ctx context.Context) (any, error) {
return obj.DeletedMailingListSubscriberID, nil
},
nil,
ec.marshalOID2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID,
true,
false,
)
}
func (ec *executionContext) fieldContext_UnsubscribeFromMailingListPayload_deletedMailingListSubscriberId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "UnsubscribeFromMailingListPayload",
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) _UpdateFullNamePayload_success(ctx context.Context, field graphql.CollectedField, obj *types.UpdateFullNamePayload) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
@@ -10218,6 +10701,13 @@ func (ec *executionContext) _Node(ctx context.Context, sel ast.SelectionSet, obj
return graphql.Null
}
return ec._Organization(ctx, sel, obj)
case types.MailingListSubscriber:
return ec._MailingListSubscriber(ctx, sel, &obj)
case *types.MailingListSubscriber:
if obj == nil {
return graphql.Null
}
return ec._MailingListSubscriber(ctx, sel, obj)
case types.Identity:
return ec._Identity(ctx, sel, &obj)
case *types.Identity:
@@ -11438,6 +11928,65 @@ func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet,
return out
}
var mailingListSubscriberImplementors = []string{"MailingListSubscriber", "Node"}
func (ec *executionContext) _MailingListSubscriber(ctx context.Context, sel ast.SelectionSet, obj *types.MailingListSubscriber) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, mailingListSubscriberImplementors)
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("MailingListSubscriber")
case "id":
out.Values[i] = ec._MailingListSubscriber_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "email":
out.Values[i] = ec._MailingListSubscriber_email(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "status":
out.Values[i] = ec._MailingListSubscriber_status(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "createdAt":
out.Values[i] = ec._MailingListSubscriber_createdAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "updatedAt":
out.Values[i] = ec._MailingListSubscriber_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"}
func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
@@ -11526,6 +12075,20 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_recordSigningEvent(ctx, field)
})
case "subscribeToMailingList":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_subscribeToMailingList(ctx, field)
})
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "unsubscribeFromMailingList":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_unsubscribeFromMailingList(ctx, field)
})
if out.Values[i] == graphql.Null {
out.Invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@@ -12238,6 +12801,45 @@ func (ec *executionContext) _SendMagicLinkPayload(ctx context.Context, sel ast.S
return out
}
var subscribeToMailingListPayloadImplementors = []string{"SubscribeToMailingListPayload"}
func (ec *executionContext) _SubscribeToMailingListPayload(ctx context.Context, sel ast.SelectionSet, obj *types.SubscribeToMailingListPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, subscribeToMailingListPayloadImplementors)
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("SubscribeToMailingListPayload")
case "subscription":
out.Values[i] = ec._SubscribeToMailingListPayload_subscription(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"}
func (ec *executionContext) _TrustCenter(ctx context.Context, sel ast.SelectionSet, obj *types.TrustCenter) graphql.Marshaler {
@@ -12362,6 +12964,39 @@ func (ec *executionContext) _TrustCenter(ctx context.Context, sel ast.SelectionS
continue
}
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
case "viewerSubscription":
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._TrustCenter_viewerSubscription(ctx, field, obj)
return res
}
if field.Deferrable != nil {
dfs, ok := deferred[field.Deferrable.Label]
di := 0
if ok {
dfs.AddField(field)
di = len(dfs.Values) - 1
} else {
dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
deferred[field.Deferrable.Label] = dfs
}
dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
return innerFunc(ctx, dfs)
})
// don't run the out.Concurrently() call below
out.Values[i] = graphql.Null
continue
}
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
case "organization":
field := field
@@ -13114,6 +13749,42 @@ func (ec *executionContext) _TrustCenterReferenceEdge(ctx context.Context, sel a
return out
}
var unsubscribeFromMailingListPayloadImplementors = []string{"UnsubscribeFromMailingListPayload"}
func (ec *executionContext) _UnsubscribeFromMailingListPayload(ctx context.Context, sel ast.SelectionSet, obj *types.UnsubscribeFromMailingListPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, unsubscribeFromMailingListPayloadImplementors)
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("UnsubscribeFromMailingListPayload")
case "deletedMailingListSubscriberId":
out.Values[i] = ec._UnsubscribeFromMailingListPayload_deletedMailingListSubscriberId(ctx, field, obj)
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 updateFullNamePayloadImplementors = []string{"UpdateFullNamePayload"}
func (ec *executionContext) _UpdateFullNamePayload(ctx context.Context, sel ast.SelectionSet, obj *types.UpdateFullNamePayload) graphql.Marshaler {
@@ -14823,6 +15494,44 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti
return res
}
func (ec *executionContext) marshalNMailingListSubscriber2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐMailingListSubscriber(ctx context.Context, sel ast.SelectionSet, v *types.MailingListSubscriber) 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._MailingListSubscriber(ctx, sel, v)
}
func (ec *executionContext) unmarshalNMailingListSubscriberStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMailingListSubscriberStatus(ctx context.Context, v any) (coredata.MailingListSubscriberStatus, error) {
tmp, err := graphql.UnmarshalString(v)
res := unmarshalNMailingListSubscriberStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMailingListSubscriberStatus[tmp]
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNMailingListSubscriberStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMailingListSubscriberStatus(ctx context.Context, sel ast.SelectionSet, v coredata.MailingListSubscriberStatus) graphql.Marshaler {
_ = sel
res := graphql.MarshalString(marshalNMailingListSubscriberStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMailingListSubscriberStatus[v])
if res == graphql.Null {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow")
}
}
return res
}
var (
unmarshalNMailingListSubscriberStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMailingListSubscriberStatus = map[string]coredata.MailingListSubscriberStatus{
"PENDING": coredata.MailingListSubscriberStatusPending,
"CONFIRMED": coredata.MailingListSubscriberStatusConfirmed,
}
marshalNMailingListSubscriberStatus2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMailingListSubscriberStatus = map[coredata.MailingListSubscriberStatus]string{
coredata.MailingListSubscriberStatusPending: "PENDING",
coredata.MailingListSubscriberStatusConfirmed: "CONFIRMED",
}
)
func (ec *executionContext) marshalNNode2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐNode(ctx context.Context, sel ast.SelectionSet, v types.Node) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
@@ -14988,6 +15697,20 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S
return res
}
func (ec *executionContext) marshalNSubscribeToMailingListPayload2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSubscribeToMailingListPayload(ctx context.Context, sel ast.SelectionSet, v types.SubscribeToMailingListPayload) graphql.Marshaler {
return ec._SubscribeToMailingListPayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNSubscribeToMailingListPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐSubscribeToMailingListPayload(ctx context.Context, sel ast.SelectionSet, v *types.SubscribeToMailingListPayload) 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._SubscribeToMailingListPayload(ctx, sel, v)
}
func (ec *executionContext) marshalNTrustCenterAccess2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐTrustCenterAccess(ctx context.Context, sel ast.SelectionSet, v *types.TrustCenterAccess) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
@@ -15098,6 +15821,20 @@ func (ec *executionContext) marshalNTrustCenterReferenceEdge2ᚖgoᚗproboᚗinc
return ec._TrustCenterReferenceEdge(ctx, sel, v)
}
func (ec *executionContext) marshalNUnsubscribeFromMailingListPayload2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐUnsubscribeFromMailingListPayload(ctx context.Context, sel ast.SelectionSet, v types.UnsubscribeFromMailingListPayload) graphql.Marshaler {
return ec._UnsubscribeFromMailingListPayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNUnsubscribeFromMailingListPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐUnsubscribeFromMailingListPayload(ctx context.Context, sel ast.SelectionSet, v *types.UnsubscribeFromMailingListPayload) 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._UnsubscribeFromMailingListPayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNUpdateFullNameInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐUpdateFullNameInput(ctx context.Context, v any) (types.UpdateFullNameInput, error) {
res, err := ec.unmarshalInputUpdateFullNameInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
@@ -15468,6 +16205,24 @@ func (ec *executionContext) marshalOElectronicSignature2ᚖgoᚗproboᚗincᚋpr
return ec._ElectronicSignature(ctx, sel, v)
}
func (ec *executionContext) unmarshalOID2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx context.Context, v any) (*gid.GID, error) {
if v == nil {
return nil, nil
}
res, err := gid1.UnmarshalGIDScalar(v)
return &res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalOID2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx context.Context, sel ast.SelectionSet, v *gid.GID) graphql.Marshaler {
if v == nil {
return graphql.Null
}
_ = sel
_ = ctx
res := gid1.MarshalGIDScalar(*v)
return res
}
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 graphql.Null
@@ -15493,6 +16248,13 @@ func (ec *executionContext) marshalOInt2ᚖint(ctx context.Context, sel ast.Sele
return res
}
func (ec *executionContext) marshalOMailingListSubscriber2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐMailingListSubscriber(ctx context.Context, sel ast.SelectionSet, v *types.MailingListSubscriber) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return ec._MailingListSubscriber(ctx, sel, v)
}
func (ec *executionContext) marshalONonDisclosureAgreement2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐNonDisclosureAgreement(ctx context.Context, sel ast.SelectionSet, v *types.NonDisclosureAgreement) graphql.Marshaler {
if v == nil {
return graphql.Null

View File

@@ -0,0 +1,44 @@
// 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 types
import (
"time"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/mail"
)
type MailingListSubscriber struct {
ID gid.GID `json:"id"`
Email mail.Addr `json:"email"`
Status coredata.MailingListSubscriberStatus `json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (MailingListSubscriber) IsNode() {}
func (m MailingListSubscriber) GetID() gid.GID { return m.ID }
func NewMailingListSubscriber(s *coredata.MailingListSubscriber) *MailingListSubscriber {
return &MailingListSubscriber{
ID: s.ID,
Email: s.Email,
Status: s.Status,
CreatedAt: s.CreatedAt,
UpdatedAt: s.UpdatedAt,
}
}

View File

@@ -241,6 +241,10 @@ type SendMagicLinkPayload struct {
Success bool `json:"success"`
}
type SubscribeToMailingListPayload struct {
Subscription *MailingListSubscriber `json:"subscription"`
}
type TrustCenter struct {
ID gid.GID `json:"id"`
Active bool `json:"active"`
@@ -248,6 +252,7 @@ type TrustCenter struct {
LogoFileURL *string `json:"logoFileUrl,omitempty"`
DarkLogoFileURL *string `json:"darkLogoFileUrl,omitempty"`
NonDisclosureAgreement *NonDisclosureAgreement `json:"nonDisclosureAgreement,omitempty"`
ViewerSubscription *MailingListSubscriber `json:"viewerSubscription,omitempty"`
Organization *Organization `json:"organization"`
Documents *DocumentConnection `json:"documents"`
Audits *AuditConnection `json:"audits"`
@@ -314,6 +319,10 @@ type TrustCenterReferenceEdge struct {
Node *TrustCenterReference `json:"node"`
}
type UnsubscribeFromMailingListPayload struct {
DeletedMailingListSubscriberID *gid.GID `json:"deletedMailingListSubscriberId,omitempty"`
}
type UpdateFullNameInput struct {
FullName string `json:"fullName"`
}

View File

@@ -19,6 +19,7 @@ import (
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailman"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/saferedirect"
"go.probo.inc/probo/pkg/server/api/authn"
@@ -694,6 +695,62 @@ func (r *mutationResolver) RecordSigningEvent(ctx context.Context, input types.R
return &types.RecordSigningEventPayload{Success: true}, nil
}
// SubscribeToMailingList is the resolver for the subscribeToMailingList field.
func (r *mutationResolver) SubscribeToMailingList(ctx context.Context) (*types.SubscribeToMailingListPayload, error) {
trustCenter := compliancepage.CompliancePageFromContext(ctx)
if trustCenter.MailingListID == nil {
return nil, gqlutils.NotFoundf(ctx, "mailing list not found")
}
mlSvc := r.MailmanService(ctx, trustCenter.ID.TenantID())
identity := authn.IdentityFromContext(ctx)
subscriber, err := mlSvc.CreateSubscriber(ctx, *trustCenter.MailingListID, identity.EmailAddress, identity.FullName)
if err != nil {
if errors.Is(err, mailman.ErrSubscriberAlreadyExist) {
subscriber, err = mlSvc.GetSubscriber(ctx, *trustCenter.MailingListID, identity.EmailAddress)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get existing mailing list subscription", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
} else {
r.logger.ErrorCtx(ctx, "cannot subscribe to mailing list", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
}
return &types.SubscribeToMailingListPayload{
Subscription: types.NewMailingListSubscriber(subscriber),
}, nil
}
// UnsubscribeFromMailingList is the resolver for the unsubscribeFromMailingList field.
func (r *mutationResolver) UnsubscribeFromMailingList(ctx context.Context) (*types.UnsubscribeFromMailingListPayload, error) {
trustCenter := compliancepage.CompliancePageFromContext(ctx)
if trustCenter.MailingListID == nil {
return nil, gqlutils.NotFoundf(ctx, "mailing list not found")
}
mlSvc := r.MailmanService(ctx, trustCenter.ID.TenantID())
identity := authn.IdentityFromContext(ctx)
subscriber, err := mlSvc.GetSubscriber(ctx, *trustCenter.MailingListID, identity.EmailAddress)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get mailing list subscription", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
if subscriber == nil {
return nil, gqlutils.NotFoundf(ctx, "mailing list subscription not found")
}
if err := mlSvc.DeleteSubscriber(ctx, subscriber.ID); err != nil {
r.logger.ErrorCtx(ctx, "cannot unsubscribe from mailing list", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.UnsubscribeFromMailingListPayload{DeletedMailingListSubscriberID: &subscriber.ID}, nil
}
// FileURL is the resolver for the fileUrl field.
func (r *nonDisclosureAgreementResolver) FileURL(ctx context.Context, obj *types.NonDisclosureAgreement) (string, error) {
trustCenter := compliancepage.CompliancePageFromContext(ctx)
@@ -983,6 +1040,31 @@ func (r *trustCenterResolver) NonDisclosureAgreement(ctx context.Context, obj *t
return types.NewNonDisclosureAgreement(file), nil
}
// ViewerSubscription is the resolver for the viewerSubscription field.
func (r *trustCenterResolver) ViewerSubscription(ctx context.Context, obj *types.TrustCenter) (*types.MailingListSubscriber, error) {
trustCenter := compliancepage.CompliancePageFromContext(ctx)
if trustCenter.MailingListID == nil {
return nil, nil
}
identity := authn.IdentityFromContext(ctx)
if identity == nil {
return nil, nil
}
subscriber, err := r.MailmanService(ctx, trustCenter.ID.TenantID()).GetSubscriber(ctx, *trustCenter.MailingListID, identity.EmailAddress)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get mailing list subscription", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
if subscriber == nil {
return nil, nil
}
return types.NewMailingListSubscriber(subscriber), nil
}
// Organization is the resolver for the organization field.
func (r *trustCenterResolver) Organization(ctx context.Context, obj *types.TrustCenter) (*types.Organization, error) {
return obj.Organization, nil

View File

@@ -27,6 +27,7 @@ import (
"go.probo.inc/probo/pkg/connector"
"go.probo.inc/probo/pkg/esign"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailman"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server/api"
@@ -46,6 +47,7 @@ type Config struct {
Trust *trust.Service
ESign *esign.Service
Slack *slack.Service
Mailman *mailman.Service
Cookie securecookie.Config
TokenSecret string
ConnectorRegistry *connector.ConnectorRegistry
@@ -74,6 +76,7 @@ func NewServer(cfg Config) (*Server, error) {
Trust: cfg.Trust,
ESign: cfg.ESign,
Slack: cfg.Slack,
Mailman: cfg.Mailman,
Cookie: cfg.Cookie,
TokenSecret: cfg.TokenSecret,
ConnectorRegistry: cfg.ConnectorRegistry,

View File

@@ -333,3 +333,36 @@ func (s *TrustCenterService) EmailPresenterConfig(ctx context.Context, complianc
return emailPresenterCfg, nil
}
func (s *TrustCenterService) GetMailingList(
ctx context.Context,
trustCenterID gid.GID,
) (*coredata.MailingList, error) {
var mailingList *coredata.MailingList
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
trustCenter := &coredata.TrustCenter{}
if err := trustCenter.LoadByID(ctx, conn, s.svc.scope, trustCenterID); err != nil {
return fmt.Errorf("cannot load trust center: %w", err)
}
if trustCenter.MailingListID == nil {
return nil
}
mailingList = &coredata.MailingList{}
if err := mailingList.LoadByID(ctx, conn, s.svc.scope, *trustCenter.MailingListID); err != nil {
return fmt.Errorf("cannot load mailing list: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return mailingList, nil
}