Send mailing list emails
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<7e30e59b53799ccae49199d7b46f0320>>
|
* @generated SignedSource<<1ddc7eb54e71de0d95fb5af865fc0348>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -21,6 +21,7 @@ export type CompliancePageMailingListPageQuery$data = {
|
|||||||
readonly mailingList: {
|
readonly mailingList: {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly replyTo: string | null | undefined;
|
readonly replyTo: string | null | undefined;
|
||||||
|
readonly " $fragmentSpreads": FragmentRefs<"CompliancePageUpdatesListFragment">;
|
||||||
} | null | undefined;
|
} | null | undefined;
|
||||||
readonly " $fragmentSpreads": FragmentRefs<"CompliancePageMailingListFragment">;
|
readonly " $fragmentSpreads": FragmentRefs<"CompliancePageMailingListFragment">;
|
||||||
};
|
};
|
||||||
@@ -77,7 +78,65 @@ v5 = [
|
|||||||
"name": "first",
|
"name": "first",
|
||||||
"value": 20
|
"value": 20
|
||||||
}
|
}
|
||||||
];
|
],
|
||||||
|
v6 = {
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
v7 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "status",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v8 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "createdAt",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v9 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "cursor",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v10 = {
|
||||||
|
"kind": "ClientExtension",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "__id",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
"fragment": {
|
"fragment": {
|
||||||
"argumentDefinitions": (v0/*: any*/),
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
@@ -117,7 +176,12 @@ return {
|
|||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
(v3/*: any*/),
|
(v3/*: any*/),
|
||||||
(v4/*: any*/)
|
(v4/*: any*/),
|
||||||
|
{
|
||||||
|
"args": null,
|
||||||
|
"kind": "FragmentSpread",
|
||||||
|
"name": "CompliancePageUpdatesListFragment"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
@@ -179,6 +243,76 @@ return {
|
|||||||
"selections": [
|
"selections": [
|
||||||
(v3/*: any*/),
|
(v3/*: any*/),
|
||||||
(v4/*: any*/),
|
(v4/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v5/*: any*/),
|
||||||
|
"concreteType": "MailingListUpdateConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "updates",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v6/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "MailingListUpdateEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "MailingListUpdate",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "title",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "body",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v7/*: any*/),
|
||||||
|
(v8/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "updatedAt",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v2/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v9/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
(v10/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": "updates(first:20)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v5/*: any*/),
|
||||||
|
"filters": null,
|
||||||
|
"handle": "connection",
|
||||||
|
"key": "CompliancePageUpdatesList_updates",
|
||||||
|
"kind": "LinkedHandle",
|
||||||
|
"name": "updates"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": (v5/*: any*/),
|
"args": (v5/*: any*/),
|
||||||
@@ -187,31 +321,7 @@ return {
|
|||||||
"name": "subscribers",
|
"name": "subscribers",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
(v6/*: any*/),
|
||||||
"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,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -243,46 +353,17 @@ return {
|
|||||||
"name": "email",
|
"name": "email",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
{
|
(v7/*: any*/),
|
||||||
"alias": null,
|
(v8/*: any*/),
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "status",
|
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "createdAt",
|
|
||||||
"storageKey": null
|
|
||||||
},
|
|
||||||
(v2/*: any*/)
|
(v2/*: any*/)
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
{
|
(v9/*: any*/)
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "cursor",
|
|
||||||
"storageKey": null
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
{
|
(v10/*: any*/)
|
||||||
"kind": "ClientExtension",
|
|
||||||
"selections": [
|
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"kind": "ScalarField",
|
|
||||||
"name": "__id",
|
|
||||||
"storageKey": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"storageKey": "subscribers(first:20)"
|
"storageKey": "subscribers(first:20)"
|
||||||
},
|
},
|
||||||
@@ -312,16 +393,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "8eb57a5cf8f8b1294da6aee3228176f3",
|
"cacheID": "c8ba2eeecdfac4115f0ab1493ecf02ce",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "CompliancePageMailingListPageQuery",
|
"name": "CompliancePageMailingListPageQuery",
|
||||||
"operationKind": "query",
|
"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"
|
"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 ...CompliancePageUpdatesListFragment\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\nfragment CompliancePageUpdatesListFragment on MailingList {\n updates(first: 20) {\n pageInfo {\n hasNextPage\n endCursor\n }\n edges {\n node {\n id\n title\n body\n status\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n }\n id\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "0df7b2a8b9f0d2f421f354bd56773e43";
|
(node as any).hash = "bda0ad6e5ff15a324d4c2ad76d0c94cd";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
132
apps/console/src/__generated__/core/CompliancePageUpdatesListDeleteMutation.graphql.ts
generated
Normal file
132
apps/console/src/__generated__/core/CompliancePageUpdatesListDeleteMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<8fc725e69722e66e680df0ed52206c77>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type DeleteMailingListUpdateInput = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
export type CompliancePageUpdatesListDeleteMutation$variables = {
|
||||||
|
connections: ReadonlyArray<string>;
|
||||||
|
input: DeleteMailingListUpdateInput;
|
||||||
|
};
|
||||||
|
export type CompliancePageUpdatesListDeleteMutation$data = {
|
||||||
|
readonly deleteMailingListUpdate: {
|
||||||
|
readonly deletedMailingListUpdateId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type CompliancePageUpdatesListDeleteMutation = {
|
||||||
|
response: CompliancePageUpdatesListDeleteMutation$data;
|
||||||
|
variables: CompliancePageUpdatesListDeleteMutation$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": "deletedMailingListUpdateId",
|
||||||
|
"storageKey": null
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": [
|
||||||
|
(v0/*: any*/),
|
||||||
|
(v1/*: any*/)
|
||||||
|
],
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "CompliancePageUpdatesListDeleteMutation",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v2/*: any*/),
|
||||||
|
"concreteType": "DeleteMailingListUpdatePayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "deleteMailingListUpdate",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": [
|
||||||
|
(v1/*: any*/),
|
||||||
|
(v0/*: any*/)
|
||||||
|
],
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "CompliancePageUpdatesListDeleteMutation",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v2/*: any*/),
|
||||||
|
"concreteType": "DeleteMailingListUpdatePayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "deleteMailingListUpdate",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"filters": null,
|
||||||
|
"handle": "deleteEdge",
|
||||||
|
"key": "",
|
||||||
|
"kind": "ScalarHandle",
|
||||||
|
"name": "deletedMailingListUpdateId",
|
||||||
|
"handleArgs": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "connections",
|
||||||
|
"variableName": "connections"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "892a75687f4d8560945e5a30915aecbd",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "CompliancePageUpdatesListDeleteMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation CompliancePageUpdatesListDeleteMutation(\n $input: DeleteMailingListUpdateInput!\n) {\n deleteMailingListUpdate(input: $input) {\n deletedMailingListUpdateId\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "29a3142fa30bc64beb74d21f5162a00c";
|
||||||
|
|
||||||
|
export default node;
|
||||||
226
apps/console/src/__generated__/core/CompliancePageUpdatesListFragment.graphql.ts
generated
Normal file
226
apps/console/src/__generated__/core/CompliancePageUpdatesListFragment.graphql.ts
generated
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<d32034602cfb0c3128ebdfaa7e26af40>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ReaderFragment } from 'relay-runtime';
|
||||||
|
export type MailingListUpdateStatus = "DRAFT" | "ENQUEUED" | "PROCESSING" | "SENT";
|
||||||
|
import { FragmentRefs } from "relay-runtime";
|
||||||
|
export type CompliancePageUpdatesListFragment$data = {
|
||||||
|
readonly id: string;
|
||||||
|
readonly updates: {
|
||||||
|
readonly __id: string;
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly body: string;
|
||||||
|
readonly createdAt: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly status: MailingListUpdateStatus;
|
||||||
|
readonly title: string;
|
||||||
|
readonly updatedAt: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
readonly pageInfo: {
|
||||||
|
readonly endCursor: string | null | undefined;
|
||||||
|
readonly hasNextPage: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
readonly " $fragmentType": "CompliancePageUpdatesListFragment";
|
||||||
|
};
|
||||||
|
export type CompliancePageUpdatesListFragment$key = {
|
||||||
|
readonly " $data"?: CompliancePageUpdatesListFragment$data;
|
||||||
|
readonly " $fragmentSpreads": FragmentRefs<"CompliancePageUpdatesListFragment">;
|
||||||
|
};
|
||||||
|
|
||||||
|
import CompliancePageUpdatesListQuery_graphql from './CompliancePageUpdatesListQuery.graphql';
|
||||||
|
|
||||||
|
const node: ReaderFragment = (function(){
|
||||||
|
var v0 = [
|
||||||
|
"updates"
|
||||||
|
],
|
||||||
|
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": CompliancePageUpdatesListQuery_graphql,
|
||||||
|
"identifierInfo": {
|
||||||
|
"identifierField": "id",
|
||||||
|
"identifierQueryVariableName": "id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "CompliancePageUpdatesListFragment",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": "updates",
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "MailingListUpdateConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "__CompliancePageUpdatesList_updates_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": "MailingListUpdateEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "MailingListUpdate",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v1/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "title",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "body",
|
||||||
|
"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": "updatedAt",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
(v1/*: any*/)
|
||||||
|
],
|
||||||
|
"type": "MailingList",
|
||||||
|
"abstractKey": null
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "97bcf2215cc4fa04d1d4b84b2da4c33a";
|
||||||
|
|
||||||
|
export default node;
|
||||||
270
apps/console/src/__generated__/core/CompliancePageUpdatesListQuery.graphql.ts
generated
Normal file
270
apps/console/src/__generated__/core/CompliancePageUpdatesListQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<dcc3921bab2d3c42dc01f71fbf1a4338>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
import { FragmentRefs } from "relay-runtime";
|
||||||
|
export type CompliancePageUpdatesListQuery$variables = {
|
||||||
|
after?: string | null | undefined;
|
||||||
|
first?: number | null | undefined;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
export type CompliancePageUpdatesListQuery$data = {
|
||||||
|
readonly node: {
|
||||||
|
readonly " $fragmentSpreads": FragmentRefs<"CompliancePageUpdatesListFragment">;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type CompliancePageUpdatesListQuery = {
|
||||||
|
response: CompliancePageUpdatesListQuery$data;
|
||||||
|
variables: CompliancePageUpdatesListQuery$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": "CompliancePageUpdatesListQuery",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v1/*: any*/),
|
||||||
|
"concreteType": null,
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"args": (v2/*: any*/),
|
||||||
|
"kind": "FragmentSpread",
|
||||||
|
"name": "CompliancePageUpdatesListFragment"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "CompliancePageUpdatesListQuery",
|
||||||
|
"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": (v2/*: any*/),
|
||||||
|
"concreteType": "MailingListUpdateConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "updates",
|
||||||
|
"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": "MailingListUpdateEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "MailingListUpdate",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v4/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "title",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "body",
|
||||||
|
"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": "updatedAt",
|
||||||
|
"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": "CompliancePageUpdatesList_updates",
|
||||||
|
"kind": "LinkedHandle",
|
||||||
|
"name": "updates"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "MailingList",
|
||||||
|
"abstractKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "e1a6160f157715c1c52adca86d02b9c6",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "CompliancePageUpdatesListQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query CompliancePageUpdatesListQuery(\n $after: CursorKey = null\n $first: Int = 20\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...CompliancePageUpdatesListFragment_2HEEH6\n id\n }\n}\n\nfragment CompliancePageUpdatesListFragment_2HEEH6 on MailingList {\n updates(first: $first, after: $after) {\n pageInfo {\n hasNextPage\n endCursor\n }\n edges {\n node {\n id\n title\n body\n status\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n }\n id\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "97bcf2215cc4fa04d1d4b84b2da4c33a";
|
||||||
|
|
||||||
|
export default node;
|
||||||
138
apps/console/src/__generated__/core/CompliancePageUpdatesListSendMutation.graphql.ts
generated
Normal file
138
apps/console/src/__generated__/core/CompliancePageUpdatesListSendMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<f5323e8e7bd73882c9169eb01318a21c>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type MailingListUpdateStatus = "DRAFT" | "ENQUEUED" | "PROCESSING" | "SENT";
|
||||||
|
export type SendMailingListUpdateInput = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
export type CompliancePageUpdatesListSendMutation$variables = {
|
||||||
|
input: SendMailingListUpdateInput;
|
||||||
|
};
|
||||||
|
export type CompliancePageUpdatesListSendMutation$data = {
|
||||||
|
readonly sendMailingListUpdate: {
|
||||||
|
readonly mailingListUpdate: {
|
||||||
|
readonly body: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly status: MailingListUpdateStatus;
|
||||||
|
readonly title: string;
|
||||||
|
readonly updatedAt: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type CompliancePageUpdatesListSendMutation = {
|
||||||
|
response: CompliancePageUpdatesListSendMutation$data;
|
||||||
|
variables: CompliancePageUpdatesListSendMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "SendMailingListUpdatePayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "sendMailingListUpdate",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "MailingListUpdate",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "mailingListUpdate",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "title",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "body",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "status",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "updatedAt",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "CompliancePageUpdatesListSendMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "CompliancePageUpdatesListSendMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "23fee99a4999d2c9cbeecd6fa9863b67",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "CompliancePageUpdatesListSendMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation CompliancePageUpdatesListSendMutation(\n $input: SendMailingListUpdateInput!\n) {\n sendMailingListUpdate(input: $input) {\n mailingListUpdate {\n id\n title\n body\n status\n updatedAt\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "37c68b46d761280e2ff16f5bff08b249";
|
||||||
|
|
||||||
|
export default node;
|
||||||
140
apps/console/src/__generated__/core/EditComplianceUpdateDialogMutation.graphql.ts
generated
Normal file
140
apps/console/src/__generated__/core/EditComplianceUpdateDialogMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<622eccc63036573a8e38ffc8dd36e7d7>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type MailingListUpdateStatus = "DRAFT" | "ENQUEUED" | "PROCESSING" | "SENT";
|
||||||
|
export type UpdateMailingListUpdateInput = {
|
||||||
|
body: string;
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
export type EditComplianceUpdateDialogMutation$variables = {
|
||||||
|
input: UpdateMailingListUpdateInput;
|
||||||
|
};
|
||||||
|
export type EditComplianceUpdateDialogMutation$data = {
|
||||||
|
readonly updateMailingListUpdate: {
|
||||||
|
readonly mailingListUpdate: {
|
||||||
|
readonly body: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly status: MailingListUpdateStatus;
|
||||||
|
readonly title: string;
|
||||||
|
readonly updatedAt: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type EditComplianceUpdateDialogMutation = {
|
||||||
|
response: EditComplianceUpdateDialogMutation$data;
|
||||||
|
variables: EditComplianceUpdateDialogMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "UpdateMailingListUpdatePayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "updateMailingListUpdate",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "MailingListUpdate",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "mailingListUpdate",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "title",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "body",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "status",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "updatedAt",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "EditComplianceUpdateDialogMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "EditComplianceUpdateDialogMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "49b4c9f194a0bf6ebe01408ae6387f2e",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "EditComplianceUpdateDialogMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation EditComplianceUpdateDialogMutation(\n $input: UpdateMailingListUpdateInput!\n) {\n updateMailingListUpdate(input: $input) {\n mailingListUpdate {\n id\n title\n body\n status\n updatedAt\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "8b2246ab3bddba658cc488610afcd80f";
|
||||||
|
|
||||||
|
export default node;
|
||||||
193
apps/console/src/__generated__/core/NewComplianceUpdateDialogMutation.graphql.ts
generated
Normal file
193
apps/console/src/__generated__/core/NewComplianceUpdateDialogMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<458f30b721ecd9df210621a8853008c3>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type MailingListUpdateStatus = "DRAFT" | "ENQUEUED" | "PROCESSING" | "SENT";
|
||||||
|
export type CreateMailingListUpdateInput = {
|
||||||
|
body: string;
|
||||||
|
mailingListId: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
export type NewComplianceUpdateDialogMutation$variables = {
|
||||||
|
connections: ReadonlyArray<string>;
|
||||||
|
input: CreateMailingListUpdateInput;
|
||||||
|
};
|
||||||
|
export type NewComplianceUpdateDialogMutation$data = {
|
||||||
|
readonly createMailingListUpdate: {
|
||||||
|
readonly mailingListUpdate: {
|
||||||
|
readonly body: string;
|
||||||
|
readonly createdAt: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly status: MailingListUpdateStatus;
|
||||||
|
readonly title: string;
|
||||||
|
readonly updatedAt: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type NewComplianceUpdateDialogMutation = {
|
||||||
|
response: NewComplianceUpdateDialogMutation$data;
|
||||||
|
variables: NewComplianceUpdateDialogMutation$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": "MailingListUpdate",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "mailingListUpdate",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "title",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "body",
|
||||||
|
"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": "updatedAt",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": [
|
||||||
|
(v0/*: any*/),
|
||||||
|
(v1/*: any*/)
|
||||||
|
],
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "NewComplianceUpdateDialogMutation",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v2/*: any*/),
|
||||||
|
"concreteType": "CreateMailingListUpdatePayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "createMailingListUpdate",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": [
|
||||||
|
(v1/*: any*/),
|
||||||
|
(v0/*: any*/)
|
||||||
|
],
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "NewComplianceUpdateDialogMutation",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": (v2/*: any*/),
|
||||||
|
"concreteType": "CreateMailingListUpdatePayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "createMailingListUpdate",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v3/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"filters": null,
|
||||||
|
"handle": "prependNode",
|
||||||
|
"key": "",
|
||||||
|
"kind": "LinkedHandle",
|
||||||
|
"name": "mailingListUpdate",
|
||||||
|
"handleArgs": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "connections",
|
||||||
|
"variableName": "connections"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "edgeTypeName",
|
||||||
|
"value": "MailingListUpdateEdge"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "33fc392100d93803de3a189e75961a75",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "NewComplianceUpdateDialogMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation NewComplianceUpdateDialogMutation(\n $input: CreateMailingListUpdateInput!\n) {\n createMailingListUpdate(input: $input) {\n mailingListUpdate {\n id\n title\n body\n status\n createdAt\n updatedAt\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "bf02b571efaf38d4f4a8fde50b311832";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { Button, Card, Field, IconPlusLarge, Spinner, useDialogRef } from "@probo/ui";
|
import { Button, Card, Field, IconPlusLarge, Spinner, TabItem, Tabs, useDialogRef } from "@probo/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||||
import { ConnectionHandler, graphql } from "relay-runtime";
|
import { ConnectionHandler, graphql } from "relay-runtime";
|
||||||
@@ -9,7 +9,10 @@ import type { CompliancePageMailingListPageQuery } from "#/__generated__/core/Co
|
|||||||
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
||||||
|
|
||||||
import { CompliancePageMailingList } from "./_components/CompliancePageMailingList";
|
import { CompliancePageMailingList } from "./_components/CompliancePageMailingList";
|
||||||
|
import { CompliancePageUpdatesList, type UpdateNode } from "./_components/CompliancePageUpdatesList";
|
||||||
|
import { EditComplianceUpdateDialog } from "./_components/EditComplianceUpdateDialog";
|
||||||
import { NewCompliancePageSubscriberDialog } from "./_components/NewCompliancePageSubscriberDialog";
|
import { NewCompliancePageSubscriberDialog } from "./_components/NewCompliancePageSubscriberDialog";
|
||||||
|
import { NewComplianceUpdateDialog } from "./_components/NewComplianceUpdateDialog";
|
||||||
|
|
||||||
export const compliancePageMailingListPageQuery = graphql`
|
export const compliancePageMailingListPageQuery = graphql`
|
||||||
query CompliancePageMailingListPageQuery($organizationId: ID!) {
|
query CompliancePageMailingListPageQuery($organizationId: ID!) {
|
||||||
@@ -21,6 +24,7 @@ export const compliancePageMailingListPageQuery = graphql`
|
|||||||
mailingList {
|
mailingList {
|
||||||
id
|
id
|
||||||
replyTo
|
replyTo
|
||||||
|
...CompliancePageUpdatesListFragment
|
||||||
}
|
}
|
||||||
...CompliancePageMailingListFragment
|
...CompliancePageMailingListFragment
|
||||||
}
|
}
|
||||||
@@ -40,12 +44,19 @@ const updateMailingListMutation = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
type Tab = "updates" | "subscribers";
|
||||||
|
|
||||||
export function CompliancePageMailingListPage(props: {
|
export function CompliancePageMailingListPage(props: {
|
||||||
queryRef: PreloadedQuery<CompliancePageMailingListPageQuery>;
|
queryRef: PreloadedQuery<CompliancePageMailingListPageQuery>;
|
||||||
}) {
|
}) {
|
||||||
const { queryRef } = props;
|
const { queryRef } = props;
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const dialogRef = useDialogRef();
|
const subscriberDialogRef = useDialogRef();
|
||||||
|
const newUpdateDialogRef = useDialogRef();
|
||||||
|
const editUpdateDialogRef = useDialogRef();
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState<Tab>("updates");
|
||||||
|
const [selectedUpdate, setSelectedUpdate] = useState<UpdateNode | null>(null);
|
||||||
|
|
||||||
const { organization } = usePreloadedQuery<CompliancePageMailingListPageQuery>(
|
const { organization } = usePreloadedQuery<CompliancePageMailingListPageQuery>(
|
||||||
compliancePageMailingListPageQuery,
|
compliancePageMailingListPageQuery,
|
||||||
@@ -56,13 +67,18 @@ export function CompliancePageMailingListPage(props: {
|
|||||||
throw new Error("invalid type for node");
|
throw new Error("invalid type for node");
|
||||||
}
|
}
|
||||||
|
|
||||||
const mailingList = organization.compliancePage.mailingList;
|
const { compliancePage } = organization;
|
||||||
|
const mailingList = compliancePage.mailingList;
|
||||||
const mailingListId = mailingList?.id;
|
const mailingListId = mailingList?.id;
|
||||||
|
|
||||||
const connectionId = mailingListId
|
const subscriberConnectionId = mailingListId
|
||||||
? ConnectionHandler.getConnectionID(mailingListId, "CompliancePageMailingList_subscribers")
|
? ConnectionHandler.getConnectionID(mailingListId, "CompliancePageMailingList_subscribers")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const updatesConnectionId = mailingListId
|
||||||
|
? ConnectionHandler.getConnectionID(mailingListId, "CompliancePageUpdatesList_updates")
|
||||||
|
: null;
|
||||||
|
|
||||||
const [replyTo, setReplyTo] = useState(mailingList?.replyTo ?? "");
|
const [replyTo, setReplyTo] = useState(mailingList?.replyTo ?? "");
|
||||||
|
|
||||||
const [updateMailingList, isUpdating] = useMutationWithToasts<CompliancePageMailingListPage_updateMailingListMutation>(
|
const [updateMailingList, isUpdating] = useMutationWithToasts<CompliancePageMailingListPage_updateMailingListMutation>(
|
||||||
@@ -85,6 +101,11 @@ export function CompliancePageMailingListPage(props: {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditUpdate = (update: UpdateNode) => {
|
||||||
|
setSelectedUpdate({ ...update });
|
||||||
|
editUpdateDialogRef.current?.open();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{mailingListId && (
|
{mailingListId && (
|
||||||
@@ -119,27 +140,57 @@ export function CompliancePageMailingListPage(props: {
|
|||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<Tabs>
|
||||||
<h3 className="text-base font-medium">{__("Subscribers")}</h3>
|
<TabItem active={activeTab === "updates"} onClick={() => setActiveTab("updates")}>
|
||||||
<p className="text-sm text-txt-tertiary">
|
{__("Updates")}
|
||||||
{__("People subscribed to receive security and compliance updates")}
|
</TabItem>
|
||||||
</p>
|
<TabItem active={activeTab === "subscribers"} onClick={() => setActiveTab("subscribers")}>
|
||||||
</div>
|
{__("Subscribers")}
|
||||||
{mailingListId && (
|
</TabItem>
|
||||||
<Button icon={IconPlusLarge} onClick={() => dialogRef.current?.open()}>
|
</Tabs>
|
||||||
|
|
||||||
|
{activeTab === "updates" && mailingListId && (
|
||||||
|
<Button icon={IconPlusLarge} onClick={() => newUpdateDialogRef.current?.open()}>
|
||||||
|
{__("Add Update")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{activeTab === "subscribers" && mailingListId && (
|
||||||
|
<Button icon={IconPlusLarge} onClick={() => subscriberDialogRef.current?.open()}>
|
||||||
{__("Add Subscriber")}
|
{__("Add Subscriber")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CompliancePageMailingList fragmentRef={organization.compliancePage} />
|
{activeTab === "updates" && mailingList && (
|
||||||
|
<CompliancePageUpdatesList
|
||||||
|
fragmentRef={mailingList}
|
||||||
|
onEdit={handleEditUpdate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "subscribers" && (
|
||||||
|
<CompliancePageMailingList fragmentRef={compliancePage} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{mailingListId && connectionId && (
|
{mailingListId && updatesConnectionId && (
|
||||||
<NewCompliancePageSubscriberDialog
|
<NewComplianceUpdateDialog
|
||||||
ref={dialogRef}
|
ref={newUpdateDialogRef}
|
||||||
mailingListId={mailingListId}
|
mailingListId={mailingListId}
|
||||||
connectionId={connectionId}
|
connectionId={updatesConnectionId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<EditComplianceUpdateDialog
|
||||||
|
ref={editUpdateDialogRef}
|
||||||
|
update={selectedUpdate}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{mailingListId && subscriberConnectionId && (
|
||||||
|
<NewCompliancePageSubscriberDialog
|
||||||
|
ref={subscriberDialogRef}
|
||||||
|
mailingListId={mailingListId}
|
||||||
|
connectionId={subscriberConnectionId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import { Badge, Button, IconChevronDown, IconPencil, IconSend, IconTrashCan, Spinner, Table, Tbody, Td, Th, Thead, Tr, useConfirm } from "@probo/ui";
|
||||||
|
import { usePaginationFragment } from "react-relay";
|
||||||
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
|
import type { CompliancePageUpdatesListDeleteMutation } from "#/__generated__/core/CompliancePageUpdatesListDeleteMutation.graphql";
|
||||||
|
import type { CompliancePageUpdatesListFragment$data, CompliancePageUpdatesListFragment$key } from "#/__generated__/core/CompliancePageUpdatesListFragment.graphql";
|
||||||
|
import type { CompliancePageUpdatesListQuery } from "#/__generated__/core/CompliancePageUpdatesListQuery.graphql";
|
||||||
|
import type { CompliancePageUpdatesListSendMutation } from "#/__generated__/core/CompliancePageUpdatesListSendMutation.graphql";
|
||||||
|
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
||||||
|
|
||||||
|
const deleteMutation = graphql`
|
||||||
|
mutation CompliancePageUpdatesListDeleteMutation(
|
||||||
|
$input: DeleteMailingListUpdateInput!
|
||||||
|
$connections: [ID!]!
|
||||||
|
) {
|
||||||
|
deleteMailingListUpdate(input: $input) {
|
||||||
|
deletedMailingListUpdateId @deleteEdge(connections: $connections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const sendMutation = graphql`
|
||||||
|
mutation CompliancePageUpdatesListSendMutation($input: SendMailingListUpdateInput!) {
|
||||||
|
sendMailingListUpdate(input: $input) {
|
||||||
|
mailingListUpdate {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
body
|
||||||
|
status
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const fragment = graphql`
|
||||||
|
fragment CompliancePageUpdatesListFragment on MailingList
|
||||||
|
@argumentDefinitions(
|
||||||
|
first: { type: Int, defaultValue: 20 }
|
||||||
|
after: { type: CursorKey, defaultValue: null }
|
||||||
|
)
|
||||||
|
@refetchable(queryName: "CompliancePageUpdatesListQuery") {
|
||||||
|
updates(
|
||||||
|
first: $first
|
||||||
|
after: $after
|
||||||
|
) @connection(key: "CompliancePageUpdatesList_updates") {
|
||||||
|
__id
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
body
|
||||||
|
status
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export type UpdateNode = CompliancePageUpdatesListFragment$data["updates"]["edges"][number]["node"];
|
||||||
|
|
||||||
|
export function CompliancePageUpdatesList(props: {
|
||||||
|
fragmentRef: CompliancePageUpdatesListFragment$key;
|
||||||
|
onEdit: (update: UpdateNode) => void;
|
||||||
|
}) {
|
||||||
|
const { fragmentRef, onEdit } = props;
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const confirm = useConfirm();
|
||||||
|
|
||||||
|
const { data, hasNext, loadNext, isLoadingNext } = usePaginationFragment<
|
||||||
|
CompliancePageUpdatesListQuery,
|
||||||
|
CompliancePageUpdatesListFragment$key
|
||||||
|
>(fragment, fragmentRef);
|
||||||
|
|
||||||
|
const connection = data.updates;
|
||||||
|
|
||||||
|
const [deleteUpdate, isDeleting]
|
||||||
|
= useMutationWithToasts<CompliancePageUpdatesListDeleteMutation>(deleteMutation, {
|
||||||
|
successMessage: __("Update deleted successfully"),
|
||||||
|
errorMessage: __("Failed to delete update"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [sendUpdate, isSending]
|
||||||
|
= useMutationWithToasts<CompliancePageUpdatesListSendMutation>(sendMutation, {
|
||||||
|
successMessage: __("Update enqueued for delivery"),
|
||||||
|
errorMessage: __("Failed to enqueue update for delivery"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDelete = (id: string) => {
|
||||||
|
void deleteUpdate({
|
||||||
|
variables: {
|
||||||
|
input: { id },
|
||||||
|
connections: [connection.__id],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSend = (node: UpdateNode) => {
|
||||||
|
confirm(
|
||||||
|
() =>
|
||||||
|
sendUpdate({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id: node.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
title: __("Send this update to all subscribers?"),
|
||||||
|
message: `"${node.title}" — ${node.body.length > 120 ? node.body.slice(0, 120) + "…" : node.body}`,
|
||||||
|
label: __("Send"),
|
||||||
|
variant: "primary",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{connection.edges.length === 0
|
||||||
|
? (
|
||||||
|
<Table>
|
||||||
|
<Tbody>
|
||||||
|
<Tr>
|
||||||
|
<Td className="text-center text-txt-tertiary py-8">
|
||||||
|
{__("No updates yet")}
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<>
|
||||||
|
<Table>
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>{__("Title")}</Th>
|
||||||
|
<Th>{__("Status")}</Th>
|
||||||
|
<Th>{__("Created")}</Th>
|
||||||
|
<Th />
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{connection.edges.map(({ node }) => (
|
||||||
|
<Tr key={node.id}>
|
||||||
|
<Td>{node.title}</Td>
|
||||||
|
<Td>
|
||||||
|
<Badge variant={node.status === "SENT" ? "success" : node.status === "DRAFT" ? "warning" : "info"}>
|
||||||
|
{node.status === "SENT" ? __("Sent") : node.status === "ENQUEUED" ? __("Queued") : node.status === "PROCESSING" ? __("Processing…") : __("Draft")}
|
||||||
|
</Badge>
|
||||||
|
</Td>
|
||||||
|
<Td className="text-txt-tertiary text-sm">
|
||||||
|
{new Date(node.createdAt).toLocaleDateString()}
|
||||||
|
</Td>
|
||||||
|
<Td className="w-auto">
|
||||||
|
<div className="flex gap-1 justify-end">
|
||||||
|
{node.status === "DRAFT" && (
|
||||||
|
<Button
|
||||||
|
icon={IconSend}
|
||||||
|
disabled={isSending}
|
||||||
|
onClick={() => handleSend(node)}
|
||||||
|
className="bg-green-600 text-white hover:bg-green-700 active:bg-green-800 shadow-none"
|
||||||
|
aria-label={__("Send")}
|
||||||
|
>
|
||||||
|
{__("Send")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="tertiary"
|
||||||
|
icon={IconPencil}
|
||||||
|
onClick={() => onEdit(node)}
|
||||||
|
aria-label={__("Edit update")}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="tertiary"
|
||||||
|
icon={IconTrashCan}
|
||||||
|
disabled={isDeleting}
|
||||||
|
onClick={() => handleDelete(node.id)}
|
||||||
|
aria-label={__("Delete update")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import { Button, Dialog, DialogContent, DialogFooter, type DialogRef, Field, IconCircleInfo, Spinner, Textarea } from "@probo/ui";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { graphql } from "relay-runtime";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import type { EditComplianceUpdateDialogMutation } from "#/__generated__/core/EditComplianceUpdateDialogMutation.graphql";
|
||||||
|
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||||
|
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
||||||
|
|
||||||
|
import type { UpdateNode } from "./CompliancePageUpdatesList";
|
||||||
|
|
||||||
|
const updateMutation = graphql`
|
||||||
|
mutation EditComplianceUpdateDialogMutation($input: UpdateMailingListUpdateInput!) {
|
||||||
|
updateMailingListUpdate(input: $input) {
|
||||||
|
mailingListUpdate {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
body
|
||||||
|
status
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function EditComplianceUpdateDialog(props: {
|
||||||
|
update: UpdateNode | null;
|
||||||
|
ref: DialogRef;
|
||||||
|
}) {
|
||||||
|
const { update, ref } = props;
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
|
const isSent = update?.status !== "DRAFT";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
title: z.string().trim().min(1, __("Title is required")),
|
||||||
|
body: z.string().trim().min(1, __("Body is required")),
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useFormWithSchema(schema, {
|
||||||
|
defaultValues: { title: "", body: "" },
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (update) {
|
||||||
|
form.reset({ title: update.title, body: update.body });
|
||||||
|
}
|
||||||
|
}, [update, form]);
|
||||||
|
|
||||||
|
const [saveUpdate, isSaving] = useMutationWithToasts<EditComplianceUpdateDialogMutation>(
|
||||||
|
updateMutation,
|
||||||
|
{
|
||||||
|
successMessage: __("Update saved successfully"),
|
||||||
|
errorMessage: __("Failed to save update"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = async (data: { title: string; body: string }) => {
|
||||||
|
if (!update) return;
|
||||||
|
await saveUpdate({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id: update.id,
|
||||||
|
title: data.title.trim(),
|
||||||
|
body: data.body.trim(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (_, errors) => {
|
||||||
|
if (!errors?.length) {
|
||||||
|
ref.current?.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog ref={ref} title={__("Edit Update")}>
|
||||||
|
<form onSubmit={e => void form.handleSubmit(handleSubmit)(e)}>
|
||||||
|
<DialogContent padded className="space-y-6">
|
||||||
|
<div className="flex gap-2.5 rounded-lg bg-surface-secondary p-3 text-sm text-txt-secondary">
|
||||||
|
<IconCircleInfo size={16} className="mt-0.5 shrink-0 text-txt-tertiary" />
|
||||||
|
<p>
|
||||||
|
{__("Do not include confidential or sensitive information in this update. Any content that requires protection should be placed behind your NDA-gated documents instead.")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Field
|
||||||
|
label={__("Title")}
|
||||||
|
required
|
||||||
|
disabled={isSent}
|
||||||
|
error={form.formState.errors.title?.message}
|
||||||
|
{...form.register("title")}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label={__("Body")}
|
||||||
|
required
|
||||||
|
error={form.formState.errors.body?.message}
|
||||||
|
>
|
||||||
|
<Textarea rows={6} disabled={isSent} {...form.register("body")} />
|
||||||
|
</Field>
|
||||||
|
{isSent && (
|
||||||
|
<p className="text-sm text-txt-tertiary">
|
||||||
|
{__("This update has been sent and can no longer be edited.")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogFooter>
|
||||||
|
{!isSent && (
|
||||||
|
<Button type="submit" disabled={isSaving}>
|
||||||
|
{isSaving && <Spinner />}
|
||||||
|
{__("Save")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -36,7 +36,7 @@ export function NewCompliancePageSubscriberDialog(props: {
|
|||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
fullName: z.string().min(1, __("Full name is required")).trim(),
|
fullName: z.string().trim().min(1, __("Full name is required")),
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, __("Email is required"))
|
.min(1, __("Email is required"))
|
||||||
@@ -90,6 +90,7 @@ export function NewCompliancePageSubscriberDialog(props: {
|
|||||||
label={__("Full Name")}
|
label={__("Full Name")}
|
||||||
required
|
required
|
||||||
error={form.formState.errors.fullName?.message}
|
error={form.formState.errors.fullName?.message}
|
||||||
|
type="text"
|
||||||
{...form.register("fullName")}
|
{...form.register("fullName")}
|
||||||
placeholder={__("John Doe")}
|
placeholder={__("John Doe")}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import { Button, Dialog, DialogContent, DialogFooter, type DialogRef, Field, Spinner, Textarea } from "@probo/ui";
|
||||||
|
import { graphql } from "relay-runtime";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import type { NewComplianceUpdateDialogMutation } from "#/__generated__/core/NewComplianceUpdateDialogMutation.graphql";
|
||||||
|
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||||
|
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
||||||
|
|
||||||
|
const createMutation = graphql`
|
||||||
|
mutation NewComplianceUpdateDialogMutation(
|
||||||
|
$input: CreateMailingListUpdateInput!
|
||||||
|
$connections: [ID!]!
|
||||||
|
) {
|
||||||
|
createMailingListUpdate(input: $input) {
|
||||||
|
mailingListUpdate
|
||||||
|
@prependNode(connections: $connections, edgeTypeName: "MailingListUpdateEdge") {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
body
|
||||||
|
status
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function NewComplianceUpdateDialog(props: {
|
||||||
|
mailingListId: string;
|
||||||
|
connectionId: string;
|
||||||
|
ref: DialogRef;
|
||||||
|
onCreated?: () => void;
|
||||||
|
}) {
|
||||||
|
const { mailingListId, connectionId, ref, onCreated } = props;
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
title: z.string().trim().min(1, __("Title is required")),
|
||||||
|
body: z.string().trim().min(1, __("Body is required")),
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useFormWithSchema(schema, {
|
||||||
|
defaultValues: { title: "", body: "" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const [createUpdate, isCreating] = useMutationWithToasts<NewComplianceUpdateDialogMutation>(
|
||||||
|
createMutation,
|
||||||
|
{
|
||||||
|
successMessage: __("Update created successfully"),
|
||||||
|
errorMessage: __("Failed to create update"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = async (data: { title: string; body: string }) => {
|
||||||
|
await createUpdate({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
mailingListId,
|
||||||
|
title: data.title.trim(),
|
||||||
|
body: data.body.trim(),
|
||||||
|
},
|
||||||
|
connections: [connectionId],
|
||||||
|
},
|
||||||
|
onCompleted: (_, errors) => {
|
||||||
|
if (!errors?.length) {
|
||||||
|
form.reset();
|
||||||
|
ref.current?.close();
|
||||||
|
onCreated?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog ref={ref} title={__("Add Update")}>
|
||||||
|
<form onSubmit={e => void form.handleSubmit(handleSubmit)(e)}>
|
||||||
|
<DialogContent padded className="space-y-6">
|
||||||
|
<Field
|
||||||
|
label={__("Title")}
|
||||||
|
required
|
||||||
|
error={form.formState.errors.title?.message}
|
||||||
|
{...form.register("title")}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label={__("Body")}
|
||||||
|
required
|
||||||
|
error={form.formState.errors.body?.message}
|
||||||
|
>
|
||||||
|
<Textarea rows={6} {...form.register("body")} />
|
||||||
|
</Field>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" disabled={isCreating}>
|
||||||
|
{isCreating && <Spinner />}
|
||||||
|
{__("Create")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,8 +4,7 @@ import { useCallback, useState, useTransition } from "react";
|
|||||||
import { useRefetchableFragment } from "react-relay";
|
import { useRefetchableFragment } from "react-relay";
|
||||||
import { ConnectionHandler, graphql } from "relay-runtime";
|
import { ConnectionHandler, graphql } from "relay-runtime";
|
||||||
|
|
||||||
import type { CompliancePageFrameworkList_compliancePageFragment$key } from "#/__generated__/core/CompliancePageFrameworkList_compliancePageFragment.graphql";
|
import type { CompliancePageFrameworkList_compliancePageFragment$data, CompliancePageFrameworkList_compliancePageFragment$key } from "#/__generated__/core/CompliancePageFrameworkList_compliancePageFragment.graphql";
|
||||||
import type { CompliancePageFrameworkList_compliancePageFragment$data } from "#/__generated__/core/CompliancePageFrameworkList_compliancePageFragment.graphql";
|
|
||||||
import type { CompliancePageFrameworkList_compliancePageRefetchQuery } from "#/__generated__/core/CompliancePageFrameworkList_compliancePageRefetchQuery.graphql";
|
import type { CompliancePageFrameworkList_compliancePageRefetchQuery } from "#/__generated__/core/CompliancePageFrameworkList_compliancePageRefetchQuery.graphql";
|
||||||
import type { CompliancePageFrameworkList_createMutation } from "#/__generated__/core/CompliancePageFrameworkList_createMutation.graphql";
|
import type { CompliancePageFrameworkList_createMutation } from "#/__generated__/core/CompliancePageFrameworkList_createMutation.graphql";
|
||||||
import type { CompliancePageFrameworkList_deleteMutation } from "#/__generated__/core/CompliancePageFrameworkList_deleteMutation.graphql";
|
import type { CompliancePageFrameworkList_deleteMutation } from "#/__generated__/core/CompliancePageFrameworkList_deleteMutation.graphql";
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export function MainLayout(props: Props) {
|
|||||||
<TabLink to="/documents">{__("Documents")}</TabLink>
|
<TabLink to="/documents">{__("Documents")}</TabLink>
|
||||||
{trustCenter.vendorInfo.totalCount > 0
|
{trustCenter.vendorInfo.totalCount > 0
|
||||||
&& <TabLink to="/subprocessors">{__("Subprocessors")}</TabLink>}
|
&& <TabLink to="/subprocessors">{__("Subprocessors")}</TabLink>}
|
||||||
|
{isAuthenticated
|
||||||
|
&& <TabLink to="/updates">{__("Updates")}</TabLink>}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Outlet context={{ trustCenter }} />
|
<Outlet context={{ trustCenter }} />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
108
apps/trust/src/pages/UpdatesPage.tsx
Normal file
108
apps/trust/src/pages/UpdatesPage.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import { IconChevronDown } from "@probo/ui";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||||
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
|
import type { UpdatesPageQuery } from "#/pages/__generated__/UpdatesPageQuery.graphql";
|
||||||
|
|
||||||
|
export const currentTrustUpdatesQuery = graphql`
|
||||||
|
query UpdatesPageQuery {
|
||||||
|
currentTrustCenter {
|
||||||
|
id
|
||||||
|
updates(first: 50) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
body
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
queryRef: PreloadedQuery<UpdatesPageQuery>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function UpdatesPage({ queryRef }: Props) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const data = usePreloadedQuery<UpdatesPageQuery>(
|
||||||
|
currentTrustUpdatesQuery,
|
||||||
|
queryRef,
|
||||||
|
);
|
||||||
|
|
||||||
|
const items
|
||||||
|
= data.currentTrustCenter?.updates.edges.map(e => e.node) ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className="font-medium mb-1">{__("Updates")}</h2>
|
||||||
|
<p className="text-sm text-txt-secondary mb-4">
|
||||||
|
{__("Latest compliance and security updates:")}
|
||||||
|
</p>
|
||||||
|
<div className="space-y-0">
|
||||||
|
{items.map(item => (
|
||||||
|
<UpdateItem key={item.id} item={item} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateItemType = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function UpdateItem({ item }: { item: UpdateItemType }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-border-solid -mt-px first:rounded-t-lg last:rounded-b-lg overflow-hidden">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen(o => !o)}
|
||||||
|
className="w-full flex items-center justify-between px-6 py-4 text-left hover:bg-highlight transition-colors group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
|
<span className="text-sm font-medium text-txt-primary truncate">
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 flex-none ml-4">
|
||||||
|
<span className="text-xs text-txt-tertiary hidden sm:block">
|
||||||
|
{new Date(item.updatedAt).toLocaleDateString(undefined, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<IconChevronDown
|
||||||
|
size={16}
|
||||||
|
className={`flex-none text-txt-tertiary transition-transform duration-200 ${open ? "rotate-180" : ""}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{open && (
|
||||||
|
<div className="px-6 pb-5 pt-0 border-t border-border-solid bg-highlight/40">
|
||||||
|
<p className="text-sm text-txt-secondary whitespace-pre-wrap leading-relaxed pt-4">
|
||||||
|
{item.body}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-txt-tertiary mt-3 sm:hidden">
|
||||||
|
{new Date(item.updatedAt).toLocaleDateString(undefined, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
146
apps/trust/src/pages/__generated__/UpdatesPageQuery.graphql.ts
generated
Normal file
146
apps/trust/src/pages/__generated__/UpdatesPageQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<4560049874109fe98528d64249673d23>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type UpdatesPageQuery$variables = Record<PropertyKey, never>;
|
||||||
|
export type UpdatesPageQuery$data = {
|
||||||
|
readonly currentTrustCenter: {
|
||||||
|
readonly id: string;
|
||||||
|
readonly updates: {
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly body: string;
|
||||||
|
readonly id: string;
|
||||||
|
readonly title: string;
|
||||||
|
readonly updatedAt: any;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
} | null | undefined;
|
||||||
|
};
|
||||||
|
export type UpdatesPageQuery = {
|
||||||
|
response: UpdatesPageQuery$data;
|
||||||
|
variables: UpdatesPageQuery$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = {
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "id",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "TrustCenter",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "currentTrustCenter",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v0/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "first",
|
||||||
|
"value": 50
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "MailingListUpdateConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "updates",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "MailingListUpdateEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "MailingListUpdate",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v0/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "title",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "body",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "updatedAt",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": "updates(first:50)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": [],
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "UpdatesPageQuery",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Query",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": [],
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "UpdatesPageQuery",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "5d323cbbdf9bfec294bb22dd6a6be735",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "UpdatesPageQuery",
|
||||||
|
"operationKind": "query",
|
||||||
|
"text": "query UpdatesPageQuery {\n currentTrustCenter {\n id\n updates(first: 50) {\n edges {\n node {\n id\n title\n body\n updatedAt\n }\n }\n }\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "5c0e074ce562f24bd841cc750f492988";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -13,6 +13,7 @@ import { MainLayout } from "#/layouts/MainLayout";
|
|||||||
import { DocumentsPage } from "#/pages/DocumentsPage";
|
import { DocumentsPage } from "#/pages/DocumentsPage";
|
||||||
import { OverviewPage } from "#/pages/OverviewPage";
|
import { OverviewPage } from "#/pages/OverviewPage";
|
||||||
import { SubprocessorsPage } from "#/pages/SubprocessorsPage";
|
import { SubprocessorsPage } from "#/pages/SubprocessorsPage";
|
||||||
|
import { currentTrustUpdatesQuery, UpdatesPage } from "#/pages/UpdatesPage";
|
||||||
import {
|
import {
|
||||||
currentTrustDocumentsQuery,
|
currentTrustDocumentsQuery,
|
||||||
currentTrustGraphQuery,
|
currentTrustGraphQuery,
|
||||||
@@ -112,6 +113,25 @@ const routes = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/updates",
|
||||||
|
loader: loaderFromQueryLoader(() =>
|
||||||
|
loadQuery(consoleEnvironment, currentTrustGraphQuery, {}),
|
||||||
|
),
|
||||||
|
Component: withQueryRef(MainLayout),
|
||||||
|
Fallback: MainSkeleton,
|
||||||
|
ErrorBoundary: RootErrorBoundary,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
loader: loaderFromQueryLoader(() =>
|
||||||
|
loadQuery(consoleEnvironment, currentTrustUpdatesQuery, {}),
|
||||||
|
),
|
||||||
|
Fallback: TabSkeleton,
|
||||||
|
Component: withQueryRef(UpdatesPage),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
// Fallback URL to the NotFound Page
|
// Fallback URL to the NotFound Page
|
||||||
{
|
{
|
||||||
path: "*",
|
path: "*",
|
||||||
|
|||||||
@@ -219,6 +219,9 @@ const (
|
|||||||
subjectTrustCenterDocumentAccessRejected = "Compliance Page Document Access Rejected - %s"
|
subjectTrustCenterDocumentAccessRejected = "Compliance Page Document Access Rejected - %s"
|
||||||
subjectMagicLink = "Connect to %s"
|
subjectMagicLink = "Connect to %s"
|
||||||
subjectElectronicSignatureCertificate = "Your signed %s - Certificate of Completion"
|
subjectElectronicSignatureCertificate = "Your signed %s - Certificate of Completion"
|
||||||
|
subjectMailingListSubscription = "%s – Confirm Your Compliance Updates Subscription"
|
||||||
|
subjectMailingListUnsubscription = "%s – You've been unsubscribed"
|
||||||
|
subjectMailingListUpdates = "%s – %s"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -242,6 +245,12 @@ var (
|
|||||||
magicLinkTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/magic-link.txt.tmpl"))
|
magicLinkTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/magic-link.txt.tmpl"))
|
||||||
electronicSignatureCertificateHTMLTemplate = htmltemplate.Must(htmltemplate.ParseFS(Templates, "dist/electronic-signature-certificate.html.tmpl"))
|
electronicSignatureCertificateHTMLTemplate = htmltemplate.Must(htmltemplate.ParseFS(Templates, "dist/electronic-signature-certificate.html.tmpl"))
|
||||||
electronicSignatureCertificateTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/electronic-signature-certificate.txt.tmpl"))
|
electronicSignatureCertificateTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/electronic-signature-certificate.txt.tmpl"))
|
||||||
|
mailingListSubscriptionHTMLTemplate = htmltemplate.Must(htmltemplate.ParseFS(Templates, "dist/mailing-list-subscription.html.tmpl"))
|
||||||
|
mailingListSubscriptionTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/mailing-list-subscription.txt.tmpl"))
|
||||||
|
mailingListUnsubscriptionHTMLTemplate = htmltemplate.Must(htmltemplate.ParseFS(Templates, "dist/mailing-list-unsubscription.html.tmpl"))
|
||||||
|
mailingListUnsubscriptionTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/mailing-list-unsubscription.txt.tmpl"))
|
||||||
|
mailingListUpdatesHTMLTemplate = htmltemplate.Must(htmltemplate.ParseFS(Templates, "dist/mailing-list-updates.html.tmpl"))
|
||||||
|
mailingListUpdatesTextTemplate = texttemplate.Must(texttemplate.ParseFS(Templates, "dist/mailing-list-updates.txt.tmpl"))
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Presenter) getCommonVariables(ctx context.Context) (*CommonVariables, error) {
|
func (p *Presenter) getCommonVariables(ctx context.Context) (*CommonVariables, error) {
|
||||||
@@ -489,6 +498,84 @@ func (p *Presenter) RenderElectronicSignatureCertificate(ctx context.Context, si
|
|||||||
return fmt.Sprintf(subjectElectronicSignatureCertificate, documentType), textBody, htmlBody, err
|
return fmt.Sprintf(subjectElectronicSignatureCertificate, documentType), textBody, htmlBody, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Presenter) RenderMailingListSubscription(ctx context.Context, organizationName string, confirmURL string, unsubscribeURL string) (subject string, textBody string, htmlBody *string, err error) {
|
||||||
|
vars, err := p.getCommonVariables(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", nil, fmt.Errorf("cannot get common variables: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
*CommonVariables
|
||||||
|
OrganizationName string
|
||||||
|
ConfirmURL string
|
||||||
|
UnsubscribeURL string
|
||||||
|
}{
|
||||||
|
CommonVariables: vars,
|
||||||
|
OrganizationName: organizationName,
|
||||||
|
ConfirmURL: confirmURL,
|
||||||
|
UnsubscribeURL: unsubscribeURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
textBody, htmlBody, err = renderEmail(mailingListSubscriptionTextTemplate, mailingListSubscriptionHTMLTemplate, data)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(subjectMailingListSubscription, organizationName), textBody, htmlBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Presenter) RenderMailingListUnsubscription(ctx context.Context, organizationName string) (subject string, textBody string, htmlBody *string, err error) {
|
||||||
|
vars, err := p.getCommonVariables(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", nil, fmt.Errorf("cannot get common variables: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
*CommonVariables
|
||||||
|
OrganizationName string
|
||||||
|
}{
|
||||||
|
CommonVariables: vars,
|
||||||
|
OrganizationName: organizationName,
|
||||||
|
}
|
||||||
|
|
||||||
|
textBody, htmlBody, err = renderEmail(mailingListUnsubscriptionTextTemplate, mailingListUnsubscriptionHTMLTemplate, data)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(subjectMailingListUnsubscription, organizationName), textBody, htmlBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Presenter) RenderMailingListNews(ctx context.Context, organizationName string, newsTitle string, newsBody string, compliancePageURL string, unsubscribeURL string) (subject string, textBody string, htmlBody *string, err error) {
|
||||||
|
vars, err := p.getCommonVariables(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", nil, fmt.Errorf("cannot get common variables: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
*CommonVariables
|
||||||
|
OrganizationName string
|
||||||
|
NewsTitle string
|
||||||
|
NewsBody string
|
||||||
|
CompliancePageURL string
|
||||||
|
UnsubscribeURL string
|
||||||
|
}{
|
||||||
|
CommonVariables: vars,
|
||||||
|
OrganizationName: organizationName,
|
||||||
|
NewsTitle: newsTitle,
|
||||||
|
NewsBody: newsBody,
|
||||||
|
CompliancePageURL: compliancePageURL,
|
||||||
|
UnsubscribeURL: unsubscribeURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
textBody, htmlBody, err = renderEmail(mailingListUpdatesTextTemplate, mailingListUpdatesHTMLTemplate, data)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(subjectMailingListUpdates, organizationName, newsTitle), textBody, htmlBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
func renderEmail(textTemplate *texttemplate.Template, htmlTemplate *htmltemplate.Template, data any) (textBody string, htmlBody *string, err error) {
|
func renderEmail(textTemplate *texttemplate.Template, htmlTemplate *htmltemplate.Template, data any) (textBody string, htmlBody *string, err error) {
|
||||||
var textBuf bytes.Buffer
|
var textBuf bytes.Buffer
|
||||||
if err := textTemplate.Execute(&textBuf, data); err != nil {
|
if err := textTemplate.Execute(&textBuf, data); err != nil {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
|
|||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
import MailingListUpdates from "../src/MailingListUpdates";
|
||||||
import ConfirmEmail from "../src/ConfirmEmail";
|
import ConfirmEmail from "../src/ConfirmEmail";
|
||||||
import DocumentExport from "../src/DocumentExport";
|
import DocumentExport from "../src/DocumentExport";
|
||||||
import DocumentSigning from "../src/DocumentSigning";
|
import DocumentSigning from "../src/DocumentSigning";
|
||||||
@@ -13,6 +14,8 @@ import PasswordReset from "../src/PasswordReset";
|
|||||||
import TrustCenterAccess from "../src/TrustCenterAccess";
|
import TrustCenterAccess from "../src/TrustCenterAccess";
|
||||||
import TrustCenterDocumentAccessRejected from "../src/TrustCenterDocumentAccessRejected";
|
import TrustCenterDocumentAccessRejected from "../src/TrustCenterDocumentAccessRejected";
|
||||||
import ElectronicSignatureCertificate from "../src/ElectronicSignatureCertificate";
|
import ElectronicSignatureCertificate from "../src/ElectronicSignatureCertificate";
|
||||||
|
import MailingListSubscription from "../src/MailingListSubscription";
|
||||||
|
import MailingListUnsubscription from "../src/MailingListUnsubscription";
|
||||||
import MagicLink from "../src/MagicLink";
|
import MagicLink from "../src/MagicLink";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
@@ -64,6 +67,18 @@ const templates: TemplateConfig[] = [
|
|||||||
name: "electronic-signature-certificate",
|
name: "electronic-signature-certificate",
|
||||||
render: () => ElectronicSignatureCertificate(),
|
render: () => ElectronicSignatureCertificate(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "mailing-list-subscription",
|
||||||
|
render: () => MailingListSubscription(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mailing-list-unsubscription",
|
||||||
|
render: () => MailingListUnsubscription(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mailing-list-updates",
|
||||||
|
render: () => MailingListUpdates(),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
async function build() {
|
async function build() {
|
||||||
|
|||||||
46
packages/emails/src/MailingListSubscription.tsx
Normal file
46
packages/emails/src/MailingListSubscription.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Button, Hr, Link, Section, Text } from "@react-email/components";
|
||||||
|
import * as React from "react";
|
||||||
|
import EmailLayout, {
|
||||||
|
bodyText,
|
||||||
|
button,
|
||||||
|
buttonContainer,
|
||||||
|
footerText,
|
||||||
|
} from "./components/EmailLayout";
|
||||||
|
|
||||||
|
export const MailingListSubscription = () => {
|
||||||
|
return (
|
||||||
|
<EmailLayout
|
||||||
|
subject={`${"{{.OrganizationName}}"} – Confirm Your Compliance Updates Subscription`}
|
||||||
|
>
|
||||||
|
<Text style={bodyText}>
|
||||||
|
You requested to subscribe to compliance updates from{" "}
|
||||||
|
<strong>{"{{.OrganizationName}}"}</strong>.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={bodyText}>
|
||||||
|
Please click the button below to confirm your subscription.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Section style={buttonContainer}>
|
||||||
|
<Button style={button} href={"{{.ConfirmURL}}"}>
|
||||||
|
Confirm Subscription
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Hr style={{ borderColor: "#ecefec", margin: "8px 0 20px" }} />
|
||||||
|
|
||||||
|
<Text style={{ ...footerText, textAlign: "center" }}>
|
||||||
|
If you did not request this subscription, or no longer want to receive
|
||||||
|
these emails,{" "}
|
||||||
|
<Link
|
||||||
|
href={"{{.UnsubscribeURL}}"}
|
||||||
|
style={{ color: "#374151", textDecoration: "underline", fontWeight: "500" }}
|
||||||
|
>
|
||||||
|
unsubscribe here.
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</EmailLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MailingListSubscription;
|
||||||
26
packages/emails/src/MailingListUnsubscription.tsx
Normal file
26
packages/emails/src/MailingListUnsubscription.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Hr, Text } from "@react-email/components";
|
||||||
|
import * as React from "react";
|
||||||
|
import EmailLayout, { bodyText, footerText } from "./components/EmailLayout";
|
||||||
|
|
||||||
|
export const MailingListUnsubscription = () => {
|
||||||
|
return (
|
||||||
|
<EmailLayout
|
||||||
|
subject={`${"{{.OrganizationName}}"} – You've been unsubscribed`}
|
||||||
|
>
|
||||||
|
<Text style={bodyText}>
|
||||||
|
You've been successfully unsubscribed from{" "}
|
||||||
|
<strong>{"{{.OrganizationName}}"}</strong>'s compliance updates. You
|
||||||
|
will no longer receive notifications when new information is published
|
||||||
|
on their compliance page.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Hr style={{ borderColor: "#ecefec", margin: "8px 0 20px" }} />
|
||||||
|
|
||||||
|
<Text style={{ ...footerText, textAlign: "center" }}>
|
||||||
|
This email was sent to confirm your unsubscription request.
|
||||||
|
</Text>
|
||||||
|
</EmailLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MailingListUnsubscription;
|
||||||
48
packages/emails/src/MailingListUpdates.tsx
Normal file
48
packages/emails/src/MailingListUpdates.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Button, Hr, Link, Section, Text } from "@react-email/components";
|
||||||
|
import * as React from "react";
|
||||||
|
import EmailLayout, {
|
||||||
|
bodyText,
|
||||||
|
button,
|
||||||
|
buttonContainer,
|
||||||
|
footerText,
|
||||||
|
} from "./components/EmailLayout";
|
||||||
|
|
||||||
|
export const MailingListUpdates = () => {
|
||||||
|
return (
|
||||||
|
<EmailLayout
|
||||||
|
subject={`${"{{.OrganizationName}}"} – ${"{{.NewsTitle}}"}`}
|
||||||
|
>
|
||||||
|
<Text style={bodyText}>
|
||||||
|
{"{{.OrganizationName}}"} has published a new compliance update.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={{ ...bodyText, fontWeight: "600", fontSize: "18px" }}>
|
||||||
|
{"{{.NewsTitle}}"}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={bodyText}>{"{{.NewsBody}}"}</Text>
|
||||||
|
|
||||||
|
<Section style={buttonContainer}>
|
||||||
|
<Button style={button} href={"{{.CompliancePageURL}}"}>
|
||||||
|
View Compliance Page
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Hr style={{ borderColor: "#ecefec", margin: "8px 0 20px" }} />
|
||||||
|
|
||||||
|
<Text style={{ ...footerText, textAlign: "center" }}>
|
||||||
|
You are receiving this email because you subscribed to compliance
|
||||||
|
updates from <strong>{"{{.OrganizationName}}"}</strong>. To
|
||||||
|
unsubscribe,{" "}
|
||||||
|
<Link
|
||||||
|
href={"{{.UnsubscribeURL}}"}
|
||||||
|
style={{ color: "#374151", textDecoration: "underline", fontWeight: "500" }}
|
||||||
|
>
|
||||||
|
click here.
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</EmailLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MailingListUpdates;
|
||||||
16
packages/emails/templates/mailing-list-subscription.txt
Normal file
16
packages/emails/templates/mailing-list-subscription.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{{.SenderCompanyName}}
|
||||||
|
|
||||||
|
Hi {{.RecipientFullName}},
|
||||||
|
|
||||||
|
You requested to subscribe to compliance updates from {{.OrganizationName}}.
|
||||||
|
|
||||||
|
Please click the link below to confirm your subscription:
|
||||||
|
|
||||||
|
{{.ConfirmURL}}
|
||||||
|
|
||||||
|
If you did not request this subscription, or no longer want to receive these emails, unsubscribe here:
|
||||||
|
|
||||||
|
{{.UnsubscribeURL}}
|
||||||
|
|
||||||
|
{{.SenderCompanyHeadquarterAddress}}
|
||||||
|
Powered By Probo
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{{.SenderCompanyName}}
|
||||||
|
|
||||||
|
Hi {{.RecipientFullName}},
|
||||||
|
|
||||||
|
You've been successfully unsubscribed from {{.OrganizationName}}'s compliance updates. You will no longer receive notifications when new information is published on their compliance page.
|
||||||
|
|
||||||
|
{{.SenderCompanyHeadquarterAddress}}
|
||||||
|
Powered By Probo
|
||||||
22
packages/emails/templates/mailing-list-updates.txt
Normal file
22
packages/emails/templates/mailing-list-updates.txt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{{.SenderCompanyName}}
|
||||||
|
|
||||||
|
Hi {{.RecipientFullName}},
|
||||||
|
|
||||||
|
{{.OrganizationName}} has published a new compliance update.
|
||||||
|
|
||||||
|
{{.NewsTitle}}
|
||||||
|
|
||||||
|
{{.NewsBody}}
|
||||||
|
|
||||||
|
View the compliance page:
|
||||||
|
|
||||||
|
{{.CompliancePageURL}}
|
||||||
|
|
||||||
|
You are receiving this email because you subscribed to compliance updates from {{.OrganizationName}}.
|
||||||
|
|
||||||
|
To unsubscribe, visit:
|
||||||
|
|
||||||
|
{{.UnsubscribeURL}}
|
||||||
|
|
||||||
|
{{.SenderCompanyHeadquarterAddress}}
|
||||||
|
Powered By Probo
|
||||||
@@ -31,6 +31,9 @@ type (
|
|||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
RecipientEmail string `db:"recipient_email"`
|
RecipientEmail string `db:"recipient_email"`
|
||||||
RecipientName string `db:"recipient_name"`
|
RecipientName string `db:"recipient_name"`
|
||||||
|
ReplyTo *mail.Addr `db:"reply_to"`
|
||||||
|
UnsubscribeURL *string `db:"unsubscribe_url"`
|
||||||
|
MailingListUpdateID *gid.GID `db:"mailing_list_update_id"`
|
||||||
Subject string `db:"subject"`
|
Subject string `db:"subject"`
|
||||||
TextBody string `db:"text_body"`
|
TextBody string `db:"text_body"`
|
||||||
HtmlBody *string `db:"html_body"`
|
HtmlBody *string `db:"html_body"`
|
||||||
@@ -44,6 +47,14 @@ type (
|
|||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
SentAt *time.Time `db:"sent_at"`
|
SentAt *time.Time `db:"sent_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Emails []*Email
|
||||||
|
|
||||||
|
EmailOptions struct {
|
||||||
|
ReplyTo *mail.Addr
|
||||||
|
UnsubscribeURL *string
|
||||||
|
MailingListUpdateID *gid.GID
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -62,9 +73,10 @@ func NewEmail(
|
|||||||
subject string,
|
subject string,
|
||||||
textBody string,
|
textBody string,
|
||||||
htmlBody *string,
|
htmlBody *string,
|
||||||
|
opts *EmailOptions,
|
||||||
) *Email {
|
) *Email {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
return &Email{
|
e := &Email{
|
||||||
ID: gid.New(gid.NilTenant, EmailEntityType),
|
ID: gid.New(gid.NilTenant, EmailEntityType),
|
||||||
RecipientName: recipientName,
|
RecipientName: recipientName,
|
||||||
RecipientEmail: recipientEmail.String(),
|
RecipientEmail: recipientEmail.String(),
|
||||||
@@ -77,6 +89,14 @@ func NewEmail(
|
|||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts != nil {
|
||||||
|
e.ReplyTo = opts.ReplyTo
|
||||||
|
e.UnsubscribeURL = opts.UnsubscribeURL
|
||||||
|
e.MailingListUpdateID = opts.MailingListUpdateID
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Email) Insert(
|
func (e *Email) Insert(
|
||||||
@@ -85,19 +105,45 @@ func (e *Email) Insert(
|
|||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
INSERT INTO emails (
|
INSERT INTO emails (
|
||||||
id, recipient_email, recipient_name, subject, text_body, html_body,
|
id,
|
||||||
status, attempt_count, max_attempts, created_at, updated_at
|
recipient_email,
|
||||||
|
recipient_name,
|
||||||
|
reply_to, unsubscribe_url,
|
||||||
|
mailing_list_update_id,
|
||||||
|
subject,
|
||||||
|
text_body,
|
||||||
|
html_body,
|
||||||
|
status,
|
||||||
|
attempt_count,
|
||||||
|
max_attempts,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
@id, @recipient_email, @recipient_name, @subject, @text_body, @html_body,
|
@id,
|
||||||
@status, @attempt_count, @max_attempts, @created_at, @updated_at
|
@recipient_email,
|
||||||
|
@recipient_name,
|
||||||
|
@reply_to,
|
||||||
|
@unsubscribe_url,
|
||||||
|
@mailing_list_update_id,
|
||||||
|
@subject,
|
||||||
|
@text_body,
|
||||||
|
@html_body,
|
||||||
|
@status,
|
||||||
|
@attempt_count,
|
||||||
|
@max_attempts,
|
||||||
|
@created_at,
|
||||||
|
@updated_at
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"id": e.ID,
|
"id": e.ID,
|
||||||
"recipient_email": e.RecipientEmail,
|
"recipient_email": e.RecipientEmail,
|
||||||
"recipient_name": e.RecipientName,
|
"recipient_name": e.RecipientName,
|
||||||
|
"reply_to": e.ReplyTo,
|
||||||
|
"unsubscribe_url": e.UnsubscribeURL,
|
||||||
|
"mailing_list_update_id": e.MailingListUpdateID,
|
||||||
"subject": e.Subject,
|
"subject": e.Subject,
|
||||||
"text_body": e.TextBody,
|
"text_body": e.TextBody,
|
||||||
"html_body": e.HtmlBody,
|
"html_body": e.HtmlBody,
|
||||||
@@ -112,13 +158,47 @@ VALUES (
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (emails Emails) BulkInsert(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
) error {
|
||||||
|
if len(emails) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([][]any, 0, len(emails))
|
||||||
|
for _, e := range emails {
|
||||||
|
rows = append(rows, []any{
|
||||||
|
e.ID,
|
||||||
|
e.RecipientEmail,
|
||||||
|
e.RecipientName,
|
||||||
|
e.ReplyTo,
|
||||||
|
e.UnsubscribeURL,
|
||||||
|
e.MailingListUpdateID,
|
||||||
|
e.Subject,
|
||||||
|
e.TextBody,
|
||||||
|
e.HtmlBody,
|
||||||
|
e.CreatedAt,
|
||||||
|
e.UpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := conn.CopyFrom(
|
||||||
|
ctx,
|
||||||
|
pgx.Identifier{"emails"},
|
||||||
|
[]string{"id", "recipient_email", "recipient_name", "reply_to", "unsubscribe_url", "mailing_list_update_id", "subject", "text_body", "html_body", "created_at", "updated_at"},
|
||||||
|
pgx.CopyFromRows(rows),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Email) LoadNextPendingForUpdateSkipLocked(
|
func (e *Email) LoadNextPendingForUpdateSkipLocked(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id, recipient_email, recipient_name, subject, text_body, html_body,
|
id, recipient_email, recipient_name, reply_to, unsubscribe_url, mailing_list_update_id, subject, text_body, html_body,
|
||||||
status, processing_started_at, attempt_count, max_attempts,
|
status, processing_started_at, attempt_count, max_attempts,
|
||||||
last_attempted_at, last_error, created_at, updated_at, sent_at
|
last_attempted_at, last_error, created_at, updated_at, sent_at
|
||||||
FROM emails
|
FROM emails
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ const (
|
|||||||
ComplianceExternalURLEntityType uint16 = 63
|
ComplianceExternalURLEntityType uint16 = 63
|
||||||
MailingListEntityType uint16 = 64
|
MailingListEntityType uint16 = 64
|
||||||
MailingListSubscriberEntityType uint16 = 65
|
MailingListSubscriberEntityType uint16 = 65
|
||||||
|
MailingListUpdateEntityType uint16 = 66
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewEntityFromID(id gid.GID) (any, bool) {
|
func NewEntityFromID(id gid.GID) (any, bool) {
|
||||||
@@ -221,6 +222,8 @@ func NewEntityFromID(id gid.GID) (any, bool) {
|
|||||||
return &MailingList{ID: id}, true
|
return &MailingList{ID: id}, true
|
||||||
case MailingListSubscriberEntityType:
|
case MailingListSubscriberEntityType:
|
||||||
return &MailingListSubscriber{ID: id}, true
|
return &MailingListSubscriber{ID: id}, true
|
||||||
|
case MailingListUpdateEntityType:
|
||||||
|
return &MailingListUpdate{ID: id}, true
|
||||||
default:
|
default:
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,56 @@ func (cns *MailingListSubscriber) CursorKey(orderBy MailingListSubscriberOrderFi
|
|||||||
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cns *MailingListSubscriber) LoadByID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
id gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
mailing_list_id,
|
||||||
|
full_name,
|
||||||
|
email,
|
||||||
|
status,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
mailing_list_subscribers
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND id = @id
|
||||||
|
LIMIT 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"id": id,
|
||||||
|
}
|
||||||
|
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) LoadByMailingListIDAndEmail(
|
func (cns *MailingListSubscriber) LoadByMailingListIDAndEmail(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
@@ -227,11 +277,15 @@ WHERE
|
|||||||
args := pgx.StrictNamedArgs{"mailing_list_subscriber_id": cns.ID}
|
args := pgx.StrictNamedArgs{"mailing_list_subscriber_id": cns.ID}
|
||||||
maps.Copy(args, scope.SQLArguments())
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
_, err := conn.Exec(ctx, q, args)
|
tag, err := conn.Exec(ctx, q, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot delete mailing list subscriber: %w", err)
|
return fmt.Errorf("cannot delete mailing list subscriber: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return ErrResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,6 +321,50 @@ WHERE
|
|||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cnss *MailingListSubscribers) LoadAllConfirmedByMailingListID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
) 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 status = 'CONFIRMED'
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"mailing_list_id": mailingListID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query confirmed mailing list subscribers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribers, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[MailingListSubscriber])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect confirmed mailing list subscribers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*cnss = subscribers
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (cnss *MailingListSubscribers) LoadByMailingListID(
|
func (cnss *MailingListSubscribers) LoadByMailingListID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
|||||||
385
pkg/coredata/mailing_list_update.go
Normal file
385
pkg/coredata/mailing_list_update.go
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
// 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/page"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
MailingListUpdate struct {
|
||||||
|
ID gid.GID `db:"id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
|
MailingListID gid.GID `db:"mailing_list_id"`
|
||||||
|
Title string `db:"title"`
|
||||||
|
Body string `db:"body"`
|
||||||
|
Status MailingListUpdateStatus `db:"status"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
MailingListUpdateItems []*MailingListUpdate
|
||||||
|
)
|
||||||
|
|
||||||
|
func (mlu *MailingListUpdate) CursorKey(orderBy MailingListUpdateOrderField) page.CursorKey {
|
||||||
|
switch orderBy {
|
||||||
|
case MailingListUpdateOrderFieldCreatedAt:
|
||||||
|
return page.NewCursorKey(mlu.ID, mlu.CreatedAt)
|
||||||
|
case MailingListUpdateOrderFieldUpdatedAt:
|
||||||
|
return page.NewCursorKey(mlu.ID, mlu.UpdatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mlu *MailingListUpdate) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) {
|
||||||
|
q := `SELECT organization_id FROM mailing_list_updates WHERE id = $1 LIMIT 1;`
|
||||||
|
|
||||||
|
var organizationID gid.GID
|
||||||
|
if err := conn.QueryRow(ctx, q, mlu.ID).Scan(&organizationID); err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, ErrResourceNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("cannot query mailing list update authorization attributes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]string{"organization_id": organizationID.String()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mlu *MailingListUpdate) Insert(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||||
|
q := `
|
||||||
|
INSERT INTO mailing_list_updates (
|
||||||
|
id,
|
||||||
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
|
mailing_list_id,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
status,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES (
|
||||||
|
@id,
|
||||||
|
@tenant_id,
|
||||||
|
@organization_id,
|
||||||
|
@mailing_list_id,
|
||||||
|
@title,
|
||||||
|
@body,
|
||||||
|
@status,
|
||||||
|
@created_at,
|
||||||
|
@updated_at
|
||||||
|
)
|
||||||
|
`
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"id": mlu.ID,
|
||||||
|
"organization_id": mlu.OrganizationID,
|
||||||
|
"mailing_list_id": mlu.MailingListID,
|
||||||
|
"title": mlu.Title,
|
||||||
|
"body": mlu.Body,
|
||||||
|
"status": mlu.Status,
|
||||||
|
"created_at": mlu.CreatedAt,
|
||||||
|
"updated_at": mlu.UpdatedAt,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
_, err := conn.Exec(ctx, q, args)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mlu *MailingListUpdate) Update(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||||
|
q := `
|
||||||
|
UPDATE mailing_list_updates
|
||||||
|
SET
|
||||||
|
title = @title,
|
||||||
|
body = @body,
|
||||||
|
status = @status,
|
||||||
|
updated_at = @updated_at
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND id = @id
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"id": mlu.ID,
|
||||||
|
"title": mlu.Title,
|
||||||
|
"body": mlu.Body,
|
||||||
|
"status": mlu.Status,
|
||||||
|
"updated_at": mlu.UpdatedAt,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
tag, err := conn.Exec(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot update mailing list update: %w", err)
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return ErrResourceNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mlu *MailingListUpdate) Delete(ctx context.Context, conn pg.Conn, scope Scoper) error {
|
||||||
|
q := `
|
||||||
|
DELETE FROM mailing_list_updates
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND id = @id
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"id": mlu.ID,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
tag, err := conn.Exec(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot delete mailing list update: %w", err)
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return ErrResourceNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mlu *MailingListUpdate) LoadByID(ctx context.Context, conn pg.Conn, scope Scoper, id gid.GID) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
mailing_list_id,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
status,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM mailing_list_updates
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND id = @id
|
||||||
|
LIMIT 1;
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"id": id,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query mailing list update: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[MailingListUpdate])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrResourceNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot collect mailing list update: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*mlu = result
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mlul *MailingListUpdateItems) LoadSentByMailingListID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
cursor *page.Cursor[MailingListUpdateOrderField],
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
mailing_list_id,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
status,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM mailing_list_updates
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND mailing_list_id = @mailing_list_id
|
||||||
|
AND status = 'SENT'
|
||||||
|
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 sent mailing list updates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[MailingListUpdate])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect sent mailing list updates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*mlul = results
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mlul *MailingListUpdateItems) LoadByMailingListID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
cursor *page.Cursor[MailingListUpdateOrderField],
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
mailing_list_id,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
status,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM mailing_list_updates
|
||||||
|
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 updates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[MailingListUpdate])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect mailing list updates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*mlul = results
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mlul *MailingListUpdateItems) CountByMailingListID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
) (int, error) {
|
||||||
|
q := `
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM mailing_list_updates
|
||||||
|
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())
|
||||||
|
|
||||||
|
var count int
|
||||||
|
if err := conn.QueryRow(ctx, q, args).Scan(&count); err != nil {
|
||||||
|
return 0, fmt.Errorf("cannot count mailing list updates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mlu *MailingListUpdate) LoadNextEnqueuedForUpdateSkipLocked(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
mailing_list_id,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
status,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM mailing_list_updates
|
||||||
|
WHERE status = 'ENQUEUED'
|
||||||
|
ORDER BY updated_at ASC
|
||||||
|
LIMIT 1
|
||||||
|
FOR UPDATE SKIP LOCKED
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query enqueued mailing list updates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[MailingListUpdate])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrResourceNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot collect enqueued mailing list update: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*mlu = result
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResetStaleProcessingMailingListUpdates(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
staleAfter time.Duration,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
UPDATE mailing_list_updates
|
||||||
|
SET status = 'ENQUEUED', updated_at = NOW()
|
||||||
|
WHERE status = 'PROCESSING'
|
||||||
|
AND updated_at < NOW() - @stale_after::interval
|
||||||
|
`
|
||||||
|
_, err := conn.Exec(ctx, q, pgx.StrictNamedArgs{"stale_after": staleAfter})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot reset stale processing mailing list updates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
39
pkg/coredata/mailing_list_update_order_field.go
Normal file
39
pkg/coredata/mailing_list_update_order_field.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// 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 MailingListUpdateOrderField string
|
||||||
|
|
||||||
|
const (
|
||||||
|
MailingListUpdateOrderFieldCreatedAt MailingListUpdateOrderField = "CREATED_AT"
|
||||||
|
MailingListUpdateOrderFieldUpdatedAt MailingListUpdateOrderField = "UPDATED_AT"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f MailingListUpdateOrderField) String() string {
|
||||||
|
return string(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f MailingListUpdateOrderField) Column() string {
|
||||||
|
switch f {
|
||||||
|
case MailingListUpdateOrderFieldCreatedAt:
|
||||||
|
return "created_at"
|
||||||
|
case MailingListUpdateOrderFieldUpdatedAt:
|
||||||
|
return "updated_at"
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(fmt.Sprintf("unsupported order by: %s", f))
|
||||||
|
}
|
||||||
64
pkg/coredata/mailing_list_update_status.go
Normal file
64
pkg/coredata/mailing_list_update_status.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// 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 MailingListUpdateStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
MailingListUpdateStatusDraft MailingListUpdateStatus = "DRAFT"
|
||||||
|
MailingListUpdateStatusEnqueued MailingListUpdateStatus = "ENQUEUED"
|
||||||
|
MailingListUpdateStatusProcessing MailingListUpdateStatus = "PROCESSING"
|
||||||
|
MailingListUpdateStatusSent MailingListUpdateStatus = "SENT"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s MailingListUpdateStatus) String() string {
|
||||||
|
return string(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MailingListUpdateStatus) 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 MailingListUpdateStatus: %T", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch str {
|
||||||
|
case "DRAFT":
|
||||||
|
*s = MailingListUpdateStatusDraft
|
||||||
|
case "ENQUEUED":
|
||||||
|
*s = MailingListUpdateStatusEnqueued
|
||||||
|
case "PROCESSING":
|
||||||
|
*s = MailingListUpdateStatusProcessing
|
||||||
|
case "SENT":
|
||||||
|
*s = MailingListUpdateStatusSent
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid MailingListUpdateStatus value: %q", str)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s MailingListUpdateStatus) Value() (driver.Value, error) {
|
||||||
|
return s.String(), nil
|
||||||
|
}
|
||||||
18
pkg/coredata/migrations/20260305T120000Z.sql
Normal file
18
pkg/coredata/migrations/20260305T120000Z.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
CREATE TYPE mailing_list_update_status AS ENUM ('DRAFT', 'ENQUEUED', 'PROCESSING', 'SENT');
|
||||||
|
|
||||||
|
CREATE TABLE mailing_list_updates (
|
||||||
|
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,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
body TEXT NOT NULL,
|
||||||
|
status mailing_list_update_status NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE emails
|
||||||
|
ADD COLUMN reply_to TEXT,
|
||||||
|
ADD COLUMN unsubscribe_url TEXT,
|
||||||
|
ADD COLUMN mailing_list_update_id TEXT REFERENCES mailing_list_updates(id) ON DELETE SET NULL;
|
||||||
@@ -120,6 +120,57 @@ LIMIT 1;
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tc *TrustCenter) LoadByMailingListID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
tenant_id,
|
||||||
|
mailing_list_id,
|
||||||
|
logo_file_id,
|
||||||
|
dark_logo_file_id,
|
||||||
|
active,
|
||||||
|
slug,
|
||||||
|
non_disclosure_agreement_file_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
trust_centers
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND mailing_list_id = @mailing_list_id
|
||||||
|
LIMIT 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"mailing_list_id": mailingListID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query trust center by mailing list id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
trustCenter, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[TrustCenter])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot collect trust center: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*tc = trustCenter
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (tc *TrustCenter) LoadByOrganizationID(
|
func (tc *TrustCenter) LoadByOrganizationID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
|||||||
@@ -321,6 +321,7 @@ func (w *CompletionCertificateWorker) generateCertificate(
|
|||||||
subject,
|
subject,
|
||||||
textBody,
|
textBody,
|
||||||
htmlBody,
|
htmlBody,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
attachments := coredata.EmailAttachments{
|
attachments := coredata.EmailAttachments{
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ func (s AccountService) ChangeEmail(ctx context.Context, identityID gid.GID, req
|
|||||||
subject,
|
subject,
|
||||||
textBody,
|
textBody,
|
||||||
htmlBody,
|
htmlBody,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
err = confirmationEmail.Insert(ctx, tx)
|
err = confirmationEmail.Insert(ctx, tx)
|
||||||
|
|||||||
@@ -352,6 +352,7 @@ func (s AuthService) SendPasswordResetInstructionByEmail(
|
|||||||
subject,
|
subject,
|
||||||
textBody,
|
textBody,
|
||||||
htmlBody,
|
htmlBody,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
err = passwordResetEmail.Insert(ctx, tx)
|
err = passwordResetEmail.Insert(ctx, tx)
|
||||||
@@ -420,6 +421,7 @@ func (s AuthService) CreateIdentityWithPassword(
|
|||||||
subject,
|
subject,
|
||||||
textBody,
|
textBody,
|
||||||
htmlBody,
|
htmlBody,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
err = s.pg.WithTx(
|
err = s.pg.WithTx(
|
||||||
@@ -617,6 +619,7 @@ func (s AuthService) SendMagicLink(ctx context.Context, req *SendMagicLinkReques
|
|||||||
subject,
|
subject,
|
||||||
textBody,
|
textBody,
|
||||||
htmlBody,
|
htmlBody,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := magicLinkEmail.Insert(ctx, tx); err != nil {
|
if err := magicLinkEmail.Insert(ctx, tx); err != nil {
|
||||||
|
|||||||
@@ -415,6 +415,7 @@ func (s *OrganizationService) InviteUser(
|
|||||||
subject,
|
subject,
|
||||||
textBody,
|
textBody,
|
||||||
htmlBody,
|
htmlBody,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
err = email.Insert(ctx, tx)
|
err = email.Insert(ctx, tx)
|
||||||
|
|||||||
@@ -208,10 +208,20 @@ func (w *SendingWorker) sendAndCommit(
|
|||||||
To(email.RecipientName, email.RecipientEmail).
|
To(email.RecipientName, email.RecipientEmail).
|
||||||
Text([]byte(email.TextBody))
|
Text([]byte(email.TextBody))
|
||||||
|
|
||||||
|
if email.ReplyTo != nil {
|
||||||
|
mail = mail.ReplyTo("", email.ReplyTo.String())
|
||||||
|
}
|
||||||
|
|
||||||
if email.HtmlBody != nil {
|
if email.HtmlBody != nil {
|
||||||
mail = mail.HTML([]byte(*email.HtmlBody))
|
mail = mail.HTML([]byte(*email.HtmlBody))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if email.UnsubscribeURL != nil {
|
||||||
|
mail = mail.
|
||||||
|
Header("List-Unsubscribe", "<"+*email.UnsubscribeURL+">").
|
||||||
|
Header("List-Unsubscribe-Post", "List-Unsubscribe=One-Click")
|
||||||
|
}
|
||||||
|
|
||||||
for _, att := range attachments {
|
for _, att := range attachments {
|
||||||
var file coredata.File
|
var file coredata.File
|
||||||
if err := file.LoadByID(ctx, conn, coredata.NewNoScope(), att.FileID); err != nil {
|
if err := file.LoadByID(ctx, conn, coredata.NewNoScope(), att.FileID); err != nil {
|
||||||
|
|||||||
161
pkg/mailman/compliance_mailing_list.go
Normal file
161
pkg/mailman/compliance_mailing_list.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
// 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"
|
||||||
|
|
||||||
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/packages/emails"
|
||||||
|
"go.probo.inc/probo/pkg/baseurl"
|
||||||
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/mail"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) SubscriptionConfirmationEmailConfig(
|
||||||
|
ctx context.Context,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
) (emails.PresenterConfig, string, *mail.Addr, error) {
|
||||||
|
cfg, orgName, _, replyTo, err := s.mailingListEmailConfig(ctx, mailingListID)
|
||||||
|
return cfg, orgName, replyTo, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UnsubscribeEmailConfig(
|
||||||
|
ctx context.Context,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
) (emails.PresenterConfig, string, *mail.Addr, error) {
|
||||||
|
cfg, orgName, _, replyTo, err := s.mailingListEmailConfig(ctx, mailingListID)
|
||||||
|
return cfg, orgName, replyTo, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdateEmailConfig(
|
||||||
|
ctx context.Context,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
) (emails.PresenterConfig, string, string, *mail.Addr, error) {
|
||||||
|
return s.mailingListEmailConfig(ctx, mailingListID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) mailingListEmailConfig(
|
||||||
|
ctx context.Context,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
) (emails.PresenterConfig, string, string, *mail.Addr, error) {
|
||||||
|
var (
|
||||||
|
mailingList = &coredata.MailingList{}
|
||||||
|
compliancePage = &coredata.TrustCenter{}
|
||||||
|
organization = &coredata.Organization{}
|
||||||
|
customDomain *coredata.CustomDomain
|
||||||
|
logoFile = &coredata.File{}
|
||||||
|
defaultCfg = emails.DefaultPresenterConfig(s.bucket, s.apiBaseURL.String())
|
||||||
|
)
|
||||||
|
|
||||||
|
scope := coredata.NewScopeFromObjectID(mailingListID)
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := mailingList.LoadByID(ctx, conn, scope, mailingListID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load mailing list: %w", err)
|
||||||
|
}
|
||||||
|
if err := compliancePage.LoadByMailingListID(ctx, conn, scope, mailingListID); err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot load compliance page: %w", err)
|
||||||
|
}
|
||||||
|
if compliancePage.LogoFileID != nil {
|
||||||
|
if err := logoFile.LoadByID(ctx, conn, scope, *compliancePage.LogoFileID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load logo file: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := organization.LoadByID(ctx, conn, scope, compliancePage.OrganizationID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load organization: %w", err)
|
||||||
|
}
|
||||||
|
customDomain = &coredata.CustomDomain{}
|
||||||
|
if err := customDomain.LoadByOrganizationID(ctx, conn, scope, organization.ID); err != nil {
|
||||||
|
if !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return fmt.Errorf("cannot load custom domain: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return defaultCfg, "", "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, compliancePageURL, err := s.presenterConfigFromTrustCenter(compliancePage, organization, customDomain, logoFile)
|
||||||
|
if err != nil {
|
||||||
|
return defaultCfg, "", "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
compliancePageBase, err := baseurl.Parse(compliancePageURL)
|
||||||
|
if err != nil {
|
||||||
|
return defaultCfg, "", "", nil, fmt.Errorf("cannot parse compliance page URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatesPageURL, err := compliancePageBase.AppendPath("/updates").String()
|
||||||
|
if err != nil {
|
||||||
|
return defaultCfg, "", "", nil, fmt.Errorf("cannot build updates page URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, organization.Name, updatesPageURL, mailingList.ReplyTo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) presenterConfigFromTrustCenter(
|
||||||
|
compliancePage *coredata.TrustCenter,
|
||||||
|
organization *coredata.Organization,
|
||||||
|
customDomain *coredata.CustomDomain,
|
||||||
|
logoFile *coredata.File,
|
||||||
|
) (emails.PresenterConfig, string, error) {
|
||||||
|
cfg := emails.DefaultPresenterConfig(s.bucket, s.apiBaseURL.String())
|
||||||
|
|
||||||
|
compliancePageBase := s.apiBaseURL.WithPath("/trust/" + compliancePage.ID.String())
|
||||||
|
|
||||||
|
if customDomain != nil && customDomain.SSLStatus == coredata.CustomDomainSSLStatusActive {
|
||||||
|
customBase, err := baseurl.Parse("https://" + customDomain.Domain)
|
||||||
|
if err != nil {
|
||||||
|
return cfg, "", fmt.Errorf("cannot parse custom domain URL: %w", err)
|
||||||
|
}
|
||||||
|
compliancePageBase = customBase.WithPath("")
|
||||||
|
}
|
||||||
|
|
||||||
|
compliancePageURL, err := compliancePageBase.String()
|
||||||
|
if err != nil {
|
||||||
|
return cfg, "", fmt.Errorf("cannot build compliance page URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.BaseURL = compliancePageURL
|
||||||
|
|
||||||
|
if compliancePage.LogoFileID != nil && logoFile != nil && logoFile.FileKey != "" {
|
||||||
|
cfg.SenderCompanyLogo = emails.Asset{
|
||||||
|
Name: logoFile.FileName,
|
||||||
|
ObjectKey: logoFile.FileKey,
|
||||||
|
BucketName: logoFile.BucketName,
|
||||||
|
MimeType: logoFile.MimeType,
|
||||||
|
}
|
||||||
|
cfg.SenderCompanyName = organization.Name
|
||||||
|
if organization.WebsiteURL != nil {
|
||||||
|
cfg.SenderCompanyWebsiteURL = *organization.WebsiteURL
|
||||||
|
}
|
||||||
|
if organization.HeadquarterAddress != nil {
|
||||||
|
cfg.SenderCompanyHeadquarterAddress = *organization.HeadquarterAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, compliancePageURL, nil
|
||||||
|
}
|
||||||
@@ -20,4 +20,6 @@ var (
|
|||||||
ErrMailingListNotFound = errors.New("mailing list not found")
|
ErrMailingListNotFound = errors.New("mailing list not found")
|
||||||
ErrSubscriberNotFound = errors.New("mailing list subscriber not found")
|
ErrSubscriberNotFound = errors.New("mailing list subscriber not found")
|
||||||
ErrSubscriberAlreadyExist = errors.New("mailing list subscriber already exists")
|
ErrSubscriberAlreadyExist = errors.New("mailing list subscriber already exists")
|
||||||
|
ErrMailingListUpdateNotFound = errors.New("mailing list update not found")
|
||||||
|
ErrMailingListUpdateAlreadySent = errors.New("mailing list update already sent")
|
||||||
)
|
)
|
||||||
|
|||||||
230
pkg/mailman/mailing_list_worker.go
Normal file
230
pkg/mailman/mailing_list_worker.go
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
// 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"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.gearno.de/kit/log"
|
||||||
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
MailingListWorker struct {
|
||||||
|
service *Service
|
||||||
|
pg *pg.Client
|
||||||
|
logger *log.Logger
|
||||||
|
interval time.Duration
|
||||||
|
staleAfter time.Duration
|
||||||
|
maxConcurrency int
|
||||||
|
}
|
||||||
|
|
||||||
|
MailingListWorkerOption func(*MailingListWorker)
|
||||||
|
)
|
||||||
|
|
||||||
|
func WithMailingListWorkerInterval(d time.Duration) MailingListWorkerOption {
|
||||||
|
return func(w *MailingListWorker) { w.interval = d }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMailingListWorkerStaleAfter(d time.Duration) MailingListWorkerOption {
|
||||||
|
return func(w *MailingListWorker) { w.staleAfter = d }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMailingListWorkerMaxConcurrency(n int) MailingListWorkerOption {
|
||||||
|
return func(w *MailingListWorker) {
|
||||||
|
if n > 0 {
|
||||||
|
w.maxConcurrency = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMailingListWorker(
|
||||||
|
service *Service,
|
||||||
|
pgClient *pg.Client,
|
||||||
|
logger *log.Logger,
|
||||||
|
opts ...MailingListWorkerOption,
|
||||||
|
) *MailingListWorker {
|
||||||
|
w := &MailingListWorker{
|
||||||
|
service: service,
|
||||||
|
pg: pgClient,
|
||||||
|
logger: logger,
|
||||||
|
interval: 10 * time.Second,
|
||||||
|
staleAfter: 5 * time.Minute,
|
||||||
|
maxConcurrency: 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *MailingListWorker) Run(ctx context.Context) error {
|
||||||
|
var (
|
||||||
|
wg sync.WaitGroup
|
||||||
|
sem = make(chan struct{}, w.maxConcurrency)
|
||||||
|
)
|
||||||
|
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
LOOP:
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(w.interval):
|
||||||
|
// From there we should not accept cancellations anymore.
|
||||||
|
nonCancelableCtx := context.WithoutCancel(ctx)
|
||||||
|
w.recoverStaleRows(nonCancelableCtx)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if err := w.processNext(ctx, sem, &wg); err != nil {
|
||||||
|
if !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
w.logger.ErrorCtx(nonCancelableCtx, "cannot claim mailing list update", log.Error(err))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
goto LOOP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *MailingListWorker) processNext(ctx context.Context, sem chan struct{}, wg *sync.WaitGroup) error {
|
||||||
|
select {
|
||||||
|
case sem <- struct{}{}:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
mlu coredata.MailingListUpdate
|
||||||
|
now = time.Now()
|
||||||
|
nonCancelableCtx = context.WithoutCancel(ctx)
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := w.pg.WithTx(
|
||||||
|
nonCancelableCtx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
if err := mlu.LoadNextEnqueuedForUpdateSkipLocked(nonCancelableCtx, tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
scope := coredata.NewScopeFromObjectID(mlu.ID)
|
||||||
|
mlu.Status = coredata.MailingListUpdateStatusProcessing
|
||||||
|
mlu.UpdatedAt = now
|
||||||
|
|
||||||
|
if err := mlu.Update(nonCancelableCtx, tx, scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot claim mailing list update: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
<-sem
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func(mlu coredata.MailingListUpdate) {
|
||||||
|
defer wg.Done()
|
||||||
|
defer func() { <-sem }()
|
||||||
|
|
||||||
|
if err := w.sendAndCommit(nonCancelableCtx, &mlu); err != nil {
|
||||||
|
w.logger.ErrorCtx(nonCancelableCtx, "cannot send mailing list update",
|
||||||
|
log.Error(err),
|
||||||
|
log.String("mailing_list_update_id", mlu.ID.String()),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := w.resetEnqueued(nonCancelableCtx, &mlu); err != nil {
|
||||||
|
w.logger.ErrorCtx(nonCancelableCtx, "cannot reset mailing list update to enqueued",
|
||||||
|
log.Error(err),
|
||||||
|
log.String("mailing_list_update_id", mlu.ID.String()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(mlu)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *MailingListWorker) sendAndCommit(ctx context.Context, mlu *coredata.MailingListUpdate) error {
|
||||||
|
if err := w.service.CreateUpdateEmails(ctx, mlu.MailingListID, mlu.ID, mlu.Title, mlu.Body); err != nil {
|
||||||
|
return fmt.Errorf("cannot create update emails: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
scope := coredata.NewScopeFromObjectID(mlu.ID)
|
||||||
|
|
||||||
|
var current coredata.MailingListUpdate
|
||||||
|
if err := current.LoadByID(ctx, tx, scope, mlu.ID); err != nil {
|
||||||
|
return fmt.Errorf("cannot reload mailing list update: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if current.Status != coredata.MailingListUpdateStatusProcessing {
|
||||||
|
return fmt.Errorf("unexpected status %s, expected PROCESSING", current.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
mlu.Status = coredata.MailingListUpdateStatusSent
|
||||||
|
mlu.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
if err := mlu.Update(ctx, tx, scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot mark mailing list update as sent: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *MailingListWorker) resetEnqueued(ctx context.Context, mlu *coredata.MailingListUpdate) error {
|
||||||
|
return w.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
scope := coredata.NewScopeFromObjectID(mlu.ID)
|
||||||
|
mlu.Status = coredata.MailingListUpdateStatusEnqueued
|
||||||
|
mlu.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
if err := mlu.Update(ctx, tx, scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot reset mailing list update: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *MailingListWorker) recoverStaleRows(ctx context.Context) {
|
||||||
|
err := w.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := coredata.ResetStaleProcessingMailingListUpdates(ctx, conn, w.staleAfter); err != nil {
|
||||||
|
return fmt.Errorf("cannot reset stale processing mailing list updates: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
w.logger.ErrorCtx(ctx, "cannot recover stale processing mailing list updates", log.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,19 +20,35 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.gearno.de/kit/log"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/packages/emails"
|
||||||
|
"go.probo.inc/probo/pkg/baseurl"
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
"go.probo.inc/probo/pkg/crypto/cipher"
|
||||||
|
"go.probo.inc/probo/pkg/filemanager"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
"go.probo.inc/probo/pkg/gid"
|
||||||
"go.probo.inc/probo/pkg/mail"
|
"go.probo.inc/probo/pkg/mail"
|
||||||
"go.probo.inc/probo/pkg/page"
|
"go.probo.inc/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pathUnsubscribe = "/mail-actions/unsubscribe"
|
||||||
|
pathConfirm = "/mail-actions/confirm"
|
||||||
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
pg *pg.Client
|
pg *pg.Client
|
||||||
|
fm *filemanager.Service
|
||||||
|
tokenSecret string
|
||||||
|
apiBaseURL *baseurl.BaseURL
|
||||||
|
bucket string
|
||||||
|
encryptionKey cipher.EncryptionKey
|
||||||
|
logger *log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(pgClient *pg.Client) *Service {
|
func NewService(pgClient *pg.Client, fm *filemanager.Service, tokenSecret string, apiBaseURL *baseurl.BaseURL, bucket string, encryptionKey cipher.EncryptionKey, logger *log.Logger) *Service {
|
||||||
return &Service{pg: pgClient}
|
return &Service{pg: pgClient, fm: fm, tokenSecret: tokenSecret, apiBaseURL: apiBaseURL, bucket: bucket, encryptionKey: encryptionKey, logger: logger}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) UpdateMailingList(
|
func (s *Service) UpdateMailingList(
|
||||||
@@ -40,8 +56,8 @@ func (s *Service) UpdateMailingList(
|
|||||||
id gid.GID,
|
id gid.GID,
|
||||||
replyTo *mail.Addr,
|
replyTo *mail.Addr,
|
||||||
) (*coredata.MailingList, error) {
|
) (*coredata.MailingList, error) {
|
||||||
|
var ml coredata.MailingList
|
||||||
scope := coredata.NewScopeFromObjectID(id)
|
scope := coredata.NewScopeFromObjectID(id)
|
||||||
ml := coredata.MailingList{}
|
|
||||||
|
|
||||||
err := s.pg.WithConn(
|
err := s.pg.WithConn(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -105,8 +121,12 @@ func (s *Service) CreateSubscriber(
|
|||||||
fullName string,
|
fullName string,
|
||||||
) (*coredata.MailingListSubscriber, error) {
|
) (*coredata.MailingListSubscriber, error) {
|
||||||
scope := coredata.NewScopeFromObjectID(mailingListID)
|
scope := coredata.NewScopeFromObjectID(mailingListID)
|
||||||
now := time.Now()
|
emailRecord, err := s.buildConfirmationMail(ctx, mailingListID, email, fullName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot build confirmation mail: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
subscriber := &coredata.MailingListSubscriber{
|
subscriber := &coredata.MailingListSubscriber{
|
||||||
ID: gid.New(scope.GetTenantID(), coredata.MailingListSubscriberEntityType),
|
ID: gid.New(scope.GetTenantID(), coredata.MailingListSubscriberEntityType),
|
||||||
MailingListID: mailingListID,
|
MailingListID: mailingListID,
|
||||||
@@ -117,53 +137,150 @@ func (s *Service) CreateSubscriber(
|
|||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.pg.WithConn(
|
if err := s.pg.WithTx(
|
||||||
ctx,
|
ctx,
|
||||||
func(conn pg.Conn) error {
|
func(tx pg.Conn) error {
|
||||||
ml := coredata.MailingList{}
|
var ml coredata.MailingList
|
||||||
if err := ml.LoadByID(ctx, conn, scope, mailingListID); err != nil {
|
if err := ml.LoadByID(ctx, tx, scope, mailingListID); err != nil {
|
||||||
return fmt.Errorf("cannot load mailing list: %w", err)
|
return fmt.Errorf("cannot load mailing list: %w", err)
|
||||||
}
|
}
|
||||||
subscriber.OrganizationID = ml.OrganizationID
|
subscriber.OrganizationID = ml.OrganizationID
|
||||||
|
|
||||||
if err := subscriber.Insert(ctx, conn, scope); err != nil {
|
if err := subscriber.Insert(ctx, tx, scope); err != nil {
|
||||||
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
||||||
return ErrSubscriberAlreadyExist
|
return ErrSubscriberAlreadyExist
|
||||||
}
|
}
|
||||||
return fmt.Errorf("cannot insert mailing list subscriber: %w", err)
|
return fmt.Errorf("cannot insert mailing list subscriber: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := emailRecord.Insert(ctx, tx); err != nil {
|
||||||
|
return fmt.Errorf("cannot insert subscription confirmation email: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)
|
); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return subscriber, nil
|
return subscriber, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) UnsubscribeByEmail(
|
||||||
|
ctx context.Context,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
email mail.Addr,
|
||||||
|
) error {
|
||||||
|
scope := coredata.NewScopeFromObjectID(mailingListID)
|
||||||
|
return s.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
var subscriber coredata.MailingListSubscriber
|
||||||
|
if err := subscriber.LoadByMailingListIDAndEmail(ctx, tx, scope, mailingListID, email); err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return ErrSubscriberNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot load mailing list subscriber: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wasConfirmed := subscriber.Status == coredata.MailingListSubscriberStatusConfirmed
|
||||||
|
|
||||||
|
if err := subscriber.Delete(ctx, tx, scope); err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return ErrSubscriberNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot delete mailing list subscriber: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wasConfirmed {
|
||||||
|
emailRecord, err := s.buildUnsubscriptionMail(ctx, mailingListID, subscriber.Email, subscriber.FullName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot build unsubscription email: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := emailRecord.Insert(ctx, tx); err != nil {
|
||||||
|
return fmt.Errorf("cannot insert unsubscription email: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ConfirmSubscriberByEmail(
|
||||||
|
ctx context.Context,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
email mail.Addr,
|
||||||
|
) error {
|
||||||
|
scope := coredata.NewScopeFromObjectID(mailingListID)
|
||||||
|
|
||||||
|
return s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
var subscriber coredata.MailingListSubscriber
|
||||||
|
if err := subscriber.LoadByMailingListIDAndEmail(ctx, conn, scope, mailingListID, email); err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return ErrSubscriberNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot load mailing list subscriber: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriber.Status = coredata.MailingListSubscriberStatusConfirmed
|
||||||
|
subscriber.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
if err := subscriber.Update(ctx, conn, scope); err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return ErrSubscriberNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot update mailing list subscriber: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) DeleteSubscriber(
|
func (s *Service) DeleteSubscriber(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id gid.GID,
|
id gid.GID,
|
||||||
) error {
|
) error {
|
||||||
scope := coredata.NewScopeFromObjectID(id)
|
scope := coredata.NewScopeFromObjectID(id)
|
||||||
|
|
||||||
err := s.pg.WithConn(
|
return s.pg.WithTx(
|
||||||
ctx,
|
ctx,
|
||||||
func(conn pg.Conn) error {
|
func(tx pg.Conn) error {
|
||||||
subscriber := coredata.MailingListSubscriber{ID: id}
|
var subscriber coredata.MailingListSubscriber
|
||||||
if err := subscriber.Delete(ctx, conn, scope); err != nil {
|
if err := subscriber.LoadByID(ctx, tx, scope, id); err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return ErrSubscriberNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot load mailing list subscriber: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wasConfirmed := subscriber.Status == coredata.MailingListSubscriberStatusConfirmed
|
||||||
|
|
||||||
|
if err := subscriber.Delete(ctx, tx, scope); err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return ErrSubscriberNotFound
|
||||||
|
}
|
||||||
return fmt.Errorf("cannot delete mailing list subscriber: %w", err)
|
return fmt.Errorf("cannot delete mailing list subscriber: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
},
|
if wasConfirmed {
|
||||||
)
|
emailRecord, err := s.buildUnsubscriptionMail(ctx, subscriber.MailingListID, subscriber.Email, subscriber.FullName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("cannot build unsubscription email: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := emailRecord.Insert(ctx, tx); err != nil {
|
||||||
|
return fmt.Errorf("cannot insert unsubscription email: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CountSubscribers(
|
func (s *Service) CountSubscribers(
|
||||||
@@ -171,7 +288,7 @@ func (s *Service) CountSubscribers(
|
|||||||
mailingListID gid.GID,
|
mailingListID gid.GID,
|
||||||
) (int, error) {
|
) (int, error) {
|
||||||
scope := coredata.NewScopeFromObjectID(mailingListID)
|
scope := coredata.NewScopeFromObjectID(mailingListID)
|
||||||
count := 0
|
var count int
|
||||||
|
|
||||||
err := s.pg.WithConn(
|
err := s.pg.WithConn(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -197,7 +314,7 @@ func (s *Service) ListSubscribers(
|
|||||||
cursor *page.Cursor[coredata.MailingListSubscriberOrderField],
|
cursor *page.Cursor[coredata.MailingListSubscriberOrderField],
|
||||||
) (*page.Page[*coredata.MailingListSubscriber, coredata.MailingListSubscriberOrderField], error) {
|
) (*page.Page[*coredata.MailingListSubscriber, coredata.MailingListSubscriberOrderField], error) {
|
||||||
scope := coredata.NewScopeFromObjectID(mailingListID)
|
scope := coredata.NewScopeFromObjectID(mailingListID)
|
||||||
subscribers := coredata.MailingListSubscribers{}
|
var subscribers coredata.MailingListSubscribers
|
||||||
|
|
||||||
err := s.pg.WithConn(
|
err := s.pg.WithConn(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -214,3 +331,386 @@ func (s *Service) ListSubscribers(
|
|||||||
|
|
||||||
return page.NewPage(subscribers, cursor), nil
|
return page.NewPage(subscribers, cursor), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateMailingListUpdate(
|
||||||
|
ctx context.Context,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
title string,
|
||||||
|
body string,
|
||||||
|
) (*coredata.MailingListUpdate, error) {
|
||||||
|
scope := coredata.NewScopeFromObjectID(mailingListID)
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
mlu := &coredata.MailingListUpdate{
|
||||||
|
ID: gid.New(scope.GetTenantID(), coredata.MailingListUpdateEntityType),
|
||||||
|
MailingListID: mailingListID,
|
||||||
|
Title: title,
|
||||||
|
Body: body,
|
||||||
|
Status: coredata.MailingListUpdateStatusDraft,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
var ml coredata.MailingList
|
||||||
|
if err := ml.LoadByID(ctx, conn, scope, mailingListID); err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return ErrMailingListNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot load mailing list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mlu.OrganizationID = ml.OrganizationID
|
||||||
|
|
||||||
|
if err := mlu.Insert(ctx, conn, scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot insert mailing list update: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mlu, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetMailingListUpdate(
|
||||||
|
ctx context.Context,
|
||||||
|
id gid.GID,
|
||||||
|
) (*coredata.MailingListUpdate, error) {
|
||||||
|
scope := coredata.NewScopeFromObjectID(id)
|
||||||
|
var mlu coredata.MailingListUpdate
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := mlu.LoadByID(ctx, conn, scope, id); err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return ErrMailingListUpdateNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot load mailing list update: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &mlu, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdateMailingListUpdate(
|
||||||
|
ctx context.Context,
|
||||||
|
id gid.GID,
|
||||||
|
title string,
|
||||||
|
body string,
|
||||||
|
) (*coredata.MailingListUpdate, error) {
|
||||||
|
scope := coredata.NewScopeFromObjectID(id)
|
||||||
|
var mlu coredata.MailingListUpdate
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := mlu.LoadByID(ctx, conn, scope, id); err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return ErrMailingListUpdateNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot load mailing list update: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mlu.Status != coredata.MailingListUpdateStatusDraft {
|
||||||
|
return ErrMailingListUpdateAlreadySent
|
||||||
|
}
|
||||||
|
|
||||||
|
mlu.Title = title
|
||||||
|
mlu.Body = body
|
||||||
|
mlu.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
if err := mlu.Update(ctx, conn, scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot update mailing list update: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &mlu, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) SendMailingListUpdate(
|
||||||
|
ctx context.Context,
|
||||||
|
id gid.GID,
|
||||||
|
) (*coredata.MailingListUpdate, error) {
|
||||||
|
scope := coredata.NewScopeFromObjectID(id)
|
||||||
|
var mlu coredata.MailingListUpdate
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := mlu.LoadByID(ctx, conn, scope, id); err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return ErrMailingListUpdateNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot load mailing list update: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mlu.Status != coredata.MailingListUpdateStatusDraft {
|
||||||
|
return ErrMailingListUpdateAlreadySent
|
||||||
|
}
|
||||||
|
|
||||||
|
mlu.Status = coredata.MailingListUpdateStatusEnqueued
|
||||||
|
mlu.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
if err := mlu.Update(ctx, conn, scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot queue mailing list update for sending: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &mlu, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) DeleteMailingListUpdate(
|
||||||
|
ctx context.Context,
|
||||||
|
id gid.GID,
|
||||||
|
) error {
|
||||||
|
scope := coredata.NewScopeFromObjectID(id)
|
||||||
|
|
||||||
|
return s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
mlu := coredata.MailingListUpdate{ID: id}
|
||||||
|
if err := mlu.Delete(ctx, conn, scope); err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return ErrMailingListUpdateNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("cannot delete mailing list update: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListMailingListUpdates(
|
||||||
|
ctx context.Context,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
cursor *page.Cursor[coredata.MailingListUpdateOrderField],
|
||||||
|
) (*page.Page[*coredata.MailingListUpdate, coredata.MailingListUpdateOrderField], error) {
|
||||||
|
scope := coredata.NewScopeFromObjectID(mailingListID)
|
||||||
|
var items coredata.MailingListUpdateItems
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := items.LoadByMailingListID(ctx, conn, scope, mailingListID, cursor); err != nil {
|
||||||
|
return fmt.Errorf("cannot load mailing list updates: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return page.NewPage(items, cursor), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListSentMailingListUpdates(
|
||||||
|
ctx context.Context,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
cursor *page.Cursor[coredata.MailingListUpdateOrderField],
|
||||||
|
) (*page.Page[*coredata.MailingListUpdate, coredata.MailingListUpdateOrderField], error) {
|
||||||
|
scope := coredata.NewScopeFromObjectID(mailingListID)
|
||||||
|
var items coredata.MailingListUpdateItems
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
if err := items.LoadSentByMailingListID(ctx, conn, scope, mailingListID, cursor); err != nil {
|
||||||
|
return fmt.Errorf("cannot load sent mailing list updates: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return page.NewPage(items, cursor), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CountMailingListUpdates(
|
||||||
|
ctx context.Context,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
) (int, error) {
|
||||||
|
scope := coredata.NewScopeFromObjectID(mailingListID)
|
||||||
|
var count int
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
var items coredata.MailingListUpdateItems
|
||||||
|
var err error
|
||||||
|
count, err = items.CountByMailingListID(ctx, conn, scope, mailingListID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot count mailing list updates: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateUpdateEmails(
|
||||||
|
ctx context.Context,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
mailingListUpdateID gid.GID,
|
||||||
|
updateTitle string,
|
||||||
|
updateBody string,
|
||||||
|
) error {
|
||||||
|
scope := coredata.NewScopeFromObjectID(mailingListID)
|
||||||
|
|
||||||
|
presenterCfg, orgName, compliancePageURL, replyTo, err := s.UpdateEmailConfig(ctx, mailingListID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot get update email config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
var subscribers coredata.MailingListSubscribers
|
||||||
|
if err := subscribers.LoadAllConfirmedByMailingListID(ctx, tx, scope, mailingListID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load confirmed subscribers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(subscribers) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
emailRecords := make(coredata.Emails, 0, len(subscribers))
|
||||||
|
for _, sub := range subscribers {
|
||||||
|
unsubscribeURL, err := s.buildUnsubscribeURL(mailingListID, sub.Email)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot generate unsubscribe URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
subject, textBody, htmlBody, err := emails.NewPresenterFromConfig(s.fm, presenterCfg, sub.FullName).
|
||||||
|
RenderMailingListNews(ctx, orgName, updateTitle, updateBody, compliancePageURL, unsubscribeURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot render mailing list update email: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
emailRecords = append(
|
||||||
|
emailRecords,
|
||||||
|
coredata.NewEmail(
|
||||||
|
sub.FullName,
|
||||||
|
sub.Email,
|
||||||
|
subject,
|
||||||
|
textBody,
|
||||||
|
htmlBody,
|
||||||
|
&coredata.EmailOptions{
|
||||||
|
ReplyTo: replyTo,
|
||||||
|
UnsubscribeURL: &unsubscribeURL,
|
||||||
|
MailingListUpdateID: &mailingListUpdateID,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := emailRecords.BulkInsert(ctx, tx); err != nil {
|
||||||
|
return fmt.Errorf("cannot bulk insert update emails: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) buildConfirmationMail(
|
||||||
|
ctx context.Context,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
email mail.Addr,
|
||||||
|
fullName string,
|
||||||
|
) (*coredata.Email, error) {
|
||||||
|
unsubscribeURL, err := s.buildUnsubscribeURL(mailingListID, email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot generate unsubscribe URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmURL, err := s.buildConfirmURL(mailingListID, email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot generate confirm URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
presenterCfg, orgName, replyTo, err := s.SubscriptionConfirmationEmailConfig(ctx, mailingListID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot get subscription confirmation email config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
subject, textBody, htmlBody, err := emails.NewPresenterFromConfig(s.fm, presenterCfg, fullName).
|
||||||
|
RenderMailingListSubscription(ctx, orgName, confirmURL, unsubscribeURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot render subscription confirmation email: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return coredata.NewEmail(fullName, email, subject, textBody, htmlBody, &coredata.EmailOptions{ReplyTo: replyTo, UnsubscribeURL: &unsubscribeURL}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) buildUnsubscriptionMail(
|
||||||
|
ctx context.Context,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
email mail.Addr,
|
||||||
|
fullName string,
|
||||||
|
) (*coredata.Email, error) {
|
||||||
|
presenterCfg, orgName, replyTo, err := s.UnsubscribeEmailConfig(ctx, mailingListID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot get unsubscription email config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
subject, textBody, htmlBody, err := emails.NewPresenterFromConfig(s.fm, presenterCfg, fullName).
|
||||||
|
RenderMailingListUnsubscription(ctx, orgName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot render unsubscription email: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return coredata.NewEmail(fullName, email, subject, textBody, htmlBody, &coredata.EmailOptions{ReplyTo: replyTo}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) buildUnsubscribeURL(mailingListID gid.GID, email mail.Addr) (string, error) {
|
||||||
|
if s.tokenSecret == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
token, err := newUnsubscribeToken(s.tokenSecret, mailingListID, email)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return s.apiBaseURL.WithPath(pathUnsubscribe).WithQuery("token", token).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) buildConfirmURL(mailingListID gid.GID, email mail.Addr) (string, error) {
|
||||||
|
if s.tokenSecret == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
token, err := newConfirmToken(s.tokenSecret, mailingListID, email)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return s.apiBaseURL.WithPath(pathConfirm).WithQuery("token", token).String()
|
||||||
|
}
|
||||||
|
|||||||
86
pkg/mailman/token.go
Normal file
86
pkg/mailman/token.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/mail"
|
||||||
|
"go.probo.inc/probo/pkg/statelesstoken"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TokenTypeUnsubscribe = "mailing_list_unsubscribe"
|
||||||
|
TokenTypeConfirm = "mailing_list_confirm_subscription"
|
||||||
|
|
||||||
|
unsubscribeTokenExpiry = 365 * 24 * time.Hour
|
||||||
|
confirmTokenExpiry = 15 * 24 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnsubscribeTokenData struct {
|
||||||
|
MailingListID gid.GID `json:"m"`
|
||||||
|
Email mail.Addr `json:"e"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUnsubscribeToken(secret string, mailingListID gid.GID, recipientEmail mail.Addr) (string, error) {
|
||||||
|
return statelesstoken.NewToken(
|
||||||
|
secret,
|
||||||
|
TokenTypeUnsubscribe,
|
||||||
|
unsubscribeTokenExpiry,
|
||||||
|
UnsubscribeTokenData{
|
||||||
|
MailingListID: mailingListID,
|
||||||
|
Email: recipientEmail,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateUnsubscribeToken(secret, tokenString string) (*UnsubscribeTokenData, error) {
|
||||||
|
payload, err := statelesstoken.ValidateToken[UnsubscribeTokenData](secret, TokenTypeUnsubscribe, tokenString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot validate unsubscribe token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &payload.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfirmTokenData struct {
|
||||||
|
MailingListID gid.GID `json:"m"`
|
||||||
|
Email mail.Addr `json:"e"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfirmToken(secret string, mailingListID gid.GID, recipientEmail mail.Addr) (string, error) {
|
||||||
|
return statelesstoken.NewToken(
|
||||||
|
secret,
|
||||||
|
TokenTypeConfirm,
|
||||||
|
confirmTokenExpiry,
|
||||||
|
ConfirmTokenData{
|
||||||
|
MailingListID: mailingListID,
|
||||||
|
Email: recipientEmail,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateConfirmToken validates a subscription confirmation token and returns
|
||||||
|
// the embedded payload. Exported so the HTTP handler can use it.
|
||||||
|
func ValidateConfirmToken(secret, tokenString string) (*ConfirmTokenData, error) {
|
||||||
|
payload, err := statelesstoken.ValidateToken[ConfirmTokenData](secret, TokenTypeConfirm, tokenString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot validate confirm token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &payload.Data, nil
|
||||||
|
}
|
||||||
@@ -40,6 +40,13 @@ const (
|
|||||||
ActionTrustCenterAccessUpdate = "core:trust-center-access:update"
|
ActionTrustCenterAccessUpdate = "core:trust-center-access:update"
|
||||||
ActionTrustCenterAccessDelete = "core:trust-center-access:delete"
|
ActionTrustCenterAccessDelete = "core:trust-center-access:delete"
|
||||||
|
|
||||||
|
// MailingListUpdate actions
|
||||||
|
ActionMailingListUpdateList = "core:mailing-list-update:list"
|
||||||
|
ActionMailingListUpdateCreate = "core:mailing-list-update:create"
|
||||||
|
ActionMailingListUpdateUpdate = "core:mailing-list-update:update"
|
||||||
|
ActionMailingListUpdateSend = "core:mailing-list-update:send"
|
||||||
|
ActionMailingListUpdateDelete = "core:mailing-list-update:delete"
|
||||||
|
|
||||||
// MailingList actions
|
// MailingList actions
|
||||||
ActionMailingListUpdate = "core:mailing-list:update"
|
ActionMailingListUpdate = "core:mailing-list:update"
|
||||||
|
|
||||||
|
|||||||
@@ -664,6 +664,7 @@ func (s *DocumentService) SendSigningNotifications(
|
|||||||
subject,
|
subject,
|
||||||
textBody,
|
textBody,
|
||||||
htmlBody,
|
htmlBody,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := email.Insert(ctx, tx); err != nil {
|
if err := email.Insert(ctx, tx); err != nil {
|
||||||
@@ -2010,6 +2011,7 @@ func (s *DocumentService) SendExportEmail(
|
|||||||
subject,
|
subject,
|
||||||
textBody,
|
textBody,
|
||||||
htmlBody,
|
htmlBody,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := email.Insert(ctx, tx); err != nil {
|
if err := email.Insert(ctx, tx); err != nil {
|
||||||
|
|||||||
@@ -635,6 +635,7 @@ func (s FrameworkService) SendExportEmail(
|
|||||||
subject,
|
subject,
|
||||||
textBody,
|
textBody,
|
||||||
htmlBody,
|
htmlBody,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := email.Insert(ctx, tx); err != nil {
|
if err := email.Insert(ctx, tx); err != nil {
|
||||||
|
|||||||
@@ -374,6 +374,7 @@ func (s TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Con
|
|||||||
subject,
|
subject,
|
||||||
textBody,
|
textBody,
|
||||||
htmlBody,
|
htmlBody,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := accessEmail.Insert(ctx, tx); err != nil {
|
if err := accessEmail.Insert(ctx, tx); err != nil {
|
||||||
|
|||||||
@@ -436,6 +436,8 @@ func (impl *Implm) Run(
|
|||||||
l.Named("esign"),
|
l.Named("esign"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mailmanService := mailman.NewService(pgClient, fileManagerService, impl.cfg.Auth.Cookie.Secret, baseURL, impl.cfg.AWS.Bucket, encryptionKey, l)
|
||||||
|
|
||||||
proboService, err := probo.NewService(
|
proboService, err := probo.NewService(
|
||||||
ctx,
|
ctx,
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
@@ -472,8 +474,6 @@ func (impl *Implm) Run(
|
|||||||
slackService,
|
slackService,
|
||||||
)
|
)
|
||||||
|
|
||||||
mailmanService := mailman.NewService(pgClient)
|
|
||||||
|
|
||||||
serverHandler, err := server.NewServer(
|
serverHandler, err := server.NewServer(
|
||||||
server.Config{
|
server.Config{
|
||||||
AllowedOrigins: impl.cfg.Api.Cors.AllowedOrigins,
|
AllowedOrigins: impl.cfg.Api.Cors.AllowedOrigins,
|
||||||
@@ -593,6 +593,16 @@ func (impl *Implm) Run(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mailingListWorker := mailman.NewMailingListWorker(mailmanService, pgClient, l.Named("mailing-list-worker"))
|
||||||
|
mailingListWorkerCtx, stopMailingListWorker := context.WithCancel(context.Background())
|
||||||
|
wg.Go(
|
||||||
|
func() {
|
||||||
|
if err := mailingListWorker.Run(mailingListWorkerCtx); err != nil {
|
||||||
|
cancel(fmt.Errorf("mailing list worker crashed: %w", err))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
trustCenterServerCtx, stopTrustCenterServer := context.WithCancel(context.Background())
|
trustCenterServerCtx, stopTrustCenterServer := context.WithCancel(context.Background())
|
||||||
defer stopTrustCenterServer()
|
defer stopTrustCenterServer()
|
||||||
wg.Go(
|
wg.Go(
|
||||||
@@ -619,6 +629,7 @@ func (impl *Implm) Run(
|
|||||||
stopTrustCenterServer()
|
stopTrustCenterServer()
|
||||||
stopWebhookSender()
|
stopWebhookSender()
|
||||||
stopESignService()
|
stopESignService()
|
||||||
|
stopMailingListWorker()
|
||||||
stopExportJobExporter()
|
stopExportJobExporter()
|
||||||
stopIAMService()
|
stopIAMService()
|
||||||
stopMailer()
|
stopMailer()
|
||||||
|
|||||||
@@ -1695,6 +1695,13 @@ type MailingList implements Node {
|
|||||||
last: Int
|
last: Int
|
||||||
before: CursorKey
|
before: CursorKey
|
||||||
): MailingListSubscriberConnection! @goField(forceResolver: true)
|
): MailingListSubscriberConnection! @goField(forceResolver: true)
|
||||||
|
|
||||||
|
updates(
|
||||||
|
first: Int
|
||||||
|
after: CursorKey
|
||||||
|
last: Int
|
||||||
|
before: CursorKey
|
||||||
|
): MailingListUpdateConnection! @goField(forceResolver: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MailingListSubscriberStatus
|
enum MailingListSubscriberStatus
|
||||||
@@ -1734,6 +1741,51 @@ type MailingListSubscriberEdge {
|
|||||||
node: MailingListSubscriber!
|
node: MailingListSubscriber!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum MailingListUpdateStatus
|
||||||
|
@goModel(
|
||||||
|
model: "go.probo.inc/probo/pkg/coredata.MailingListUpdateStatus"
|
||||||
|
) {
|
||||||
|
DRAFT
|
||||||
|
@goEnum(
|
||||||
|
value: "go.probo.inc/probo/pkg/coredata.MailingListUpdateStatusDraft"
|
||||||
|
)
|
||||||
|
ENQUEUED
|
||||||
|
@goEnum(
|
||||||
|
value: "go.probo.inc/probo/pkg/coredata.MailingListUpdateStatusEnqueued"
|
||||||
|
)
|
||||||
|
PROCESSING
|
||||||
|
@goEnum(
|
||||||
|
value: "go.probo.inc/probo/pkg/coredata.MailingListUpdateStatusProcessing"
|
||||||
|
)
|
||||||
|
SENT
|
||||||
|
@goEnum(
|
||||||
|
value: "go.probo.inc/probo/pkg/coredata.MailingListUpdateStatusSent"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MailingListUpdate implements Node {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
body: String!
|
||||||
|
status: MailingListUpdateStatus!
|
||||||
|
createdAt: Datetime!
|
||||||
|
updatedAt: Datetime!
|
||||||
|
}
|
||||||
|
|
||||||
|
type MailingListUpdateConnection
|
||||||
|
@goModel(
|
||||||
|
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.MailingListUpdateConnection"
|
||||||
|
) {
|
||||||
|
edges: [MailingListUpdateEdge!]!
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
totalCount: Int! @goField(forceResolver: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MailingListUpdateEdge {
|
||||||
|
cursor: CursorKey!
|
||||||
|
node: MailingListUpdate!
|
||||||
|
}
|
||||||
|
|
||||||
type Organization implements Node {
|
type Organization implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
name: String!
|
name: String!
|
||||||
@@ -3407,6 +3459,19 @@ type Mutation {
|
|||||||
deleteTrustCenterAccess(
|
deleteTrustCenterAccess(
|
||||||
input: DeleteTrustCenterAccessInput!
|
input: DeleteTrustCenterAccessInput!
|
||||||
): DeleteTrustCenterAccessPayload!
|
): DeleteTrustCenterAccessPayload!
|
||||||
|
# Compliance News mutations
|
||||||
|
createMailingListUpdate(
|
||||||
|
input: CreateMailingListUpdateInput!
|
||||||
|
): CreateMailingListUpdatePayload!
|
||||||
|
updateMailingListUpdate(
|
||||||
|
input: UpdateMailingListUpdateInput!
|
||||||
|
): UpdateMailingListUpdatePayload!
|
||||||
|
sendMailingListUpdate(
|
||||||
|
input: SendMailingListUpdateInput!
|
||||||
|
): SendMailingListUpdatePayload!
|
||||||
|
deleteMailingListUpdate(
|
||||||
|
input: DeleteMailingListUpdateInput!
|
||||||
|
): DeleteMailingListUpdatePayload!
|
||||||
# Mailing List mutations
|
# Mailing List mutations
|
||||||
updateMailingList(
|
updateMailingList(
|
||||||
input: UpdateMailingListInput!
|
input: UpdateMailingListInput!
|
||||||
@@ -3829,6 +3894,26 @@ type UpdateMailingListPayload {
|
|||||||
mailingList: MailingList!
|
mailingList: MailingList!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input CreateMailingListUpdateInput {
|
||||||
|
mailingListId: ID!
|
||||||
|
title: String!
|
||||||
|
body: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateMailingListUpdateInput {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
body: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input SendMailingListUpdateInput {
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
input DeleteMailingListUpdateInput {
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
input CreateMailingListSubscriberInput {
|
input CreateMailingListSubscriberInput {
|
||||||
mailingListId: ID!
|
mailingListId: ID!
|
||||||
fullName: String!
|
fullName: String!
|
||||||
@@ -4690,6 +4775,22 @@ type DeleteTrustCenterAccessPayload {
|
|||||||
deletedTrustCenterAccessId: ID!
|
deletedTrustCenterAccessId: ID!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateMailingListUpdatePayload {
|
||||||
|
mailingListUpdate: MailingListUpdate!
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateMailingListUpdatePayload {
|
||||||
|
mailingListUpdate: MailingListUpdate!
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendMailingListUpdatePayload {
|
||||||
|
mailingListUpdate: MailingListUpdate!
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteMailingListUpdatePayload {
|
||||||
|
deletedMailingListUpdateId: ID!
|
||||||
|
}
|
||||||
|
|
||||||
type CreateMailingListSubscriberPayload {
|
type CreateMailingListSubscriberPayload {
|
||||||
mailingListSubscriberEdge: MailingListSubscriberEdge!
|
mailingListSubscriberEdge: MailingListSubscriberEdge!
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
66
pkg/server/api/console/v1/types/mailing_list_update.go
Normal file
66
pkg/server/api/console/v1/types/mailing_list_update.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// 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 MailingListUpdateConnection struct {
|
||||||
|
TotalCount int
|
||||||
|
Edges []*MailingListUpdateEdge
|
||||||
|
PageInfo *PageInfo
|
||||||
|
|
||||||
|
Resolver any
|
||||||
|
ParentID gid.GID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMailingListUpdate(mlu *coredata.MailingListUpdate) *MailingListUpdate {
|
||||||
|
return &MailingListUpdate{
|
||||||
|
ID: mlu.ID,
|
||||||
|
Title: mlu.Title,
|
||||||
|
Body: mlu.Body,
|
||||||
|
Status: mlu.Status,
|
||||||
|
CreatedAt: mlu.CreatedAt,
|
||||||
|
UpdatedAt: mlu.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMailingListUpdateEdge(mlu *coredata.MailingListUpdate, orderBy coredata.MailingListUpdateOrderField) *MailingListUpdateEdge {
|
||||||
|
return &MailingListUpdateEdge{
|
||||||
|
Cursor: mlu.CursorKey(orderBy),
|
||||||
|
Node: NewMailingListUpdate(mlu),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMailingListUpdateConnection(
|
||||||
|
p *page.Page[*coredata.MailingListUpdate, coredata.MailingListUpdateOrderField],
|
||||||
|
resolver any,
|
||||||
|
mailingListID gid.GID,
|
||||||
|
) *MailingListUpdateConnection {
|
||||||
|
edges := make([]*MailingListUpdateEdge, len(p.Data))
|
||||||
|
for i := range edges {
|
||||||
|
edges[i] = NewMailingListUpdateEdge(p.Data[i], p.Cursor.OrderBy.Field)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MailingListUpdateConnection{
|
||||||
|
Edges: edges,
|
||||||
|
PageInfo: NewPageInfo(p),
|
||||||
|
Resolver: resolver,
|
||||||
|
ParentID: mailingListID,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -446,6 +446,16 @@ type CreateMailingListSubscriberPayload struct {
|
|||||||
MailingListSubscriberEdge *MailingListSubscriberEdge `json:"mailingListSubscriberEdge"`
|
MailingListSubscriberEdge *MailingListSubscriberEdge `json:"mailingListSubscriberEdge"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateMailingListUpdateInput struct {
|
||||||
|
MailingListID gid.GID `json:"mailingListId"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateMailingListUpdatePayload struct {
|
||||||
|
MailingListUpdate *MailingListUpdate `json:"mailingListUpdate"`
|
||||||
|
}
|
||||||
|
|
||||||
type CreateMeasureInput struct {
|
type CreateMeasureInput struct {
|
||||||
OrganizationID gid.GID `json:"organizationId"`
|
OrganizationID gid.GID `json:"organizationId"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -977,6 +987,14 @@ type DeleteMailingListSubscriberPayload struct {
|
|||||||
DeletedMailingListSubscriberID gid.GID `json:"deletedMailingListSubscriberId"`
|
DeletedMailingListSubscriberID gid.GID `json:"deletedMailingListSubscriberId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeleteMailingListUpdateInput struct {
|
||||||
|
ID gid.GID `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteMailingListUpdatePayload struct {
|
||||||
|
DeletedMailingListUpdateID gid.GID `json:"deletedMailingListUpdateId"`
|
||||||
|
}
|
||||||
|
|
||||||
type DeleteMeasureInput struct {
|
type DeleteMeasureInput struct {
|
||||||
MeasureID gid.GID `json:"measureId"`
|
MeasureID gid.GID `json:"measureId"`
|
||||||
}
|
}
|
||||||
@@ -1453,6 +1471,7 @@ type MailingList struct {
|
|||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
ReplyTo *mail.Addr `json:"replyTo,omitempty"`
|
ReplyTo *mail.Addr `json:"replyTo,omitempty"`
|
||||||
Subscribers *MailingListSubscriberConnection `json:"subscribers"`
|
Subscribers *MailingListSubscriberConnection `json:"subscribers"`
|
||||||
|
Updates *MailingListUpdateConnection `json:"updates"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (MailingList) IsNode() {}
|
func (MailingList) IsNode() {}
|
||||||
@@ -1475,6 +1494,23 @@ type MailingListSubscriberEdge struct {
|
|||||||
Node *MailingListSubscriber `json:"node"`
|
Node *MailingListSubscriber `json:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MailingListUpdate struct {
|
||||||
|
ID gid.GID `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Status coredata.MailingListUpdateStatus `json:"status"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (MailingListUpdate) IsNode() {}
|
||||||
|
func (this MailingListUpdate) GetID() gid.GID { return this.ID }
|
||||||
|
|
||||||
|
type MailingListUpdateEdge struct {
|
||||||
|
Cursor page.CursorKey `json:"cursor"`
|
||||||
|
Node *MailingListUpdate `json:"node"`
|
||||||
|
}
|
||||||
|
|
||||||
type Measure struct {
|
type Measure struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
@@ -1815,6 +1851,14 @@ type RiskFilter struct {
|
|||||||
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
|
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SendMailingListUpdateInput struct {
|
||||||
|
ID gid.GID `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendMailingListUpdatePayload struct {
|
||||||
|
MailingListUpdate *MailingListUpdate `json:"mailingListUpdate"`
|
||||||
|
}
|
||||||
|
|
||||||
type SendSigningNotificationsInput struct {
|
type SendSigningNotificationsInput struct {
|
||||||
OrganizationID gid.GID `json:"organizationId"`
|
OrganizationID gid.GID `json:"organizationId"`
|
||||||
}
|
}
|
||||||
@@ -2181,6 +2225,16 @@ type UpdateMailingListPayload struct {
|
|||||||
MailingList *MailingList `json:"mailingList"`
|
MailingList *MailingList `json:"mailingList"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateMailingListUpdateInput struct {
|
||||||
|
ID gid.GID `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateMailingListUpdatePayload struct {
|
||||||
|
MailingListUpdate *MailingListUpdate `json:"mailingListUpdate"`
|
||||||
|
}
|
||||||
|
|
||||||
type UpdateMeasureInput struct {
|
type UpdateMeasureInput struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
Name *string `json:"name,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
"go.probo.inc/probo/pkg/gid"
|
||||||
"go.probo.inc/probo/pkg/iam"
|
"go.probo.inc/probo/pkg/iam"
|
||||||
|
"go.probo.inc/probo/pkg/mailman"
|
||||||
"go.probo.inc/probo/pkg/page"
|
"go.probo.inc/probo/pkg/page"
|
||||||
"go.probo.inc/probo/pkg/probo"
|
"go.probo.inc/probo/pkg/probo"
|
||||||
"go.probo.inc/probo/pkg/server/api/authn"
|
"go.probo.inc/probo/pkg/server/api/authn"
|
||||||
@@ -1546,6 +1547,28 @@ func (r *mailingListResolver) Subscribers(ctx context.Context, obj *types.Mailin
|
|||||||
return types.NewMailingListSubscriberConnection(result, r, obj.ID), nil
|
return types.NewMailingListSubscriberConnection(result, r, obj.ID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updates is the resolver for the updates field on MailingList.
|
||||||
|
func (r *mailingListResolver) Updates(ctx context.Context, obj *types.MailingList, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.MailingListUpdateConnection, error) {
|
||||||
|
if err := r.authorize(ctx, obj.ID, probo.ActionMailingListUpdateList); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pageOrderBy := page.OrderBy[coredata.MailingListUpdateOrderField]{
|
||||||
|
Field: coredata.MailingListUpdateOrderFieldUpdatedAt,
|
||||||
|
Direction: page.OrderDirectionDesc,
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||||
|
|
||||||
|
result, err := r.mailman.ListMailingListUpdates(ctx, obj.ID, cursor)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot list mailing list updates", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.NewMailingListUpdateConnection(result, r, obj.ID), nil
|
||||||
|
}
|
||||||
|
|
||||||
// TotalCount is the resolver for the totalCount field.
|
// TotalCount is the resolver for the totalCount field.
|
||||||
func (r *mailingListSubscriberConnectionResolver) TotalCount(ctx context.Context, obj *types.MailingListSubscriberConnection) (int, error) {
|
func (r *mailingListSubscriberConnectionResolver) TotalCount(ctx context.Context, obj *types.MailingListSubscriberConnection) (int, error) {
|
||||||
if err := r.authorize(ctx, obj.ParentID, probo.ActionMailingListSubscriberList); err != nil {
|
if err := r.authorize(ctx, obj.ParentID, probo.ActionMailingListSubscriberList); err != nil {
|
||||||
@@ -1565,6 +1588,21 @@ func (r *mailingListSubscriberConnectionResolver) TotalCount(ctx context.Context
|
|||||||
panic(fmt.Errorf("not implemented: TotalCount for parent type %T", obj.Resolver))
|
panic(fmt.Errorf("not implemented: TotalCount for parent type %T", obj.Resolver))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TotalCount is the resolver for the totalCount field on MailingListUpdateConnection.
|
||||||
|
func (r *mailingListUpdateConnectionResolver) TotalCount(ctx context.Context, obj *types.MailingListUpdateConnection) (int, error) {
|
||||||
|
if err := r.authorize(ctx, obj.ParentID, probo.ActionMailingListUpdateList); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := r.mailman.CountMailingListUpdates(ctx, obj.ParentID)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot count mailing list updates", log.Error(err))
|
||||||
|
return 0, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Evidences is the resolver for the evidences field.
|
// 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) {
|
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 {
|
if err := r.authorize(ctx, obj.ID, probo.ActionEvidenceList); err != nil {
|
||||||
@@ -2028,6 +2066,88 @@ func (r *mutationResolver) DeleteTrustCenterAccess(ctx context.Context, input ty
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateMailingListUpdate is the resolver for the createMailingListUpdate field.
|
||||||
|
func (r *mutationResolver) CreateMailingListUpdate(ctx context.Context, input types.CreateMailingListUpdateInput) (*types.CreateMailingListUpdatePayload, error) {
|
||||||
|
if err := r.authorize(ctx, input.MailingListID, probo.ActionMailingListUpdateCreate); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mlu, err := r.mailman.CreateMailingListUpdate(ctx, input.MailingListID, input.Title, input.Body)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot create mailing list update", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.CreateMailingListUpdatePayload{
|
||||||
|
MailingListUpdate: types.NewMailingListUpdate(mlu),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMailingListUpdate is the resolver for the updateMailingListUpdate field.
|
||||||
|
func (r *mutationResolver) UpdateMailingListUpdate(ctx context.Context, input types.UpdateMailingListUpdateInput) (*types.UpdateMailingListUpdatePayload, error) {
|
||||||
|
if err := r.authorize(ctx, input.ID, probo.ActionMailingListUpdateUpdate); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mlu, err := r.mailman.UpdateMailingListUpdate(ctx, input.ID, input.Title, input.Body)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, mailman.ErrMailingListUpdateAlreadySent) {
|
||||||
|
return nil, gqlutils.Conflictf(ctx, "mailing list update can only be edited when in draft")
|
||||||
|
}
|
||||||
|
if errors.Is(err, mailman.ErrMailingListUpdateNotFound) {
|
||||||
|
return nil, gqlutils.NotFoundf(ctx, "mailing list update not found")
|
||||||
|
}
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot update mailing list update", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.UpdateMailingListUpdatePayload{
|
||||||
|
MailingListUpdate: types.NewMailingListUpdate(mlu),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMailingListUpdate is the resolver for the sendMailingListUpdate field.
|
||||||
|
func (r *mutationResolver) SendMailingListUpdate(ctx context.Context, input types.SendMailingListUpdateInput) (*types.SendMailingListUpdatePayload, error) {
|
||||||
|
if err := r.authorize(ctx, input.ID, probo.ActionMailingListUpdateUpdate); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mlu, err := r.mailman.SendMailingListUpdate(ctx, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, mailman.ErrMailingListUpdateAlreadySent) {
|
||||||
|
return nil, gqlutils.Conflictf(ctx, "mailing list update has already been queued for sending")
|
||||||
|
}
|
||||||
|
if errors.Is(err, mailman.ErrMailingListUpdateNotFound) {
|
||||||
|
return nil, gqlutils.NotFoundf(ctx, "mailing list update not found")
|
||||||
|
}
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot queue mailing list update for sending", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.SendMailingListUpdatePayload{
|
||||||
|
MailingListUpdate: types.NewMailingListUpdate(mlu),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMailingListUpdate is the resolver for the deleteMailingListUpdate field.
|
||||||
|
func (r *mutationResolver) DeleteMailingListUpdate(ctx context.Context, input types.DeleteMailingListUpdateInput) (*types.DeleteMailingListUpdatePayload, error) {
|
||||||
|
if err := r.authorize(ctx, input.ID, probo.ActionMailingListUpdateDelete); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.mailman.DeleteMailingListUpdate(ctx, input.ID); err != nil {
|
||||||
|
if errors.Is(err, mailman.ErrMailingListUpdateNotFound) {
|
||||||
|
return nil, gqlutils.NotFoundf(ctx, "mailing list update not found")
|
||||||
|
}
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot delete mailing list update", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.DeleteMailingListUpdatePayload{
|
||||||
|
DeletedMailingListUpdateID: input.ID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateMailingList is the resolver for the updateMailingList field.
|
// UpdateMailingList is the resolver for the updateMailingList field.
|
||||||
func (r *mutationResolver) UpdateMailingList(ctx context.Context, input types.UpdateMailingListInput) (*types.UpdateMailingListPayload, error) {
|
func (r *mutationResolver) UpdateMailingList(ctx context.Context, input types.UpdateMailingListInput) (*types.UpdateMailingListPayload, error) {
|
||||||
if err := r.authorize(ctx, input.ID, probo.ActionMailingListUpdate); err != nil {
|
if err := r.authorize(ctx, input.ID, probo.ActionMailingListUpdate); err != nil {
|
||||||
@@ -2051,8 +2171,16 @@ func (r *mutationResolver) CreateMailingListSubscriber(ctx context.Context, inpu
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriber, err := r.mailman.CreateSubscriber(ctx, input.MailingListID, input.Email, input.FullName)
|
subscriber, err := r.mailman.CreateSubscriber(
|
||||||
|
ctx,
|
||||||
|
input.MailingListID,
|
||||||
|
input.Email,
|
||||||
|
input.FullName,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, mailman.ErrSubscriberAlreadyExist) {
|
||||||
|
return nil, gqlutils.Conflictf(ctx, "subscriber already exists in this mailing list")
|
||||||
|
}
|
||||||
r.logger.ErrorCtx(ctx, "cannot create mailing list subscriber", log.Error(err))
|
r.logger.ErrorCtx(ctx, "cannot create mailing list subscriber", log.Error(err))
|
||||||
return nil, gqlutils.Internal(ctx)
|
return nil, gqlutils.Internal(ctx)
|
||||||
}
|
}
|
||||||
@@ -2069,6 +2197,9 @@ func (r *mutationResolver) DeleteMailingListSubscriber(ctx context.Context, inpu
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := r.mailman.DeleteSubscriber(ctx, input.ID); err != nil {
|
if err := r.mailman.DeleteSubscriber(ctx, input.ID); err != nil {
|
||||||
|
if errors.Is(err, mailman.ErrSubscriberNotFound) {
|
||||||
|
return nil, gqlutils.NotFoundf(ctx, "mailing list subscriber not found")
|
||||||
|
}
|
||||||
r.logger.ErrorCtx(ctx, "cannot delete mailing list subscriber", log.Error(err))
|
r.logger.ErrorCtx(ctx, "cannot delete mailing list subscriber", log.Error(err))
|
||||||
return nil, gqlutils.Internal(ctx)
|
return nil, gqlutils.Internal(ctx)
|
||||||
}
|
}
|
||||||
@@ -9196,6 +9327,11 @@ func (r *Resolver) MailingListSubscriberConnection() schema.MailingListSubscribe
|
|||||||
return &mailingListSubscriberConnectionResolver{r}
|
return &mailingListSubscriberConnectionResolver{r}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MailingListUpdateConnection returns schema.MailingListUpdateConnectionResolver implementation.
|
||||||
|
func (r *Resolver) MailingListUpdateConnection() schema.MailingListUpdateConnectionResolver {
|
||||||
|
return &mailingListUpdateConnectionResolver{r}
|
||||||
|
}
|
||||||
|
|
||||||
// Measure returns schema.MeasureResolver implementation.
|
// Measure returns schema.MeasureResolver implementation.
|
||||||
func (r *Resolver) Measure() schema.MeasureResolver { return &measureResolver{r} }
|
func (r *Resolver) Measure() schema.MeasureResolver { return &measureResolver{r} }
|
||||||
|
|
||||||
@@ -9432,6 +9568,7 @@ type frameworkResolver struct{ *Resolver }
|
|||||||
type frameworkConnectionResolver struct{ *Resolver }
|
type frameworkConnectionResolver struct{ *Resolver }
|
||||||
type mailingListResolver struct{ *Resolver }
|
type mailingListResolver struct{ *Resolver }
|
||||||
type mailingListSubscriberConnectionResolver struct{ *Resolver }
|
type mailingListSubscriberConnectionResolver struct{ *Resolver }
|
||||||
|
type mailingListUpdateConnectionResolver struct{ *Resolver }
|
||||||
type measureResolver struct{ *Resolver }
|
type measureResolver struct{ *Resolver }
|
||||||
type measureConnectionResolver struct{ *Resolver }
|
type measureConnectionResolver struct{ *Resolver }
|
||||||
type meetingResolver struct{ *Resolver }
|
type meetingResolver struct{ *Resolver }
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ func NewMux(
|
|||||||
r.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig))
|
r.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig))
|
||||||
|
|
||||||
graphqlHandler := NewGraphQLHandler(iamSvc, trustSvc, esignSvc, mailmanSvc, logger, baseURL, cookieConfig, tokenSecret)
|
graphqlHandler := NewGraphQLHandler(iamSvc, trustSvc, esignSvc, mailmanSvc, logger, baseURL, cookieConfig, tokenSecret)
|
||||||
|
|
||||||
r.Handle("/graphql", graphqlHandler)
|
r.Handle("/graphql", graphqlHandler)
|
||||||
|
|
||||||
return r
|
return r
|
||||||
@@ -99,4 +98,3 @@ func NewMux(
|
|||||||
func (r *Resolver) TrustService(ctx context.Context, tenantID gid.TenantID) *trust.TenantService {
|
func (r *Resolver) TrustService(ctx context.Context, tenantID gid.TenantID) *trust.TenantService {
|
||||||
return r.trust.WithTenant(tenantID)
|
return r.trust.WithTenant(tenantID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,23 @@ type ComplianceFrameworkEdge
|
|||||||
node: ComplianceFramework!
|
node: ComplianceFramework!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MailingListUpdate implements Node @nda {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
body: String!
|
||||||
|
updatedAt: Datetime!
|
||||||
|
}
|
||||||
|
|
||||||
|
type MailingListUpdateConnection @nda {
|
||||||
|
edges: [MailingListUpdateEdge!]!
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
}
|
||||||
|
|
||||||
|
type MailingListUpdateEdge @nda {
|
||||||
|
cursor: CursorKey!
|
||||||
|
node: MailingListUpdate!
|
||||||
|
}
|
||||||
|
|
||||||
enum CountryCode
|
enum CountryCode
|
||||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.CountryCode") {
|
@goModel(model: "go.probo.inc/probo/pkg/coredata.CountryCode") {
|
||||||
AD @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAD")
|
AD @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAD")
|
||||||
@@ -581,6 +598,13 @@ type TrustCenter implements Node {
|
|||||||
last: Int
|
last: Int
|
||||||
before: CursorKey
|
before: CursorKey
|
||||||
): ComplianceExternalURLConnection! @goField(forceResolver: true)
|
): ComplianceExternalURLConnection! @goField(forceResolver: true)
|
||||||
|
|
||||||
|
updates(
|
||||||
|
first: Int
|
||||||
|
after: CursorKey
|
||||||
|
last: Int
|
||||||
|
before: CursorKey
|
||||||
|
): MailingListUpdateConnection! @goField(forceResolver: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ComplianceExternalURL implements Node {
|
type ComplianceExternalURL implements Node {
|
||||||
@@ -926,6 +950,7 @@ type MailingListSubscriber implements Node
|
|||||||
model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.MailingListSubscriber"
|
model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.MailingListSubscriber"
|
||||||
) {
|
) {
|
||||||
id: ID!
|
id: ID!
|
||||||
|
fullName: String!
|
||||||
email: EmailAddr!
|
email: EmailAddr!
|
||||||
status: MailingListSubscriberStatus!
|
status: MailingListSubscriberStatus!
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
|
|||||||
@@ -175,11 +175,29 @@ type ComplexityRoot struct {
|
|||||||
MailingListSubscriber struct {
|
MailingListSubscriber struct {
|
||||||
CreatedAt func(childComplexity int) int
|
CreatedAt func(childComplexity int) int
|
||||||
Email func(childComplexity int) int
|
Email func(childComplexity int) int
|
||||||
|
FullName func(childComplexity int) int
|
||||||
ID func(childComplexity int) int
|
ID func(childComplexity int) int
|
||||||
Status func(childComplexity int) int
|
Status func(childComplexity int) int
|
||||||
UpdatedAt func(childComplexity int) int
|
UpdatedAt func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MailingListUpdate struct {
|
||||||
|
Body func(childComplexity int) int
|
||||||
|
ID func(childComplexity int) int
|
||||||
|
Title func(childComplexity int) int
|
||||||
|
UpdatedAt func(childComplexity int) int
|
||||||
|
}
|
||||||
|
|
||||||
|
MailingListUpdateConnection struct {
|
||||||
|
Edges func(childComplexity int) int
|
||||||
|
PageInfo func(childComplexity int) int
|
||||||
|
}
|
||||||
|
|
||||||
|
MailingListUpdateEdge struct {
|
||||||
|
Cursor func(childComplexity int) int
|
||||||
|
Node func(childComplexity int) int
|
||||||
|
}
|
||||||
|
|
||||||
Mutation struct {
|
Mutation struct {
|
||||||
AcceptElectronicSignature func(childComplexity int, input types.AcceptElectronicSignatureInput) int
|
AcceptElectronicSignature func(childComplexity int, input types.AcceptElectronicSignatureInput) int
|
||||||
ExportDocumentPDF func(childComplexity int, input types.ExportDocumentPDFInput) int
|
ExportDocumentPDF func(childComplexity int, input types.ExportDocumentPDFInput) int
|
||||||
@@ -275,6 +293,7 @@ type ComplexityRoot struct {
|
|||||||
References func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
|
References func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
|
||||||
Slug func(childComplexity int) int
|
Slug func(childComplexity int) int
|
||||||
TrustCenterFiles func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
|
TrustCenterFiles func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
|
||||||
|
Updates 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
|
Vendors func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
|
||||||
ViewerSubscription func(childComplexity int) int
|
ViewerSubscription func(childComplexity int) int
|
||||||
}
|
}
|
||||||
@@ -417,6 +436,7 @@ type TrustCenterResolver interface {
|
|||||||
TrustCenterFiles(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TrustCenterFileConnection, error)
|
TrustCenterFiles(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TrustCenterFileConnection, error)
|
||||||
ComplianceFrameworks(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.ComplianceFrameworkConnection, error)
|
ComplianceFrameworks(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.ComplianceFrameworkConnection, error)
|
||||||
ExternalUrls(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.ComplianceExternalURLConnection, error)
|
ExternalUrls(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.ComplianceExternalURLConnection, error)
|
||||||
|
Updates(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.MailingListUpdateConnection, error)
|
||||||
}
|
}
|
||||||
type TrustCenterFileResolver interface {
|
type TrustCenterFileResolver interface {
|
||||||
IsUserAuthorized(ctx context.Context, obj *types.TrustCenterFile) (bool, error)
|
IsUserAuthorized(ctx context.Context, obj *types.TrustCenterFile) (bool, error)
|
||||||
@@ -805,6 +825,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
return e.ComplexityRoot.MailingListSubscriber.Email(childComplexity), true
|
return e.ComplexityRoot.MailingListSubscriber.Email(childComplexity), true
|
||||||
|
case "MailingListSubscriber.fullName":
|
||||||
|
if e.ComplexityRoot.MailingListSubscriber.FullName == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.ComplexityRoot.MailingListSubscriber.FullName(childComplexity), true
|
||||||
case "MailingListSubscriber.id":
|
case "MailingListSubscriber.id":
|
||||||
if e.ComplexityRoot.MailingListSubscriber.ID == nil {
|
if e.ComplexityRoot.MailingListSubscriber.ID == nil {
|
||||||
break
|
break
|
||||||
@@ -824,6 +850,57 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
|
|
||||||
return e.ComplexityRoot.MailingListSubscriber.UpdatedAt(childComplexity), true
|
return e.ComplexityRoot.MailingListSubscriber.UpdatedAt(childComplexity), true
|
||||||
|
|
||||||
|
case "MailingListUpdate.body":
|
||||||
|
if e.ComplexityRoot.MailingListUpdate.Body == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.ComplexityRoot.MailingListUpdate.Body(childComplexity), true
|
||||||
|
case "MailingListUpdate.id":
|
||||||
|
if e.ComplexityRoot.MailingListUpdate.ID == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.ComplexityRoot.MailingListUpdate.ID(childComplexity), true
|
||||||
|
case "MailingListUpdate.title":
|
||||||
|
if e.ComplexityRoot.MailingListUpdate.Title == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.ComplexityRoot.MailingListUpdate.Title(childComplexity), true
|
||||||
|
case "MailingListUpdate.updatedAt":
|
||||||
|
if e.ComplexityRoot.MailingListUpdate.UpdatedAt == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.ComplexityRoot.MailingListUpdate.UpdatedAt(childComplexity), true
|
||||||
|
|
||||||
|
case "MailingListUpdateConnection.edges":
|
||||||
|
if e.ComplexityRoot.MailingListUpdateConnection.Edges == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.ComplexityRoot.MailingListUpdateConnection.Edges(childComplexity), true
|
||||||
|
case "MailingListUpdateConnection.pageInfo":
|
||||||
|
if e.ComplexityRoot.MailingListUpdateConnection.PageInfo == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.ComplexityRoot.MailingListUpdateConnection.PageInfo(childComplexity), true
|
||||||
|
|
||||||
|
case "MailingListUpdateEdge.cursor":
|
||||||
|
if e.ComplexityRoot.MailingListUpdateEdge.Cursor == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.ComplexityRoot.MailingListUpdateEdge.Cursor(childComplexity), true
|
||||||
|
case "MailingListUpdateEdge.node":
|
||||||
|
if e.ComplexityRoot.MailingListUpdateEdge.Node == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.ComplexityRoot.MailingListUpdateEdge.Node(childComplexity), true
|
||||||
|
|
||||||
case "Mutation.acceptElectronicSignature":
|
case "Mutation.acceptElectronicSignature":
|
||||||
if e.ComplexityRoot.Mutation.AcceptElectronicSignature == nil {
|
if e.ComplexityRoot.Mutation.AcceptElectronicSignature == nil {
|
||||||
break
|
break
|
||||||
@@ -1258,6 +1335,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
return e.ComplexityRoot.TrustCenter.TrustCenterFiles(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey)), true
|
return e.ComplexityRoot.TrustCenter.TrustCenterFiles(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey)), true
|
||||||
|
case "TrustCenter.updates":
|
||||||
|
if e.ComplexityRoot.TrustCenter.Updates == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
args, err := ec.field_TrustCenter_updates_args(ctx, rawArgs)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.ComplexityRoot.TrustCenter.Updates(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey)), true
|
||||||
case "TrustCenter.vendors":
|
case "TrustCenter.vendors":
|
||||||
if e.ComplexityRoot.TrustCenter.Vendors == nil {
|
if e.ComplexityRoot.TrustCenter.Vendors == nil {
|
||||||
break
|
break
|
||||||
@@ -1744,6 +1832,23 @@ type ComplianceFrameworkEdge
|
|||||||
node: ComplianceFramework!
|
node: ComplianceFramework!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MailingListUpdate implements Node @nda {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
body: String!
|
||||||
|
updatedAt: Datetime!
|
||||||
|
}
|
||||||
|
|
||||||
|
type MailingListUpdateConnection @nda {
|
||||||
|
edges: [MailingListUpdateEdge!]!
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
}
|
||||||
|
|
||||||
|
type MailingListUpdateEdge @nda {
|
||||||
|
cursor: CursorKey!
|
||||||
|
node: MailingListUpdate!
|
||||||
|
}
|
||||||
|
|
||||||
enum CountryCode
|
enum CountryCode
|
||||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.CountryCode") {
|
@goModel(model: "go.probo.inc/probo/pkg/coredata.CountryCode") {
|
||||||
AD @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAD")
|
AD @goEnum(value: "go.probo.inc/probo/pkg/coredata.CountryCodeAD")
|
||||||
@@ -2194,6 +2299,13 @@ type TrustCenter implements Node {
|
|||||||
last: Int
|
last: Int
|
||||||
before: CursorKey
|
before: CursorKey
|
||||||
): ComplianceExternalURLConnection! @goField(forceResolver: true)
|
): ComplianceExternalURLConnection! @goField(forceResolver: true)
|
||||||
|
|
||||||
|
updates(
|
||||||
|
first: Int
|
||||||
|
after: CursorKey
|
||||||
|
last: Int
|
||||||
|
before: CursorKey
|
||||||
|
): MailingListUpdateConnection! @goField(forceResolver: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ComplianceExternalURL implements Node {
|
type ComplianceExternalURL implements Node {
|
||||||
@@ -2539,6 +2651,7 @@ type MailingListSubscriber implements Node
|
|||||||
model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.MailingListSubscriber"
|
model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.MailingListSubscriber"
|
||||||
) {
|
) {
|
||||||
id: ID!
|
id: ID!
|
||||||
|
fullName: String!
|
||||||
email: EmailAddr!
|
email: EmailAddr!
|
||||||
status: MailingListSubscriberStatus!
|
status: MailingListSubscriberStatus!
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
@@ -2901,6 +3014,32 @@ func (ec *executionContext) field_TrustCenter_trustCenterFiles_args(ctx context.
|
|||||||
return args, nil
|
return args, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) field_TrustCenter_updates_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
||||||
|
var err error
|
||||||
|
args := map[string]any{}
|
||||||
|
arg0, err := graphql.ProcessArgField(ctx, rawArgs, "first", ec.unmarshalOInt2ᚖint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args["first"] = arg0
|
||||||
|
arg1, err := graphql.ProcessArgField(ctx, rawArgs, "after", ec.unmarshalOCursorKey2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋpageᚐCursorKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args["after"] = arg1
|
||||||
|
arg2, err := graphql.ProcessArgField(ctx, rawArgs, "last", ec.unmarshalOInt2ᚖint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args["last"] = arg2
|
||||||
|
arg3, err := graphql.ProcessArgField(ctx, rawArgs, "before", ec.unmarshalOCursorKey2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋpageᚐCursorKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args["before"] = arg3
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) field_TrustCenter_vendors_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
func (ec *executionContext) field_TrustCenter_vendors_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
|
||||||
var err error
|
var err error
|
||||||
args := map[string]any{}
|
args := map[string]any{}
|
||||||
@@ -4850,6 +4989,35 @@ func (ec *executionContext) fieldContext_MailingListSubscriber_id(_ context.Cont
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _MailingListSubscriber_fullName(ctx context.Context, field graphql.CollectedField, obj *types.MailingListSubscriber) (ret graphql.Marshaler) {
|
||||||
|
return graphql.ResolveField(
|
||||||
|
ctx,
|
||||||
|
ec.OperationContext,
|
||||||
|
field,
|
||||||
|
ec.fieldContext_MailingListSubscriber_fullName,
|
||||||
|
func(ctx context.Context) (any, error) {
|
||||||
|
return obj.FullName, nil
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
ec.marshalNString2string,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_MailingListSubscriber_fullName(_ 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 String 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) {
|
func (ec *executionContext) _MailingListSubscriber_email(ctx context.Context, field graphql.CollectedField, obj *types.MailingListSubscriber) (ret graphql.Marshaler) {
|
||||||
return graphql.ResolveField(
|
return graphql.ResolveField(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -4966,6 +5134,290 @@ func (ec *executionContext) fieldContext_MailingListSubscriber_updatedAt(_ conte
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _MailingListUpdate_id(ctx context.Context, field graphql.CollectedField, obj *types.MailingListUpdate) (ret graphql.Marshaler) {
|
||||||
|
return graphql.ResolveField(
|
||||||
|
ctx,
|
||||||
|
ec.OperationContext,
|
||||||
|
field,
|
||||||
|
ec.fieldContext_MailingListUpdate_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_MailingListUpdate_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "MailingListUpdate",
|
||||||
|
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) _MailingListUpdate_title(ctx context.Context, field graphql.CollectedField, obj *types.MailingListUpdate) (ret graphql.Marshaler) {
|
||||||
|
return graphql.ResolveField(
|
||||||
|
ctx,
|
||||||
|
ec.OperationContext,
|
||||||
|
field,
|
||||||
|
ec.fieldContext_MailingListUpdate_title,
|
||||||
|
func(ctx context.Context) (any, error) {
|
||||||
|
return obj.Title, nil
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
ec.marshalNString2string,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_MailingListUpdate_title(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "MailingListUpdate",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type String does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _MailingListUpdate_body(ctx context.Context, field graphql.CollectedField, obj *types.MailingListUpdate) (ret graphql.Marshaler) {
|
||||||
|
return graphql.ResolveField(
|
||||||
|
ctx,
|
||||||
|
ec.OperationContext,
|
||||||
|
field,
|
||||||
|
ec.fieldContext_MailingListUpdate_body,
|
||||||
|
func(ctx context.Context) (any, error) {
|
||||||
|
return obj.Body, nil
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
ec.marshalNString2string,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_MailingListUpdate_body(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "MailingListUpdate",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type String does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _MailingListUpdate_updatedAt(ctx context.Context, field graphql.CollectedField, obj *types.MailingListUpdate) (ret graphql.Marshaler) {
|
||||||
|
return graphql.ResolveField(
|
||||||
|
ctx,
|
||||||
|
ec.OperationContext,
|
||||||
|
field,
|
||||||
|
ec.fieldContext_MailingListUpdate_updatedAt,
|
||||||
|
func(ctx context.Context) (any, error) {
|
||||||
|
return obj.UpdatedAt, nil
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
ec.marshalNDatetime2timeᚐTime,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_MailingListUpdate_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "MailingListUpdate",
|
||||||
|
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) _MailingListUpdateConnection_edges(ctx context.Context, field graphql.CollectedField, obj *types.MailingListUpdateConnection) (ret graphql.Marshaler) {
|
||||||
|
return graphql.ResolveField(
|
||||||
|
ctx,
|
||||||
|
ec.OperationContext,
|
||||||
|
field,
|
||||||
|
ec.fieldContext_MailingListUpdateConnection_edges,
|
||||||
|
func(ctx context.Context) (any, error) {
|
||||||
|
return obj.Edges, nil
|
||||||
|
},
|
||||||
|
func(ctx context.Context, next graphql.Resolver) graphql.Resolver {
|
||||||
|
directive0 := next
|
||||||
|
|
||||||
|
directive1 := func(ctx context.Context) (any, error) {
|
||||||
|
if ec.Directives.Nda == nil {
|
||||||
|
var zeroVal []*types.MailingListUpdateEdge
|
||||||
|
return zeroVal, errors.New("directive nda is not implemented")
|
||||||
|
}
|
||||||
|
return ec.Directives.Nda(ctx, obj, directive0)
|
||||||
|
}
|
||||||
|
|
||||||
|
next = directive1
|
||||||
|
return next
|
||||||
|
},
|
||||||
|
ec.marshalNMailingListUpdateEdge2ᚕᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐMailingListUpdateEdgeᚄ,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_MailingListUpdateConnection_edges(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "MailingListUpdateConnection",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
switch field.Name {
|
||||||
|
case "cursor":
|
||||||
|
return ec.fieldContext_MailingListUpdateEdge_cursor(ctx, field)
|
||||||
|
case "node":
|
||||||
|
return ec.fieldContext_MailingListUpdateEdge_node(ctx, field)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no field named %q was found under type MailingListUpdateEdge", field.Name)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _MailingListUpdateConnection_pageInfo(ctx context.Context, field graphql.CollectedField, obj *types.MailingListUpdateConnection) (ret graphql.Marshaler) {
|
||||||
|
return graphql.ResolveField(
|
||||||
|
ctx,
|
||||||
|
ec.OperationContext,
|
||||||
|
field,
|
||||||
|
ec.fieldContext_MailingListUpdateConnection_pageInfo,
|
||||||
|
func(ctx context.Context) (any, error) {
|
||||||
|
return obj.PageInfo, nil
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
ec.marshalNPageInfo2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐPageInfo,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_MailingListUpdateConnection_pageInfo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "MailingListUpdateConnection",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
switch field.Name {
|
||||||
|
case "hasNextPage":
|
||||||
|
return ec.fieldContext_PageInfo_hasNextPage(ctx, field)
|
||||||
|
case "hasPreviousPage":
|
||||||
|
return ec.fieldContext_PageInfo_hasPreviousPage(ctx, field)
|
||||||
|
case "startCursor":
|
||||||
|
return ec.fieldContext_PageInfo_startCursor(ctx, field)
|
||||||
|
case "endCursor":
|
||||||
|
return ec.fieldContext_PageInfo_endCursor(ctx, field)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no field named %q was found under type PageInfo", field.Name)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _MailingListUpdateEdge_cursor(ctx context.Context, field graphql.CollectedField, obj *types.MailingListUpdateEdge) (ret graphql.Marshaler) {
|
||||||
|
return graphql.ResolveField(
|
||||||
|
ctx,
|
||||||
|
ec.OperationContext,
|
||||||
|
field,
|
||||||
|
ec.fieldContext_MailingListUpdateEdge_cursor,
|
||||||
|
func(ctx context.Context) (any, error) {
|
||||||
|
return obj.Cursor, nil
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
ec.marshalNCursorKey2goᚗproboᚗincᚋproboᚋpkgᚋpageᚐCursorKey,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_MailingListUpdateEdge_cursor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "MailingListUpdateEdge",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type CursorKey does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _MailingListUpdateEdge_node(ctx context.Context, field graphql.CollectedField, obj *types.MailingListUpdateEdge) (ret graphql.Marshaler) {
|
||||||
|
return graphql.ResolveField(
|
||||||
|
ctx,
|
||||||
|
ec.OperationContext,
|
||||||
|
field,
|
||||||
|
ec.fieldContext_MailingListUpdateEdge_node,
|
||||||
|
func(ctx context.Context) (any, error) {
|
||||||
|
return obj.Node, nil
|
||||||
|
},
|
||||||
|
func(ctx context.Context, next graphql.Resolver) graphql.Resolver {
|
||||||
|
directive0 := next
|
||||||
|
|
||||||
|
directive1 := func(ctx context.Context) (any, error) {
|
||||||
|
if ec.Directives.Nda == nil {
|
||||||
|
var zeroVal *types.MailingListUpdate
|
||||||
|
return zeroVal, errors.New("directive nda is not implemented")
|
||||||
|
}
|
||||||
|
return ec.Directives.Nda(ctx, obj, directive0)
|
||||||
|
}
|
||||||
|
|
||||||
|
next = directive1
|
||||||
|
return next
|
||||||
|
},
|
||||||
|
ec.marshalNMailingListUpdate2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐMailingListUpdate,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_MailingListUpdateEdge_node(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "MailingListUpdateEdge",
|
||||||
|
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_MailingListUpdate_id(ctx, field)
|
||||||
|
case "title":
|
||||||
|
return ec.fieldContext_MailingListUpdate_title(ctx, field)
|
||||||
|
case "body":
|
||||||
|
return ec.fieldContext_MailingListUpdate_body(ctx, field)
|
||||||
|
case "updatedAt":
|
||||||
|
return ec.fieldContext_MailingListUpdate_updatedAt(ctx, field)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no field named %q was found under type MailingListUpdate", field.Name)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Mutation_sendMagicLink(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Mutation_sendMagicLink(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
return graphql.ResolveField(
|
return graphql.ResolveField(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -6423,6 +6875,8 @@ func (ec *executionContext) fieldContext_Query_currentTrustCenter(_ context.Cont
|
|||||||
return ec.fieldContext_TrustCenter_complianceFrameworks(ctx, field)
|
return ec.fieldContext_TrustCenter_complianceFrameworks(ctx, field)
|
||||||
case "externalUrls":
|
case "externalUrls":
|
||||||
return ec.fieldContext_TrustCenter_externalUrls(ctx, field)
|
return ec.fieldContext_TrustCenter_externalUrls(ctx, field)
|
||||||
|
case "updates":
|
||||||
|
return ec.fieldContext_TrustCenter_updates(ctx, field)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("no field named %q was found under type TrustCenter", field.Name)
|
return nil, fmt.Errorf("no field named %q was found under type TrustCenter", field.Name)
|
||||||
},
|
},
|
||||||
@@ -6945,6 +7399,8 @@ func (ec *executionContext) fieldContext_SubscribeToMailingListPayload_subscript
|
|||||||
switch field.Name {
|
switch field.Name {
|
||||||
case "id":
|
case "id":
|
||||||
return ec.fieldContext_MailingListSubscriber_id(ctx, field)
|
return ec.fieldContext_MailingListSubscriber_id(ctx, field)
|
||||||
|
case "fullName":
|
||||||
|
return ec.fieldContext_MailingListSubscriber_fullName(ctx, field)
|
||||||
case "email":
|
case "email":
|
||||||
return ec.fieldContext_MailingListSubscriber_email(ctx, field)
|
return ec.fieldContext_MailingListSubscriber_email(ctx, field)
|
||||||
case "status":
|
case "status":
|
||||||
@@ -7168,6 +7624,8 @@ func (ec *executionContext) fieldContext_TrustCenter_viewerSubscription(_ contex
|
|||||||
switch field.Name {
|
switch field.Name {
|
||||||
case "id":
|
case "id":
|
||||||
return ec.fieldContext_MailingListSubscriber_id(ctx, field)
|
return ec.fieldContext_MailingListSubscriber_id(ctx, field)
|
||||||
|
case "fullName":
|
||||||
|
return ec.fieldContext_MailingListSubscriber_fullName(ctx, field)
|
||||||
case "email":
|
case "email":
|
||||||
return ec.fieldContext_MailingListSubscriber_email(ctx, field)
|
return ec.fieldContext_MailingListSubscriber_email(ctx, field)
|
||||||
case "status":
|
case "status":
|
||||||
@@ -7624,6 +8082,66 @@ func (ec *executionContext) fieldContext_TrustCenter_externalUrls(ctx context.Co
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _TrustCenter_updates(ctx context.Context, field graphql.CollectedField, obj *types.TrustCenter) (ret graphql.Marshaler) {
|
||||||
|
return graphql.ResolveField(
|
||||||
|
ctx,
|
||||||
|
ec.OperationContext,
|
||||||
|
field,
|
||||||
|
ec.fieldContext_TrustCenter_updates,
|
||||||
|
func(ctx context.Context) (any, error) {
|
||||||
|
fc := graphql.GetFieldContext(ctx)
|
||||||
|
return ec.Resolvers.TrustCenter().Updates(ctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey))
|
||||||
|
},
|
||||||
|
func(ctx context.Context, next graphql.Resolver) graphql.Resolver {
|
||||||
|
directive0 := next
|
||||||
|
|
||||||
|
directive1 := func(ctx context.Context) (any, error) {
|
||||||
|
if ec.Directives.Nda == nil {
|
||||||
|
var zeroVal *types.MailingListUpdateConnection
|
||||||
|
return zeroVal, errors.New("directive nda is not implemented")
|
||||||
|
}
|
||||||
|
return ec.Directives.Nda(ctx, obj, directive0)
|
||||||
|
}
|
||||||
|
|
||||||
|
next = directive1
|
||||||
|
return next
|
||||||
|
},
|
||||||
|
ec.marshalNMailingListUpdateConnection2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐMailingListUpdateConnection,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_TrustCenter_updates(ctx 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 "edges":
|
||||||
|
return ec.fieldContext_MailingListUpdateConnection_edges(ctx, field)
|
||||||
|
case "pageInfo":
|
||||||
|
return ec.fieldContext_MailingListUpdateConnection_pageInfo(ctx, field)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no field named %q was found under type MailingListUpdateConnection", field.Name)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = ec.Recover(ctx, r)
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
if fc.Args, err = ec.field_TrustCenter_updates_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return fc, err
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _TrustCenterAccess_id(ctx context.Context, field graphql.CollectedField, obj *types.TrustCenterAccess) (ret graphql.Marshaler) {
|
func (ec *executionContext) _TrustCenterAccess_id(ctx context.Context, field graphql.CollectedField, obj *types.TrustCenterAccess) (ret graphql.Marshaler) {
|
||||||
return graphql.ResolveField(
|
return graphql.ResolveField(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -10701,6 +11219,13 @@ func (ec *executionContext) _Node(ctx context.Context, sel ast.SelectionSet, obj
|
|||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
return ec._Organization(ctx, sel, obj)
|
return ec._Organization(ctx, sel, obj)
|
||||||
|
case types.MailingListUpdate:
|
||||||
|
return ec._MailingListUpdate(ctx, sel, &obj)
|
||||||
|
case *types.MailingListUpdate:
|
||||||
|
if obj == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
return ec._MailingListUpdate(ctx, sel, obj)
|
||||||
case types.MailingListSubscriber:
|
case types.MailingListSubscriber:
|
||||||
return ec._MailingListSubscriber(ctx, sel, &obj)
|
return ec._MailingListSubscriber(ctx, sel, &obj)
|
||||||
case *types.MailingListSubscriber:
|
case *types.MailingListSubscriber:
|
||||||
@@ -11944,6 +12469,11 @@ func (ec *executionContext) _MailingListSubscriber(ctx context.Context, sel ast.
|
|||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
out.Invalids++
|
out.Invalids++
|
||||||
}
|
}
|
||||||
|
case "fullName":
|
||||||
|
out.Values[i] = ec._MailingListSubscriber_fullName(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
out.Invalids++
|
||||||
|
}
|
||||||
case "email":
|
case "email":
|
||||||
out.Values[i] = ec._MailingListSubscriber_email(ctx, field, obj)
|
out.Values[i] = ec._MailingListSubscriber_email(ctx, field, obj)
|
||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
@@ -11987,6 +12517,148 @@ func (ec *executionContext) _MailingListSubscriber(ctx context.Context, sel ast.
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mailingListUpdateImplementors = []string{"MailingListUpdate", "Node"}
|
||||||
|
|
||||||
|
func (ec *executionContext) _MailingListUpdate(ctx context.Context, sel ast.SelectionSet, obj *types.MailingListUpdate) graphql.Marshaler {
|
||||||
|
fields := graphql.CollectFields(ec.OperationContext, sel, mailingListUpdateImplementors)
|
||||||
|
|
||||||
|
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("MailingListUpdate")
|
||||||
|
case "id":
|
||||||
|
out.Values[i] = ec._MailingListUpdate_id(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
out.Invalids++
|
||||||
|
}
|
||||||
|
case "title":
|
||||||
|
out.Values[i] = ec._MailingListUpdate_title(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
out.Invalids++
|
||||||
|
}
|
||||||
|
case "body":
|
||||||
|
out.Values[i] = ec._MailingListUpdate_body(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
out.Invalids++
|
||||||
|
}
|
||||||
|
case "updatedAt":
|
||||||
|
out.Values[i] = ec._MailingListUpdate_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 mailingListUpdateConnectionImplementors = []string{"MailingListUpdateConnection"}
|
||||||
|
|
||||||
|
func (ec *executionContext) _MailingListUpdateConnection(ctx context.Context, sel ast.SelectionSet, obj *types.MailingListUpdateConnection) graphql.Marshaler {
|
||||||
|
fields := graphql.CollectFields(ec.OperationContext, sel, mailingListUpdateConnectionImplementors)
|
||||||
|
|
||||||
|
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("MailingListUpdateConnection")
|
||||||
|
case "edges":
|
||||||
|
out.Values[i] = ec._MailingListUpdateConnection_edges(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
out.Invalids++
|
||||||
|
}
|
||||||
|
case "pageInfo":
|
||||||
|
out.Values[i] = ec._MailingListUpdateConnection_pageInfo(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 mailingListUpdateEdgeImplementors = []string{"MailingListUpdateEdge"}
|
||||||
|
|
||||||
|
func (ec *executionContext) _MailingListUpdateEdge(ctx context.Context, sel ast.SelectionSet, obj *types.MailingListUpdateEdge) graphql.Marshaler {
|
||||||
|
fields := graphql.CollectFields(ec.OperationContext, sel, mailingListUpdateEdgeImplementors)
|
||||||
|
|
||||||
|
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("MailingListUpdateEdge")
|
||||||
|
case "cursor":
|
||||||
|
out.Values[i] = ec._MailingListUpdateEdge_cursor(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
out.Invalids++
|
||||||
|
}
|
||||||
|
case "node":
|
||||||
|
out.Values[i] = ec._MailingListUpdateEdge_node(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
out.Invalids++
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("unknown field " + strconv.Quote(field.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.Dispatch(ctx)
|
||||||
|
if out.Invalids > 0 {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.AddInt32(&ec.Deferred, int32(len(deferred)))
|
||||||
|
|
||||||
|
for label, dfs := range deferred {
|
||||||
|
ec.ProcessDeferredGroup(graphql.DeferredGroup{
|
||||||
|
Label: label,
|
||||||
|
Path: graphql.GetPath(ctx),
|
||||||
|
FieldSet: dfs,
|
||||||
|
Context: ctx,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
var mutationImplementors = []string{"Mutation"}
|
var mutationImplementors = []string{"Mutation"}
|
||||||
|
|
||||||
func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
|
func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
|
||||||
@@ -13285,6 +13957,42 @@ func (ec *executionContext) _TrustCenter(ctx context.Context, sel ast.SelectionS
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
|
||||||
|
case "updates":
|
||||||
|
field := field
|
||||||
|
|
||||||
|
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
res = ec._TrustCenter_updates(ctx, field, obj)
|
||||||
|
if res == graphql.Null {
|
||||||
|
atomic.AddUint32(&fs.Invalids, 1)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Deferrable != nil {
|
||||||
|
dfs, ok := deferred[field.Deferrable.Label]
|
||||||
|
di := 0
|
||||||
|
if ok {
|
||||||
|
dfs.AddField(field)
|
||||||
|
di = len(dfs.Values) - 1
|
||||||
|
} else {
|
||||||
|
dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
|
||||||
|
deferred[field.Deferrable.Label] = dfs
|
||||||
|
}
|
||||||
|
dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
|
||||||
|
return innerFunc(ctx, dfs)
|
||||||
|
})
|
||||||
|
|
||||||
|
// don't run the out.Concurrently() call below
|
||||||
|
out.Values[i] = graphql.Null
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
|
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
|
||||||
default:
|
default:
|
||||||
panic("unknown field " + strconv.Quote(field.Name))
|
panic("unknown field " + strconv.Quote(field.Name))
|
||||||
@@ -15532,6 +16240,56 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNMailingListUpdate2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐMailingListUpdate(ctx context.Context, sel ast.SelectionSet, v *types.MailingListUpdate) 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._MailingListUpdate(ctx, sel, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNMailingListUpdateConnection2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐMailingListUpdateConnection(ctx context.Context, sel ast.SelectionSet, v types.MailingListUpdateConnection) graphql.Marshaler {
|
||||||
|
return ec._MailingListUpdateConnection(ctx, sel, &v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNMailingListUpdateConnection2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐMailingListUpdateConnection(ctx context.Context, sel ast.SelectionSet, v *types.MailingListUpdateConnection) 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._MailingListUpdateConnection(ctx, sel, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNMailingListUpdateEdge2ᚕᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐMailingListUpdateEdgeᚄ(ctx context.Context, sel ast.SelectionSet, v []*types.MailingListUpdateEdge) graphql.Marshaler {
|
||||||
|
ret := graphql.MarshalSliceConcurrently(ctx, len(v), 0, false, func(ctx context.Context, i int) graphql.Marshaler {
|
||||||
|
fc := graphql.GetFieldContext(ctx)
|
||||||
|
fc.Result = &v[i]
|
||||||
|
return ec.marshalNMailingListUpdateEdge2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐMailingListUpdateEdge(ctx, sel, v[i])
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, e := range ret {
|
||||||
|
if e == graphql.Null {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNMailingListUpdateEdge2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋtrustᚋv1ᚋtypesᚐMailingListUpdateEdge(ctx context.Context, sel ast.SelectionSet, v *types.MailingListUpdateEdge) 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._MailingListUpdateEdge(ctx, sel, v)
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
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 v == nil {
|
||||||
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
|
|
||||||
type MailingListSubscriber struct {
|
type MailingListSubscriber struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
|
FullName string `json:"fullName"`
|
||||||
Email mail.Addr `json:"email"`
|
Email mail.Addr `json:"email"`
|
||||||
Status coredata.MailingListSubscriberStatus `json:"status"`
|
Status coredata.MailingListSubscriberStatus `json:"status"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
@@ -36,6 +37,7 @@ func (m MailingListSubscriber) GetID() gid.GID { return m.ID }
|
|||||||
func NewMailingListSubscriber(s *coredata.MailingListSubscriber) *MailingListSubscriber {
|
func NewMailingListSubscriber(s *coredata.MailingListSubscriber) *MailingListSubscriber {
|
||||||
return &MailingListSubscriber{
|
return &MailingListSubscriber{
|
||||||
ID: s.ID,
|
ID: s.ID,
|
||||||
|
FullName: s.FullName,
|
||||||
Email: s.Email,
|
Email: s.Email,
|
||||||
Status: s.Status,
|
Status: s.Status,
|
||||||
CreatedAt: s.CreatedAt,
|
CreatedAt: s.CreatedAt,
|
||||||
|
|||||||
50
pkg/server/api/trust/v1/types/mailing_list_update.go
Normal file
50
pkg/server/api/trust/v1/types/mailing_list_update.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// 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/page"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewMailingListUpdate(mlu *coredata.MailingListUpdate) *MailingListUpdate {
|
||||||
|
return &MailingListUpdate{
|
||||||
|
ID: mlu.ID,
|
||||||
|
Title: mlu.Title,
|
||||||
|
Body: mlu.Body,
|
||||||
|
UpdatedAt: mlu.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMailingListUpdateEdge(mlu *coredata.MailingListUpdate) *MailingListUpdateEdge {
|
||||||
|
return &MailingListUpdateEdge{
|
||||||
|
Cursor: mlu.CursorKey(coredata.MailingListUpdateOrderFieldUpdatedAt),
|
||||||
|
Node: NewMailingListUpdate(mlu),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMailingListUpdateConnection(
|
||||||
|
p *page.Page[*coredata.MailingListUpdate, coredata.MailingListUpdateOrderField],
|
||||||
|
) *MailingListUpdateConnection {
|
||||||
|
edges := make([]*MailingListUpdateEdge, len(p.Data))
|
||||||
|
for i, mlu := range p.Data {
|
||||||
|
edges[i] = NewMailingListUpdateEdge(mlu)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MailingListUpdateConnection{
|
||||||
|
Edges: edges,
|
||||||
|
PageInfo: NewPageInfo(p),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -153,6 +153,26 @@ type Identity struct {
|
|||||||
func (Identity) IsNode() {}
|
func (Identity) IsNode() {}
|
||||||
func (this Identity) GetID() gid.GID { return this.ID }
|
func (this Identity) GetID() gid.GID { return this.ID }
|
||||||
|
|
||||||
|
type MailingListUpdate struct {
|
||||||
|
ID gid.GID `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (MailingListUpdate) IsNode() {}
|
||||||
|
func (this MailingListUpdate) GetID() gid.GID { return this.ID }
|
||||||
|
|
||||||
|
type MailingListUpdateConnection struct {
|
||||||
|
Edges []*MailingListUpdateEdge `json:"edges"`
|
||||||
|
PageInfo *PageInfo `json:"pageInfo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MailingListUpdateEdge struct {
|
||||||
|
Cursor page.CursorKey `json:"cursor"`
|
||||||
|
Node *MailingListUpdate `json:"node"`
|
||||||
|
}
|
||||||
|
|
||||||
type Mutation struct {
|
type Mutation struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,6 +281,7 @@ type TrustCenter struct {
|
|||||||
TrustCenterFiles *TrustCenterFileConnection `json:"trustCenterFiles"`
|
TrustCenterFiles *TrustCenterFileConnection `json:"trustCenterFiles"`
|
||||||
ComplianceFrameworks *ComplianceFrameworkConnection `json:"complianceFrameworks"`
|
ComplianceFrameworks *ComplianceFrameworkConnection `json:"complianceFrameworks"`
|
||||||
ExternalUrls *ComplianceExternalURLConnection `json:"externalUrls"`
|
ExternalUrls *ComplianceExternalURLConnection `json:"externalUrls"`
|
||||||
|
Updates *MailingListUpdateConnection `json:"updates"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (TrustCenter) IsNode() {}
|
func (TrustCenter) IsNode() {}
|
||||||
|
|||||||
@@ -704,19 +704,19 @@ func (r *mutationResolver) SubscribeToMailingList(ctx context.Context) (*types.S
|
|||||||
|
|
||||||
identity := authn.IdentityFromContext(ctx)
|
identity := authn.IdentityFromContext(ctx)
|
||||||
|
|
||||||
subscriber, err := r.mailman.CreateSubscriber(ctx, *trustCenter.MailingListID, identity.EmailAddress, identity.FullName)
|
subscriber, err := r.mailman.CreateSubscriber(
|
||||||
|
ctx,
|
||||||
|
*trustCenter.MailingListID,
|
||||||
|
identity.EmailAddress,
|
||||||
|
identity.FullName,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, mailman.ErrSubscriberAlreadyExist) {
|
if errors.Is(err, mailman.ErrSubscriberAlreadyExist) {
|
||||||
subscriber, err = r.mailman.GetSubscriber(ctx, *trustCenter.MailingListID, identity.EmailAddress)
|
return nil, gqlutils.Conflictf(ctx, "already subscribed to this mailing list")
|
||||||
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))
|
r.logger.ErrorCtx(ctx, "cannot subscribe to mailing list", log.Error(err))
|
||||||
return nil, gqlutils.Internal(ctx)
|
return nil, gqlutils.Internal(ctx)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return &types.SubscribeToMailingListPayload{
|
return &types.SubscribeToMailingListPayload{
|
||||||
Subscription: types.NewMailingListSubscriber(subscriber),
|
Subscription: types.NewMailingListSubscriber(subscriber),
|
||||||
@@ -738,10 +738,13 @@ func (r *mutationResolver) UnsubscribeFromMailingList(ctx context.Context) (*typ
|
|||||||
return nil, gqlutils.Internal(ctx)
|
return nil, gqlutils.Internal(ctx)
|
||||||
}
|
}
|
||||||
if subscriber == nil {
|
if subscriber == nil {
|
||||||
return nil, gqlutils.NotFoundf(ctx, "mailing list subscription not found")
|
return nil, gqlutils.NotFoundf(ctx, "not subscribed to this mailing list")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.mailman.DeleteSubscriber(ctx, subscriber.ID); err != nil {
|
if err := r.mailman.DeleteSubscriber(ctx, subscriber.ID); err != nil {
|
||||||
|
if errors.Is(err, mailman.ErrSubscriberNotFound) {
|
||||||
|
return nil, gqlutils.NotFoundf(ctx, "not subscribed to this mailing list")
|
||||||
|
}
|
||||||
r.logger.ErrorCtx(ctx, "cannot unsubscribe from mailing list", log.Error(err))
|
r.logger.ErrorCtx(ctx, "cannot unsubscribe from mailing list", log.Error(err))
|
||||||
return nil, gqlutils.Internal(ctx)
|
return nil, gqlutils.Internal(ctx)
|
||||||
}
|
}
|
||||||
@@ -1207,6 +1210,35 @@ func (r *trustCenterResolver) ExternalUrls(ctx context.Context, obj *types.Trust
|
|||||||
return types.NewComplianceExternalURLConnection(result), nil
|
return types.NewComplianceExternalURLConnection(result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updates is the resolver for the updates field.
|
||||||
|
func (r *trustCenterResolver) Updates(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.MailingListUpdateConnection, error) {
|
||||||
|
trustService := r.TrustService(ctx, obj.ID.TenantID())
|
||||||
|
|
||||||
|
tc, err := trustService.TrustCenters.Get(ctx, obj.ID)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot load trust center", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.MailingListID == nil {
|
||||||
|
return &types.MailingListUpdateConnection{Edges: []*types.MailingListUpdateEdge{}, PageInfo: &types.PageInfo{}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pageOrderBy := page.OrderBy[coredata.MailingListUpdateOrderField]{
|
||||||
|
Field: coredata.MailingListUpdateOrderFieldUpdatedAt,
|
||||||
|
Direction: page.OrderDirectionDesc,
|
||||||
|
}
|
||||||
|
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||||
|
|
||||||
|
result, err := r.mailman.ListSentMailingListUpdates(ctx, *tc.MailingListID, cursor)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot list mailing list updates", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.NewMailingListUpdateConnection(result), nil
|
||||||
|
}
|
||||||
|
|
||||||
// IsUserAuthorized is the resolver for the isUserAuthorized field.
|
// IsUserAuthorized is the resolver for the isUserAuthorized field.
|
||||||
func (r *trustCenterFileResolver) IsUserAuthorized(ctx context.Context, obj *types.TrustCenterFile) (bool, error) {
|
func (r *trustCenterFileResolver) IsUserAuthorized(ctx context.Context, obj *types.TrustCenterFile) (bool, error) {
|
||||||
trustService := r.TrustService(ctx, obj.ID.TenantID())
|
trustService := r.TrustService(ctx, obj.ID.TenantID())
|
||||||
|
|||||||
124
pkg/server/mailactions/confirm.go
Normal file
124
pkg/server/mailactions/confirm.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
// 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 mailactions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"go.probo.inc/probo/pkg/mailman"
|
||||||
|
)
|
||||||
|
|
||||||
|
func confirmGetHandler() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := r.URL.Query().Get("token")
|
||||||
|
if token == "" {
|
||||||
|
renderPage(
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
page{
|
||||||
|
Title: "Invalid link",
|
||||||
|
Heading: "Invalid link",
|
||||||
|
Body: "This confirmation link is missing required information. Please use the link from your email.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPage(
|
||||||
|
w,
|
||||||
|
http.StatusOK,
|
||||||
|
page{
|
||||||
|
Title: "Confirm subscription",
|
||||||
|
Heading: "Confirm your subscription",
|
||||||
|
Body: "Click the button below to confirm that you want to receive updates.",
|
||||||
|
Form: &form{
|
||||||
|
ActionURL: template.URL("?token=" + url.QueryEscape(token)),
|
||||||
|
Button: "Confirm subscription",
|
||||||
|
Danger: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirmPostHandler(mailmanSvc *mailman.Service, tokenSecret string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := r.URL.Query().Get("token")
|
||||||
|
if token == "" {
|
||||||
|
renderPage(
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
page{
|
||||||
|
Title: "Invalid link",
|
||||||
|
Heading: "Invalid link",
|
||||||
|
Body: "This confirmation link is missing required information. Please use the link from your email.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := mailman.ValidateConfirmToken(tokenSecret, token)
|
||||||
|
if err != nil {
|
||||||
|
renderPage(
|
||||||
|
w,
|
||||||
|
http.StatusUnauthorized,
|
||||||
|
page{
|
||||||
|
Title: "Invalid link",
|
||||||
|
Heading: "Invalid or expired link",
|
||||||
|
Body: "This confirmation link is invalid or has expired. Confirmation links are valid for 30 days — please re-subscribe to get a new one.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mailmanSvc.ConfirmSubscriberByEmail(r.Context(), data.MailingListID, data.Email); err != nil {
|
||||||
|
if errors.Is(err, mailman.ErrSubscriberNotFound) {
|
||||||
|
renderPage(
|
||||||
|
w,
|
||||||
|
http.StatusNotFound, page{
|
||||||
|
Title: "Not found",
|
||||||
|
Heading: "Subscription not found",
|
||||||
|
Body: "We could not find your subscription. It may have already been cancelled or this link was already used.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPage(
|
||||||
|
w,
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
page{
|
||||||
|
Title: "Something went wrong",
|
||||||
|
Heading: "Something went wrong",
|
||||||
|
Body: "We could not confirm your subscription. Please try again later.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPage(
|
||||||
|
w,
|
||||||
|
http.StatusOK,
|
||||||
|
page{
|
||||||
|
Title: "Subscription confirmed",
|
||||||
|
Heading: "Subscription confirmed",
|
||||||
|
Body: "You're now subscribed and will receive updates.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
40
pkg/server/mailactions/mux.go
Normal file
40
pkg/server/mailactions/mux.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// 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 mailactions provides HTTP handlers for mailing list subscription
|
||||||
|
// management. All routes are mounted at /mail-actions/ with no API version
|
||||||
|
// prefix so they can be linked directly from emails and bookmarked by users.
|
||||||
|
//
|
||||||
|
// GET /mail-actions/unsubscribe shows an unsubscribe confirmation page
|
||||||
|
// POST /mail-actions/unsubscribe RFC 8058 one-click unsubscribe
|
||||||
|
// GET /mail-actions/confirm shows a subscription confirmation page
|
||||||
|
// POST /mail-actions/confirm confirms a pending subscription
|
||||||
|
package mailactions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"go.probo.inc/probo/pkg/mailman"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewMux(mailmanSvc *mailman.Service, tokenSecret string) *chi.Mux {
|
||||||
|
r := chi.NewMux()
|
||||||
|
|
||||||
|
r.Get("/unsubscribe", unsubscribeGetHandler())
|
||||||
|
r.Post("/unsubscribe", unsubscribePostHandler(mailmanSvc, tokenSecret))
|
||||||
|
|
||||||
|
r.Get("/confirm", confirmGetHandler())
|
||||||
|
r.Post("/confirm", confirmPostHandler(mailmanSvc, tokenSecret))
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
45
pkg/server/mailactions/render.go
Normal file
45
pkg/server/mailactions/render.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// 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 mailactions
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed templates/page.html.tmpl
|
||||||
|
var pageTmplHTML string
|
||||||
|
|
||||||
|
type form struct {
|
||||||
|
ActionURL template.URL
|
||||||
|
Button string
|
||||||
|
Danger bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type page struct {
|
||||||
|
Title string
|
||||||
|
Heading string
|
||||||
|
Body string
|
||||||
|
Form *form
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmpl = template.Must(template.New("page.html.tmpl").Parse(pageTmplHTML))
|
||||||
|
|
||||||
|
func renderPage(w http.ResponseWriter, status int, p page) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
_ = tmpl.Execute(w, p)
|
||||||
|
}
|
||||||
99
pkg/server/mailactions/templates/page.html.tmpl
Normal file
99
pkg/server/mailactions/templates/page.html.tmpl
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<style>
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background: #f3f4f6;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
padding: 2.5rem 2rem;
|
||||||
|
max-width: 26rem;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: #6b7280;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin-top: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.625rem 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background: #111827;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary:hover {
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
background: #dc2626;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger:hover {
|
||||||
|
background: #b91c1c;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>{{.Heading}}</h1>
|
||||||
|
<p>{{.Body}}</p>
|
||||||
|
{{- if .Form}}
|
||||||
|
<form method="POST" action="{{.Form.ActionURL}}">
|
||||||
|
<button type="submit" class="{{if .Form.Danger}}danger{{else}}primary{{end}}">
|
||||||
|
{{.Form.Button}}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{{- end}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
116
pkg/server/mailactions/unsubscribe.go
Normal file
116
pkg/server/mailactions/unsubscribe.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// 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 mailactions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"go.probo.inc/probo/pkg/mailman"
|
||||||
|
)
|
||||||
|
|
||||||
|
func unsubscribeGetHandler() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := r.URL.Query().Get("token")
|
||||||
|
if token == "" {
|
||||||
|
renderPage(
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
page{
|
||||||
|
Title: "Invalid link",
|
||||||
|
Heading: "Invalid link",
|
||||||
|
Body: "This unsubscribe link is missing required information. Please use the link from your email.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPage(
|
||||||
|
w,
|
||||||
|
http.StatusOK,
|
||||||
|
page{
|
||||||
|
Title: "Unsubscribe",
|
||||||
|
Heading: "Unsubscribe from mailing list",
|
||||||
|
Body: "Click the button below to confirm that you no longer want to receive updates.",
|
||||||
|
Form: &form{
|
||||||
|
ActionURL: template.URL("?token=" + url.QueryEscape(token)),
|
||||||
|
Button: "Confirm unsubscribe",
|
||||||
|
Danger: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsubscribePostHandler(mailmanSvc *mailman.Service, tokenSecret string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := r.URL.Query().Get("token")
|
||||||
|
if token == "" {
|
||||||
|
renderPage(
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
page{
|
||||||
|
Title: "Invalid link",
|
||||||
|
Heading: "Invalid link",
|
||||||
|
Body: "This unsubscribe link is missing required information. Please use the link from your email.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := mailman.ValidateUnsubscribeToken(tokenSecret, token)
|
||||||
|
if err != nil {
|
||||||
|
renderPage(
|
||||||
|
w,
|
||||||
|
http.StatusUnauthorized,
|
||||||
|
page{
|
||||||
|
Title: "Invalid link",
|
||||||
|
Heading: "Invalid or expired link",
|
||||||
|
Body: "This unsubscribe link is invalid or has expired.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mailmanSvc.UnsubscribeByEmail(r.Context(), data.MailingListID, data.Email); err != nil {
|
||||||
|
if !errors.Is(err, mailman.ErrSubscriberNotFound) {
|
||||||
|
renderPage(
|
||||||
|
w,
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
page{
|
||||||
|
Title: "Something went wrong",
|
||||||
|
Heading: "Something went wrong",
|
||||||
|
Body: "We could not process your request. Please try again later.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also success when already unsubscribed — unsubscribe is idempotent
|
||||||
|
// per RFC 8058.
|
||||||
|
renderPage(
|
||||||
|
w,
|
||||||
|
http.StatusOK,
|
||||||
|
page{
|
||||||
|
Title: "Unsubscribed",
|
||||||
|
Heading: "You've been unsubscribed",
|
||||||
|
Body: "You will no longer receive updates.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/securecookie"
|
"go.probo.inc/probo/pkg/securecookie"
|
||||||
"go.probo.inc/probo/pkg/server/api"
|
"go.probo.inc/probo/pkg/server/api"
|
||||||
"go.probo.inc/probo/pkg/server/api/compliancepage"
|
"go.probo.inc/probo/pkg/server/api/compliancepage"
|
||||||
|
"go.probo.inc/probo/pkg/server/mailactions"
|
||||||
trust_web "go.probo.inc/probo/pkg/server/trust"
|
trust_web "go.probo.inc/probo/pkg/server/trust"
|
||||||
console_web "go.probo.inc/probo/pkg/server/web"
|
console_web "go.probo.inc/probo/pkg/server/web"
|
||||||
"go.probo.inc/probo/pkg/slack"
|
"go.probo.inc/probo/pkg/slack"
|
||||||
@@ -58,6 +59,7 @@ type Config struct {
|
|||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
apiServer *api.Server
|
apiServer *api.Server
|
||||||
|
mailActionsHandler http.Handler
|
||||||
consoleWebServer *console_web.Server
|
consoleWebServer *console_web.Server
|
||||||
trustWebServer *trust_web.Server
|
trustWebServer *trust_web.Server
|
||||||
router *chi.Mux
|
router *chi.Mux
|
||||||
@@ -103,6 +105,7 @@ func NewServer(cfg Config) (*Server, error) {
|
|||||||
|
|
||||||
server := &Server{
|
server := &Server{
|
||||||
apiServer: apiServer,
|
apiServer: apiServer,
|
||||||
|
mailActionsHandler: mailactions.NewMux(cfg.Mailman, cfg.TokenSecret),
|
||||||
consoleWebServer: consoleWebServer,
|
consoleWebServer: consoleWebServer,
|
||||||
trustWebServer: trustWebServer,
|
trustWebServer: trustWebServer,
|
||||||
router: router,
|
router: router,
|
||||||
@@ -119,6 +122,7 @@ func NewServer(cfg Config) (*Server, error) {
|
|||||||
|
|
||||||
func (s *Server) setupRoutes(baseURL string) {
|
func (s *Server) setupRoutes(baseURL string) {
|
||||||
s.router.Mount("/api", http.StripPrefix("/api", s.apiServer))
|
s.router.Mount("/api", http.StripPrefix("/api", s.apiServer))
|
||||||
|
s.router.Mount("/mail-actions", http.StripPrefix("/mail-actions", s.mailActionsHandler))
|
||||||
|
|
||||||
s.router.Route("/trust/{slugOrId}", func(r chi.Router) {
|
s.router.Route("/trust/{slugOrId}", func(r chi.Router) {
|
||||||
r.Use(compliancepage.NewIDMiddleware(s.trustService, baseURL))
|
r.Use(compliancepage.NewIDMiddleware(s.trustService, baseURL))
|
||||||
|
|||||||
@@ -571,6 +571,7 @@ func (s *TrustCenterAccessService) sendAccessEmail(ctx context.Context, tx pg.Co
|
|||||||
subject,
|
subject,
|
||||||
textBody,
|
textBody,
|
||||||
htmlBody,
|
htmlBody,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := accessEmail.Insert(ctx, tx); err != nil {
|
if err := accessEmail.Insert(ctx, tx); err != nil {
|
||||||
@@ -705,6 +706,7 @@ func (s *TrustCenterAccessService) sendDocumentAccessRejectedEmail(
|
|||||||
subject,
|
subject,
|
||||||
textBody,
|
textBody,
|
||||||
htmlBody,
|
htmlBody,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := accessEmail.Insert(ctx, tx); err != nil {
|
if err := accessEmail.Insert(ctx, tx); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user