payload-plugin-newsletter 0.3.2 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -1
- package/CLAUDE.md +31 -19
- package/dist/client.cjs +899 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +52 -0
- package/dist/client.d.ts +52 -0
- package/dist/client.js +867 -0
- package/dist/client.js.map +1 -0
- package/dist/components.cjs +899 -0
- package/dist/components.cjs.map +1 -0
- package/dist/components.d.cts +4 -0
- package/dist/components.d.ts +4 -0
- package/dist/components.js +867 -0
- package/dist/components.js.map +1 -0
- package/dist/index.cjs +2004 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.js +1967 -0
- package/dist/index.js.map +1 -0
- package/dist/types.cjs +19 -0
- package/dist/types.cjs.map +1 -0
- package/dist/{types/index.d.ts → types.d.cts} +19 -17
- package/dist/types.d.ts +350 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +48 -25
- package/dist/.tsbuildinfo +0 -1
- package/dist/collections/NewsletterSettings.d.ts +0 -4
- package/dist/collections/NewsletterSettings.d.ts.map +0 -1
- package/dist/collections/Subscribers.d.ts +0 -4
- package/dist/collections/Subscribers.d.ts.map +0 -1
- package/dist/components/MagicLinkVerify.d.ts +0 -27
- package/dist/components/MagicLinkVerify.d.ts.map +0 -1
- package/dist/components/NewsletterForm.d.ts +0 -5
- package/dist/components/NewsletterForm.d.ts.map +0 -1
- package/dist/components/PreferencesForm.d.ts +0 -5
- package/dist/components/PreferencesForm.d.ts.map +0 -1
- package/dist/components/index.d.ts +0 -5
- package/dist/components/index.d.ts.map +0 -1
- package/dist/endpoints/index.d.ts +0 -4
- package/dist/endpoints/index.d.ts.map +0 -1
- package/dist/endpoints/preferences.d.ts +0 -5
- package/dist/endpoints/preferences.d.ts.map +0 -1
- package/dist/endpoints/subscribe.d.ts +0 -4
- package/dist/endpoints/subscribe.d.ts.map +0 -1
- package/dist/endpoints/unsubscribe.d.ts +0 -4
- package/dist/endpoints/unsubscribe.d.ts.map +0 -1
- package/dist/endpoints/verify-magic-link.d.ts +0 -4
- package/dist/endpoints/verify-magic-link.d.ts.map +0 -1
- package/dist/exports/client.d.ts +0 -6
- package/dist/exports/client.d.ts.map +0 -1
- package/dist/exports/components.d.ts +0 -2
- package/dist/exports/components.d.ts.map +0 -1
- package/dist/exports/types.d.ts +0 -2
- package/dist/exports/types.d.ts.map +0 -1
- package/dist/fields/newsletterScheduling.d.ts +0 -4
- package/dist/fields/newsletterScheduling.d.ts.map +0 -1
- package/dist/hooks/useNewsletterAuth.d.ts +0 -16
- package/dist/hooks/useNewsletterAuth.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/providers/broadcast.d.ts +0 -19
- package/dist/providers/broadcast.d.ts.map +0 -1
- package/dist/providers/index.d.ts +0 -23
- package/dist/providers/index.d.ts.map +0 -1
- package/dist/providers/resend.d.ts +0 -20
- package/dist/providers/resend.d.ts.map +0 -1
- package/dist/providers/types.d.ts +0 -46
- package/dist/providers/types.d.ts.map +0 -1
- package/dist/src/__tests__/fixtures/newsletter-settings.js +0 -41
- package/dist/src/__tests__/fixtures/newsletter-settings.js.map +0 -1
- package/dist/src/__tests__/fixtures/subscribers.js +0 -70
- package/dist/src/__tests__/fixtures/subscribers.js.map +0 -1
- package/dist/src/__tests__/integration/collections/subscriber-hooks.test.js +0 -356
- package/dist/src/__tests__/integration/collections/subscriber-hooks.test.js.map +0 -1
- package/dist/src/__tests__/integration/endpoints/preferences.test.js +0 -266
- package/dist/src/__tests__/integration/endpoints/preferences.test.js.map +0 -1
- package/dist/src/__tests__/integration/endpoints/subscribe.test.js +0 -280
- package/dist/src/__tests__/integration/endpoints/subscribe.test.js.map +0 -1
- package/dist/src/__tests__/integration/endpoints/unsubscribe.test.js +0 -187
- package/dist/src/__tests__/integration/endpoints/unsubscribe.test.js.map +0 -1
- package/dist/src/__tests__/integration/endpoints/verify-magic-link.test.js +0 -188
- package/dist/src/__tests__/integration/endpoints/verify-magic-link.test.js.map +0 -1
- package/dist/src/__tests__/mocks/email-providers.js +0 -153
- package/dist/src/__tests__/mocks/email-providers.js.map +0 -1
- package/dist/src/__tests__/mocks/payload.js +0 -244
- package/dist/src/__tests__/mocks/payload.js.map +0 -1
- package/dist/src/__tests__/security/csrf-protection.test.js +0 -309
- package/dist/src/__tests__/security/csrf-protection.test.js.map +0 -1
- package/dist/src/__tests__/security/settings-access.test.js +0 -204
- package/dist/src/__tests__/security/settings-access.test.js.map +0 -1
- package/dist/src/__tests__/security/subscriber-access.test.js +0 -210
- package/dist/src/__tests__/security/subscriber-access.test.js.map +0 -1
- package/dist/src/__tests__/security/xss-prevention.test.js +0 -305
- package/dist/src/__tests__/security/xss-prevention.test.js.map +0 -1
- package/dist/src/__tests__/setup/integration.setup.js +0 -38
- package/dist/src/__tests__/setup/integration.setup.js.map +0 -1
- package/dist/src/__tests__/setup/unit.setup.js +0 -41
- package/dist/src/__tests__/setup/unit.setup.js.map +0 -1
- package/dist/src/__tests__/unit/utils/access.test.js +0 -116
- package/dist/src/__tests__/unit/utils/access.test.js.map +0 -1
- package/dist/src/__tests__/unit/utils/jwt.test.js +0 -238
- package/dist/src/__tests__/unit/utils/jwt.test.js.map +0 -1
- package/dist/src/collections/NewsletterSettings.js +0 -390
- package/dist/src/collections/NewsletterSettings.js.map +0 -1
- package/dist/src/collections/Subscribers.js +0 -309
- package/dist/src/collections/Subscribers.js.map +0 -1
- package/dist/src/components/MagicLinkVerify.js +0 -180
- package/dist/src/components/MagicLinkVerify.js.map +0 -1
- package/dist/src/components/NewsletterForm.js +0 -326
- package/dist/src/components/NewsletterForm.js.map +0 -1
- package/dist/src/components/PreferencesForm.js +0 -524
- package/dist/src/components/PreferencesForm.js.map +0 -1
- package/dist/src/components/index.js +0 -5
- package/dist/src/components/index.js.map +0 -1
- package/dist/src/endpoints/index.js +0 -17
- package/dist/src/endpoints/index.js.map +0 -1
- package/dist/src/endpoints/preferences.js +0 -136
- package/dist/src/endpoints/preferences.js.map +0 -1
- package/dist/src/endpoints/subscribe.js +0 -151
- package/dist/src/endpoints/subscribe.js.map +0 -1
- package/dist/src/endpoints/unsubscribe.js +0 -105
- package/dist/src/endpoints/unsubscribe.js.map +0 -1
- package/dist/src/endpoints/verify-magic-link.js +0 -103
- package/dist/src/endpoints/verify-magic-link.js.map +0 -1
- package/dist/src/exports/client.js +0 -7
- package/dist/src/exports/client.js.map +0 -1
- package/dist/src/exports/components.js +0 -6
- package/dist/src/exports/components.js.map +0 -1
- package/dist/src/exports/types.js +0 -3
- package/dist/src/exports/types.js.map +0 -1
- package/dist/src/fields/newsletterScheduling.js +0 -195
- package/dist/src/fields/newsletterScheduling.js.map +0 -1
- package/dist/src/hooks/useNewsletterAuth.js +0 -112
- package/dist/src/hooks/useNewsletterAuth.js.map +0 -1
- package/dist/src/index.js +0 -130
- package/dist/src/index.js.map +0 -1
- package/dist/src/providers/broadcast.js +0 -158
- package/dist/src/providers/broadcast.js.map +0 -1
- package/dist/src/providers/index.js +0 -63
- package/dist/src/providers/index.js.map +0 -1
- package/dist/src/providers/resend.js +0 -122
- package/dist/src/providers/resend.js.map +0 -1
- package/dist/src/providers/types.js +0 -12
- package/dist/src/providers/types.js.map +0 -1
- package/dist/src/templates/BaseTemplate.js +0 -105
- package/dist/src/templates/BaseTemplate.js.map +0 -1
- package/dist/src/templates/MagicLinkTemplate.js +0 -178
- package/dist/src/templates/MagicLinkTemplate.js.map +0 -1
- package/dist/src/templates/NewsletterTemplate.js +0 -150
- package/dist/src/templates/NewsletterTemplate.js.map +0 -1
- package/dist/src/templates/WelcomeTemplate.js +0 -192
- package/dist/src/templates/WelcomeTemplate.js.map +0 -1
- package/dist/src/templates/index.js +0 -6
- package/dist/src/templates/index.js.map +0 -1
- package/dist/src/types/index.js +0 -3
- package/dist/src/types/index.js.map +0 -1
- package/dist/src/utils/access.js +0 -80
- package/dist/src/utils/access.js.map +0 -1
- package/dist/src/utils/jwt.js +0 -91
- package/dist/src/utils/jwt.js.map +0 -1
- package/dist/src/utils/validation.js +0 -74
- package/dist/src/utils/validation.js.map +0 -1
- package/dist/templates/BaseTemplate.d.ts +0 -45
- package/dist/templates/BaseTemplate.d.ts.map +0 -1
- package/dist/templates/MagicLinkTemplate.d.ts +0 -67
- package/dist/templates/MagicLinkTemplate.d.ts.map +0 -1
- package/dist/templates/NewsletterTemplate.d.ts +0 -112
- package/dist/templates/NewsletterTemplate.d.ts.map +0 -1
- package/dist/templates/WelcomeTemplate.d.ts +0 -55
- package/dist/templates/WelcomeTemplate.d.ts.map +0 -1
- package/dist/templates/index.d.ts +0 -7
- package/dist/templates/index.d.ts.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/utils/access.d.ts +0 -15
- package/dist/utils/access.d.ts.map +0 -1
- package/dist/utils/jwt.d.ts +0 -32
- package/dist/utils/jwt.d.ts.map +0 -1
- package/dist/utils/validation.d.ts +0 -25
- package/dist/utils/validation.d.ts.map +0 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2004 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
default: () => newsletterPlugin,
|
|
34
|
+
newsletterPlugin: () => newsletterPlugin
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(src_exports);
|
|
37
|
+
|
|
38
|
+
// src/utils/access.ts
|
|
39
|
+
var isAdmin = (user, config) => {
|
|
40
|
+
if (!user || user.collection !== "users") {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (config?.access?.isAdmin) {
|
|
44
|
+
return config.access.isAdmin(user);
|
|
45
|
+
}
|
|
46
|
+
if (user.roles?.includes("admin")) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (user.isAdmin === true) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (user.role === "admin") {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (user.admin === true) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
};
|
|
60
|
+
var adminOnly = (config) => ({ req }) => {
|
|
61
|
+
const user = req.user;
|
|
62
|
+
return isAdmin(user, config);
|
|
63
|
+
};
|
|
64
|
+
var adminOrSelf = (config) => ({ req, id }) => {
|
|
65
|
+
const user = req.user;
|
|
66
|
+
if (!user) {
|
|
67
|
+
if (!id) {
|
|
68
|
+
return {
|
|
69
|
+
id: {
|
|
70
|
+
equals: "unauthorized-no-access"
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (isAdmin(user, config)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (user.collection === "subscribers") {
|
|
80
|
+
if (!id) {
|
|
81
|
+
return {
|
|
82
|
+
id: {
|
|
83
|
+
equals: user.id
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return id === user.id;
|
|
88
|
+
}
|
|
89
|
+
if (!id) {
|
|
90
|
+
return {
|
|
91
|
+
id: {
|
|
92
|
+
equals: "unauthorized-no-access"
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// src/collections/Subscribers.ts
|
|
100
|
+
var createSubscribersCollection = (pluginConfig) => {
|
|
101
|
+
const slug = pluginConfig.subscribersSlug || "subscribers";
|
|
102
|
+
const defaultFields = [
|
|
103
|
+
// Core fields
|
|
104
|
+
{
|
|
105
|
+
name: "email",
|
|
106
|
+
type: "email",
|
|
107
|
+
required: true,
|
|
108
|
+
unique: true,
|
|
109
|
+
admin: {
|
|
110
|
+
description: "Subscriber email address"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "name",
|
|
115
|
+
type: "text",
|
|
116
|
+
admin: {
|
|
117
|
+
description: "Subscriber full name"
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "locale",
|
|
122
|
+
type: "select",
|
|
123
|
+
options: pluginConfig.i18n?.locales?.map((locale) => ({
|
|
124
|
+
label: locale.toUpperCase(),
|
|
125
|
+
value: locale
|
|
126
|
+
})) || [
|
|
127
|
+
{ label: "EN", value: "en" }
|
|
128
|
+
],
|
|
129
|
+
defaultValue: pluginConfig.i18n?.defaultLocale || "en",
|
|
130
|
+
admin: {
|
|
131
|
+
description: "Preferred language for communications"
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
// Authentication fields (hidden from admin UI)
|
|
135
|
+
{
|
|
136
|
+
name: "magicLinkToken",
|
|
137
|
+
type: "text",
|
|
138
|
+
hidden: true
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "magicLinkTokenExpiry",
|
|
142
|
+
type: "date",
|
|
143
|
+
hidden: true
|
|
144
|
+
},
|
|
145
|
+
// Subscription status
|
|
146
|
+
{
|
|
147
|
+
name: "subscriptionStatus",
|
|
148
|
+
type: "select",
|
|
149
|
+
options: [
|
|
150
|
+
{ label: "Active", value: "active" },
|
|
151
|
+
{ label: "Unsubscribed", value: "unsubscribed" },
|
|
152
|
+
{ label: "Pending", value: "pending" }
|
|
153
|
+
],
|
|
154
|
+
defaultValue: "pending",
|
|
155
|
+
required: true,
|
|
156
|
+
admin: {
|
|
157
|
+
description: "Current subscription status"
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "unsubscribedAt",
|
|
162
|
+
type: "date",
|
|
163
|
+
admin: {
|
|
164
|
+
condition: (data) => data?.subscriptionStatus === "unsubscribed",
|
|
165
|
+
description: "When the user unsubscribed",
|
|
166
|
+
readOnly: true
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
// Email preferences
|
|
170
|
+
{
|
|
171
|
+
name: "emailPreferences",
|
|
172
|
+
type: "group",
|
|
173
|
+
fields: [
|
|
174
|
+
{
|
|
175
|
+
name: "newsletter",
|
|
176
|
+
type: "checkbox",
|
|
177
|
+
defaultValue: true,
|
|
178
|
+
label: "Newsletter",
|
|
179
|
+
admin: {
|
|
180
|
+
description: "Receive regular newsletter updates"
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: "announcements",
|
|
185
|
+
type: "checkbox",
|
|
186
|
+
defaultValue: true,
|
|
187
|
+
label: "Announcements",
|
|
188
|
+
admin: {
|
|
189
|
+
description: "Receive important announcements"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
],
|
|
193
|
+
admin: {
|
|
194
|
+
description: "Email communication preferences"
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
// Source tracking
|
|
198
|
+
{
|
|
199
|
+
name: "source",
|
|
200
|
+
type: "text",
|
|
201
|
+
admin: {
|
|
202
|
+
description: "Where the subscriber signed up from"
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
];
|
|
206
|
+
if (pluginConfig.features?.utmTracking?.enabled) {
|
|
207
|
+
const utmFields = pluginConfig.features.utmTracking.fields || [
|
|
208
|
+
"source",
|
|
209
|
+
"medium",
|
|
210
|
+
"campaign",
|
|
211
|
+
"content",
|
|
212
|
+
"term"
|
|
213
|
+
];
|
|
214
|
+
defaultFields.push({
|
|
215
|
+
name: "utmParameters",
|
|
216
|
+
type: "group",
|
|
217
|
+
fields: utmFields.map((field) => ({
|
|
218
|
+
name: field,
|
|
219
|
+
type: "text",
|
|
220
|
+
admin: {
|
|
221
|
+
description: `UTM ${field} parameter`
|
|
222
|
+
}
|
|
223
|
+
})),
|
|
224
|
+
admin: {
|
|
225
|
+
description: "UTM tracking parameters"
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
defaultFields.push({
|
|
230
|
+
name: "signupMetadata",
|
|
231
|
+
type: "group",
|
|
232
|
+
fields: [
|
|
233
|
+
{
|
|
234
|
+
name: "ipAddress",
|
|
235
|
+
type: "text",
|
|
236
|
+
admin: {
|
|
237
|
+
readOnly: true
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: "userAgent",
|
|
242
|
+
type: "text",
|
|
243
|
+
admin: {
|
|
244
|
+
readOnly: true
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "referrer",
|
|
249
|
+
type: "text",
|
|
250
|
+
admin: {
|
|
251
|
+
readOnly: true
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: "signupPage",
|
|
256
|
+
type: "text",
|
|
257
|
+
admin: {
|
|
258
|
+
readOnly: true
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
],
|
|
262
|
+
admin: {
|
|
263
|
+
description: "Technical information about signup"
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
if (pluginConfig.features?.leadMagnets?.enabled) {
|
|
267
|
+
defaultFields.push({
|
|
268
|
+
name: "leadMagnet",
|
|
269
|
+
type: "relationship",
|
|
270
|
+
relationTo: pluginConfig.features.leadMagnets.collection || "media",
|
|
271
|
+
admin: {
|
|
272
|
+
description: "Lead magnet downloaded at signup"
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
let fields = defaultFields;
|
|
277
|
+
if (pluginConfig.fields?.overrides) {
|
|
278
|
+
fields = pluginConfig.fields.overrides({ defaultFields });
|
|
279
|
+
}
|
|
280
|
+
if (pluginConfig.fields?.additional) {
|
|
281
|
+
fields = [...fields, ...pluginConfig.fields.additional];
|
|
282
|
+
}
|
|
283
|
+
const subscribersCollection = {
|
|
284
|
+
slug,
|
|
285
|
+
labels: {
|
|
286
|
+
singular: "Subscriber",
|
|
287
|
+
plural: "Subscribers"
|
|
288
|
+
},
|
|
289
|
+
admin: {
|
|
290
|
+
useAsTitle: "email",
|
|
291
|
+
defaultColumns: ["email", "name", "subscriptionStatus", "createdAt"],
|
|
292
|
+
group: "Newsletter"
|
|
293
|
+
},
|
|
294
|
+
fields,
|
|
295
|
+
hooks: {
|
|
296
|
+
afterChange: [
|
|
297
|
+
async ({ doc, req, operation, previousDoc }) => {
|
|
298
|
+
if (operation === "create") {
|
|
299
|
+
const emailService = req.payload.newsletterEmailService;
|
|
300
|
+
if (emailService) {
|
|
301
|
+
try {
|
|
302
|
+
await emailService.addContact(doc);
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (doc.subscriptionStatus === "active" && emailService) {
|
|
307
|
+
try {
|
|
308
|
+
} catch {
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (pluginConfig.hooks?.afterSubscribe) {
|
|
312
|
+
await pluginConfig.hooks.afterSubscribe({ doc, req });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (operation === "update" && previousDoc) {
|
|
316
|
+
const emailService = req.payload.newsletterEmailService;
|
|
317
|
+
if (doc.subscriptionStatus !== previousDoc.subscriptionStatus && emailService) {
|
|
318
|
+
try {
|
|
319
|
+
await emailService.updateContact(doc);
|
|
320
|
+
} catch {
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (doc.subscriptionStatus === "unsubscribed" && previousDoc.subscriptionStatus !== "unsubscribed") {
|
|
324
|
+
doc.unsubscribedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
325
|
+
if (pluginConfig.hooks?.afterUnsubscribe) {
|
|
326
|
+
await pluginConfig.hooks.afterUnsubscribe({ doc, req });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
],
|
|
332
|
+
beforeDelete: [
|
|
333
|
+
async ({ id, req }) => {
|
|
334
|
+
const emailService = req.payload.newsletterEmailService;
|
|
335
|
+
if (emailService) {
|
|
336
|
+
try {
|
|
337
|
+
const doc = await req.payload.findByID({
|
|
338
|
+
collection: slug,
|
|
339
|
+
id
|
|
340
|
+
});
|
|
341
|
+
await emailService.removeContact(doc.email);
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
]
|
|
347
|
+
},
|
|
348
|
+
access: {
|
|
349
|
+
create: () => true,
|
|
350
|
+
// Public can subscribe
|
|
351
|
+
read: adminOrSelf(pluginConfig),
|
|
352
|
+
update: adminOrSelf(pluginConfig),
|
|
353
|
+
delete: adminOnly(pluginConfig)
|
|
354
|
+
},
|
|
355
|
+
timestamps: true
|
|
356
|
+
};
|
|
357
|
+
return subscribersCollection;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// src/collections/NewsletterSettings.ts
|
|
361
|
+
var createNewsletterSettingsCollection = (pluginConfig) => {
|
|
362
|
+
const slug = pluginConfig.settingsSlug || "newsletter-settings";
|
|
363
|
+
return {
|
|
364
|
+
slug,
|
|
365
|
+
labels: {
|
|
366
|
+
singular: "Newsletter Setting",
|
|
367
|
+
plural: "Newsletter Settings"
|
|
368
|
+
},
|
|
369
|
+
admin: {
|
|
370
|
+
useAsTitle: "name",
|
|
371
|
+
defaultColumns: ["name", "provider", "active", "updatedAt"],
|
|
372
|
+
group: "Newsletter",
|
|
373
|
+
description: "Configure email provider settings and templates"
|
|
374
|
+
},
|
|
375
|
+
fields: [
|
|
376
|
+
{
|
|
377
|
+
name: "name",
|
|
378
|
+
type: "text",
|
|
379
|
+
label: "Configuration Name",
|
|
380
|
+
required: true,
|
|
381
|
+
admin: {
|
|
382
|
+
description: 'A descriptive name for this configuration (e.g., "Production", "Development", "Marketing Emails")'
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
name: "active",
|
|
387
|
+
type: "checkbox",
|
|
388
|
+
label: "Active",
|
|
389
|
+
defaultValue: false,
|
|
390
|
+
admin: {
|
|
391
|
+
description: "Only one configuration can be active at a time"
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
type: "tabs",
|
|
396
|
+
tabs: [
|
|
397
|
+
{
|
|
398
|
+
label: "Provider Settings",
|
|
399
|
+
fields: [
|
|
400
|
+
{
|
|
401
|
+
name: "provider",
|
|
402
|
+
type: "select",
|
|
403
|
+
label: "Email Provider",
|
|
404
|
+
required: true,
|
|
405
|
+
options: [
|
|
406
|
+
{ label: "Resend", value: "resend" },
|
|
407
|
+
{ label: "Broadcast (Self-Hosted)", value: "broadcast" }
|
|
408
|
+
],
|
|
409
|
+
defaultValue: pluginConfig.providers.default,
|
|
410
|
+
admin: {
|
|
411
|
+
description: "Choose which email service to use"
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
name: "resendSettings",
|
|
416
|
+
type: "group",
|
|
417
|
+
label: "Resend Settings",
|
|
418
|
+
admin: {
|
|
419
|
+
condition: (data) => data?.provider === "resend"
|
|
420
|
+
},
|
|
421
|
+
fields: [
|
|
422
|
+
{
|
|
423
|
+
name: "apiKey",
|
|
424
|
+
type: "text",
|
|
425
|
+
label: "API Key",
|
|
426
|
+
required: true,
|
|
427
|
+
admin: {
|
|
428
|
+
description: "Your Resend API key"
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: "audienceIds",
|
|
433
|
+
type: "array",
|
|
434
|
+
label: "Audience IDs by Locale",
|
|
435
|
+
fields: [
|
|
436
|
+
{
|
|
437
|
+
name: "locale",
|
|
438
|
+
type: "select",
|
|
439
|
+
label: "Locale",
|
|
440
|
+
required: true,
|
|
441
|
+
options: pluginConfig.i18n?.locales?.map((locale) => ({
|
|
442
|
+
label: locale.toUpperCase(),
|
|
443
|
+
value: locale
|
|
444
|
+
})) || [
|
|
445
|
+
{ label: "EN", value: "en" }
|
|
446
|
+
]
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: "production",
|
|
450
|
+
type: "text",
|
|
451
|
+
label: "Production Audience ID"
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
name: "development",
|
|
455
|
+
type: "text",
|
|
456
|
+
label: "Development Audience ID"
|
|
457
|
+
}
|
|
458
|
+
]
|
|
459
|
+
}
|
|
460
|
+
]
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: "broadcastSettings",
|
|
464
|
+
type: "group",
|
|
465
|
+
label: "Broadcast Settings",
|
|
466
|
+
admin: {
|
|
467
|
+
condition: (data) => data?.provider === "broadcast"
|
|
468
|
+
},
|
|
469
|
+
fields: [
|
|
470
|
+
{
|
|
471
|
+
name: "apiUrl",
|
|
472
|
+
type: "text",
|
|
473
|
+
label: "API URL",
|
|
474
|
+
required: true,
|
|
475
|
+
admin: {
|
|
476
|
+
description: "Your Broadcast instance URL"
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
name: "productionToken",
|
|
481
|
+
type: "text",
|
|
482
|
+
label: "Production Token",
|
|
483
|
+
admin: {
|
|
484
|
+
description: "Token for production environment"
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
name: "developmentToken",
|
|
489
|
+
type: "text",
|
|
490
|
+
label: "Development Token",
|
|
491
|
+
admin: {
|
|
492
|
+
description: "Token for development environment"
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
]
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: "fromAddress",
|
|
499
|
+
type: "email",
|
|
500
|
+
label: "From Address",
|
|
501
|
+
required: true,
|
|
502
|
+
admin: {
|
|
503
|
+
description: "Default sender email address"
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
name: "fromName",
|
|
508
|
+
type: "text",
|
|
509
|
+
label: "From Name",
|
|
510
|
+
required: true,
|
|
511
|
+
admin: {
|
|
512
|
+
description: "Default sender name"
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: "replyTo",
|
|
517
|
+
type: "email",
|
|
518
|
+
label: "Reply-To Address",
|
|
519
|
+
admin: {
|
|
520
|
+
description: "Optional reply-to email address"
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
]
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
label: "Email Templates",
|
|
527
|
+
fields: [
|
|
528
|
+
{
|
|
529
|
+
name: "emailTemplates",
|
|
530
|
+
type: "group",
|
|
531
|
+
label: "Email Templates",
|
|
532
|
+
fields: [
|
|
533
|
+
{
|
|
534
|
+
name: "welcome",
|
|
535
|
+
type: "group",
|
|
536
|
+
label: "Welcome Email",
|
|
537
|
+
fields: [
|
|
538
|
+
{
|
|
539
|
+
name: "enabled",
|
|
540
|
+
type: "checkbox",
|
|
541
|
+
label: "Send Welcome Email",
|
|
542
|
+
defaultValue: true
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
name: "subject",
|
|
546
|
+
type: "text",
|
|
547
|
+
label: "Subject Line",
|
|
548
|
+
defaultValue: "Welcome to {{fromName}}!",
|
|
549
|
+
admin: {
|
|
550
|
+
condition: (data) => data?.emailTemplates?.welcome?.enabled
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
name: "preheader",
|
|
555
|
+
type: "text",
|
|
556
|
+
label: "Preheader Text",
|
|
557
|
+
admin: {
|
|
558
|
+
condition: (data) => data?.emailTemplates?.welcome?.enabled
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
]
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: "magicLink",
|
|
565
|
+
type: "group",
|
|
566
|
+
label: "Magic Link Email",
|
|
567
|
+
fields: [
|
|
568
|
+
{
|
|
569
|
+
name: "subject",
|
|
570
|
+
type: "text",
|
|
571
|
+
label: "Subject Line",
|
|
572
|
+
defaultValue: "Sign in to {{fromName}}"
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
name: "preheader",
|
|
576
|
+
type: "text",
|
|
577
|
+
label: "Preheader Text",
|
|
578
|
+
defaultValue: "Click the link to access your preferences"
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
name: "expirationTime",
|
|
582
|
+
type: "select",
|
|
583
|
+
label: "Link Expiration",
|
|
584
|
+
defaultValue: "7d",
|
|
585
|
+
options: [
|
|
586
|
+
{ label: "1 hour", value: "1h" },
|
|
587
|
+
{ label: "24 hours", value: "24h" },
|
|
588
|
+
{ label: "7 days", value: "7d" },
|
|
589
|
+
{ label: "30 days", value: "30d" }
|
|
590
|
+
]
|
|
591
|
+
}
|
|
592
|
+
]
|
|
593
|
+
}
|
|
594
|
+
]
|
|
595
|
+
}
|
|
596
|
+
]
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
label: "Subscription Settings",
|
|
600
|
+
fields: [
|
|
601
|
+
{
|
|
602
|
+
name: "subscriptionSettings",
|
|
603
|
+
type: "group",
|
|
604
|
+
label: "Subscription Settings",
|
|
605
|
+
fields: [
|
|
606
|
+
{
|
|
607
|
+
name: "requireDoubleOptIn",
|
|
608
|
+
type: "checkbox",
|
|
609
|
+
label: "Require Double Opt-In",
|
|
610
|
+
defaultValue: false,
|
|
611
|
+
admin: {
|
|
612
|
+
description: "Require email confirmation before activating subscriptions"
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
name: "allowedDomains",
|
|
617
|
+
type: "array",
|
|
618
|
+
label: "Allowed Email Domains",
|
|
619
|
+
admin: {
|
|
620
|
+
description: "Leave empty to allow all domains"
|
|
621
|
+
},
|
|
622
|
+
fields: [
|
|
623
|
+
{
|
|
624
|
+
name: "domain",
|
|
625
|
+
type: "text",
|
|
626
|
+
label: "Domain",
|
|
627
|
+
required: true,
|
|
628
|
+
admin: {
|
|
629
|
+
placeholder: "example.com"
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
]
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: "maxSubscribersPerIP",
|
|
636
|
+
type: "number",
|
|
637
|
+
label: "Max Subscribers per IP",
|
|
638
|
+
defaultValue: 10,
|
|
639
|
+
min: 1,
|
|
640
|
+
admin: {
|
|
641
|
+
description: "Maximum number of subscriptions allowed from a single IP address"
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
]
|
|
645
|
+
}
|
|
646
|
+
]
|
|
647
|
+
}
|
|
648
|
+
]
|
|
649
|
+
}
|
|
650
|
+
],
|
|
651
|
+
hooks: {
|
|
652
|
+
beforeChange: [
|
|
653
|
+
async ({ data, req, operation }) => {
|
|
654
|
+
if (!req.user || req.user.collection !== "users") {
|
|
655
|
+
throw new Error("Only administrators can modify newsletter settings");
|
|
656
|
+
}
|
|
657
|
+
if (data?.active && operation !== "create") {
|
|
658
|
+
await req.payload.update({
|
|
659
|
+
collection: slug,
|
|
660
|
+
where: {
|
|
661
|
+
id: {
|
|
662
|
+
not_equals: data.id
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
data: {
|
|
666
|
+
active: false
|
|
667
|
+
}
|
|
668
|
+
// Keep overrideAccess: true for admin operations after verification
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
if (operation === "create" && data?.active) {
|
|
672
|
+
const existingActive = await req.payload.find({
|
|
673
|
+
collection: slug,
|
|
674
|
+
where: {
|
|
675
|
+
active: {
|
|
676
|
+
equals: true
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
// Keep overrideAccess: true for admin operations
|
|
680
|
+
});
|
|
681
|
+
if (existingActive.docs.length > 0) {
|
|
682
|
+
for (const doc of existingActive.docs) {
|
|
683
|
+
await req.payload.update({
|
|
684
|
+
collection: slug,
|
|
685
|
+
id: doc.id,
|
|
686
|
+
data: {
|
|
687
|
+
active: false
|
|
688
|
+
}
|
|
689
|
+
// Keep overrideAccess: true for admin operations
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return data;
|
|
695
|
+
}
|
|
696
|
+
],
|
|
697
|
+
afterChange: [
|
|
698
|
+
async ({ doc, req }) => {
|
|
699
|
+
if (req.payload.newsletterEmailService && doc.active) {
|
|
700
|
+
try {
|
|
701
|
+
console.warn("Newsletter settings updated, reinitializing service...");
|
|
702
|
+
} catch {
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return doc;
|
|
706
|
+
}
|
|
707
|
+
]
|
|
708
|
+
},
|
|
709
|
+
access: {
|
|
710
|
+
read: () => true,
|
|
711
|
+
// Settings can be read publicly for validation
|
|
712
|
+
create: adminOnly(pluginConfig),
|
|
713
|
+
update: adminOnly(pluginConfig),
|
|
714
|
+
delete: adminOnly(pluginConfig)
|
|
715
|
+
},
|
|
716
|
+
timestamps: true
|
|
717
|
+
};
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
// src/providers/resend.ts
|
|
721
|
+
var import_resend = require("resend");
|
|
722
|
+
|
|
723
|
+
// src/providers/types.ts
|
|
724
|
+
var EmailProviderError = class extends Error {
|
|
725
|
+
constructor(message, provider, originalError) {
|
|
726
|
+
super(message);
|
|
727
|
+
this.name = "EmailProviderError";
|
|
728
|
+
this.provider = provider;
|
|
729
|
+
this.originalError = originalError;
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
// src/providers/resend.ts
|
|
734
|
+
var ResendProvider = class {
|
|
735
|
+
constructor(config) {
|
|
736
|
+
this.client = new import_resend.Resend(config.apiKey);
|
|
737
|
+
this.audienceIds = config.audienceIds || {};
|
|
738
|
+
this.fromAddress = config.fromAddress;
|
|
739
|
+
this.fromName = config.fromName;
|
|
740
|
+
this.isDevelopment = process.env.NODE_ENV !== "production";
|
|
741
|
+
}
|
|
742
|
+
getProvider() {
|
|
743
|
+
return "resend";
|
|
744
|
+
}
|
|
745
|
+
async send(params) {
|
|
746
|
+
try {
|
|
747
|
+
const from = params.from || {
|
|
748
|
+
email: this.fromAddress,
|
|
749
|
+
name: this.fromName
|
|
750
|
+
};
|
|
751
|
+
if (!params.html && !params.text) {
|
|
752
|
+
throw new Error("Either html or text content is required");
|
|
753
|
+
}
|
|
754
|
+
await this.client.emails.send({
|
|
755
|
+
from: `${from.name} <${from.email}>`,
|
|
756
|
+
to: Array.isArray(params.to) ? params.to : [params.to],
|
|
757
|
+
subject: params.subject,
|
|
758
|
+
html: params.html || "",
|
|
759
|
+
text: params.text,
|
|
760
|
+
replyTo: params.replyTo
|
|
761
|
+
});
|
|
762
|
+
} catch (error) {
|
|
763
|
+
throw new EmailProviderError(
|
|
764
|
+
`Failed to send email via Resend: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
765
|
+
"resend",
|
|
766
|
+
error
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
async addContact(contact) {
|
|
771
|
+
try {
|
|
772
|
+
const audienceId = this.getAudienceId(contact.locale);
|
|
773
|
+
if (!audienceId) {
|
|
774
|
+
console.warn(`No audience ID configured for locale: ${contact.locale}`);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
await this.client.contacts.create({
|
|
778
|
+
email: contact.email,
|
|
779
|
+
firstName: contact.name?.split(" ")[0],
|
|
780
|
+
lastName: contact.name?.split(" ").slice(1).join(" "),
|
|
781
|
+
unsubscribed: contact.subscriptionStatus === "unsubscribed",
|
|
782
|
+
audienceId
|
|
783
|
+
});
|
|
784
|
+
} catch (error) {
|
|
785
|
+
throw new EmailProviderError(
|
|
786
|
+
`Failed to add contact to Resend: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
787
|
+
"resend",
|
|
788
|
+
error
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
async updateContact(contact) {
|
|
793
|
+
try {
|
|
794
|
+
const audienceId = this.getAudienceId(contact.locale);
|
|
795
|
+
if (!audienceId) {
|
|
796
|
+
console.warn(`No audience ID configured for locale: ${contact.locale}`);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const contacts = await this.client.contacts.list({ audienceId });
|
|
800
|
+
const existingContact = contacts.data?.data?.find((c) => c.email === contact.email);
|
|
801
|
+
if (existingContact) {
|
|
802
|
+
await this.client.contacts.update({
|
|
803
|
+
id: existingContact.id,
|
|
804
|
+
audienceId,
|
|
805
|
+
firstName: contact.name?.split(" ")[0],
|
|
806
|
+
lastName: contact.name?.split(" ").slice(1).join(" "),
|
|
807
|
+
unsubscribed: contact.subscriptionStatus === "unsubscribed"
|
|
808
|
+
});
|
|
809
|
+
} else {
|
|
810
|
+
await this.addContact(contact);
|
|
811
|
+
}
|
|
812
|
+
} catch (error) {
|
|
813
|
+
throw new EmailProviderError(
|
|
814
|
+
`Failed to update contact in Resend: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
815
|
+
"resend",
|
|
816
|
+
error
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async removeContact(email) {
|
|
821
|
+
try {
|
|
822
|
+
for (const locale in this.audienceIds) {
|
|
823
|
+
const audienceId = this.getAudienceId(locale);
|
|
824
|
+
if (!audienceId) continue;
|
|
825
|
+
const contacts = await this.client.contacts.list({ audienceId });
|
|
826
|
+
const contact = contacts.data?.data?.find((c) => c.email === email);
|
|
827
|
+
if (contact) {
|
|
828
|
+
await this.client.contacts.update({
|
|
829
|
+
id: contact.id,
|
|
830
|
+
audienceId,
|
|
831
|
+
unsubscribed: true
|
|
832
|
+
});
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
} catch (error) {
|
|
837
|
+
throw new EmailProviderError(
|
|
838
|
+
`Failed to remove contact from Resend: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
839
|
+
"resend",
|
|
840
|
+
error
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
getAudienceId(locale) {
|
|
845
|
+
const localeKey = locale || "en";
|
|
846
|
+
if (!this.audienceIds) return void 0;
|
|
847
|
+
const localeConfig = this.audienceIds[localeKey];
|
|
848
|
+
if (!localeConfig) return void 0;
|
|
849
|
+
const audienceId = this.isDevelopment ? localeConfig.development || localeConfig.production : localeConfig.production || localeConfig.development;
|
|
850
|
+
return audienceId;
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// src/providers/broadcast.ts
|
|
855
|
+
var BroadcastProvider = class {
|
|
856
|
+
constructor(config) {
|
|
857
|
+
this.apiUrl = config.apiUrl.replace(/\/$/, "");
|
|
858
|
+
this.isDevelopment = process.env.NODE_ENV !== "production";
|
|
859
|
+
this.token = this.isDevelopment ? config.tokens.development || config.tokens.production || "" : config.tokens.production || config.tokens.development || "";
|
|
860
|
+
this.fromAddress = config.fromAddress;
|
|
861
|
+
this.fromName = config.fromName;
|
|
862
|
+
}
|
|
863
|
+
getProvider() {
|
|
864
|
+
return "broadcast";
|
|
865
|
+
}
|
|
866
|
+
async send(params) {
|
|
867
|
+
try {
|
|
868
|
+
const from = params.from || {
|
|
869
|
+
email: this.fromAddress,
|
|
870
|
+
name: this.fromName
|
|
871
|
+
};
|
|
872
|
+
const recipients = Array.isArray(params.to) ? params.to : [params.to];
|
|
873
|
+
const response = await fetch(`${this.apiUrl}/api/v1/emails`, {
|
|
874
|
+
method: "POST",
|
|
875
|
+
headers: {
|
|
876
|
+
"Authorization": `Bearer ${this.token}`,
|
|
877
|
+
"Content-Type": "application/json"
|
|
878
|
+
},
|
|
879
|
+
body: JSON.stringify({
|
|
880
|
+
from_email: from.email,
|
|
881
|
+
from_name: from.name,
|
|
882
|
+
to: recipients,
|
|
883
|
+
subject: params.subject,
|
|
884
|
+
html_body: params.html,
|
|
885
|
+
text_body: params.text,
|
|
886
|
+
reply_to: params.replyTo
|
|
887
|
+
})
|
|
888
|
+
});
|
|
889
|
+
if (!response.ok) {
|
|
890
|
+
const error = await response.text();
|
|
891
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
892
|
+
}
|
|
893
|
+
} catch (error) {
|
|
894
|
+
throw new EmailProviderError(
|
|
895
|
+
`Failed to send email via Broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
896
|
+
"broadcast",
|
|
897
|
+
error
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
async addContact(contact) {
|
|
902
|
+
try {
|
|
903
|
+
const response = await fetch(`${this.apiUrl}/api/v1/contacts`, {
|
|
904
|
+
method: "POST",
|
|
905
|
+
headers: {
|
|
906
|
+
"Authorization": `Bearer ${this.token}`,
|
|
907
|
+
"Content-Type": "application/json"
|
|
908
|
+
},
|
|
909
|
+
body: JSON.stringify({
|
|
910
|
+
email: contact.email,
|
|
911
|
+
name: contact.name,
|
|
912
|
+
status: contact.subscriptionStatus === "active" ? "subscribed" : "unsubscribed",
|
|
913
|
+
metadata: {
|
|
914
|
+
locale: contact.locale,
|
|
915
|
+
source: contact.source,
|
|
916
|
+
...contact.utmParameters
|
|
917
|
+
}
|
|
918
|
+
})
|
|
919
|
+
});
|
|
920
|
+
if (!response.ok) {
|
|
921
|
+
const error = await response.text();
|
|
922
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
923
|
+
}
|
|
924
|
+
} catch (error) {
|
|
925
|
+
throw new EmailProviderError(
|
|
926
|
+
`Failed to add contact to Broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
927
|
+
"broadcast",
|
|
928
|
+
error
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
async updateContact(contact) {
|
|
933
|
+
try {
|
|
934
|
+
const searchResponse = await fetch(
|
|
935
|
+
`${this.apiUrl}/api/v1/contacts?email=${encodeURIComponent(contact.email)}`,
|
|
936
|
+
{
|
|
937
|
+
headers: {
|
|
938
|
+
"Authorization": `Bearer ${this.token}`
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
);
|
|
942
|
+
if (!searchResponse.ok) {
|
|
943
|
+
await this.addContact(contact);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
const contacts = await searchResponse.json();
|
|
947
|
+
const existingContact = contacts.data?.[0];
|
|
948
|
+
if (!existingContact) {
|
|
949
|
+
await this.addContact(contact);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const response = await fetch(`${this.apiUrl}/api/v1/contacts/${existingContact.id}`, {
|
|
953
|
+
method: "PUT",
|
|
954
|
+
headers: {
|
|
955
|
+
"Authorization": `Bearer ${this.token}`,
|
|
956
|
+
"Content-Type": "application/json"
|
|
957
|
+
},
|
|
958
|
+
body: JSON.stringify({
|
|
959
|
+
email: contact.email,
|
|
960
|
+
name: contact.name,
|
|
961
|
+
status: contact.subscriptionStatus === "active" ? "subscribed" : "unsubscribed",
|
|
962
|
+
metadata: {
|
|
963
|
+
locale: contact.locale,
|
|
964
|
+
source: contact.source,
|
|
965
|
+
...contact.utmParameters
|
|
966
|
+
}
|
|
967
|
+
})
|
|
968
|
+
});
|
|
969
|
+
if (!response.ok) {
|
|
970
|
+
const error = await response.text();
|
|
971
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
972
|
+
}
|
|
973
|
+
} catch (error) {
|
|
974
|
+
throw new EmailProviderError(
|
|
975
|
+
`Failed to update contact in Broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
976
|
+
"broadcast",
|
|
977
|
+
error
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
async removeContact(email) {
|
|
982
|
+
try {
|
|
983
|
+
const searchResponse = await fetch(
|
|
984
|
+
`${this.apiUrl}/api/v1/contacts?email=${encodeURIComponent(email)}`,
|
|
985
|
+
{
|
|
986
|
+
headers: {
|
|
987
|
+
"Authorization": `Bearer ${this.token}`
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
);
|
|
991
|
+
if (!searchResponse.ok) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const contacts = await searchResponse.json();
|
|
995
|
+
const contact = contacts.data?.[0];
|
|
996
|
+
if (!contact) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
const response = await fetch(`${this.apiUrl}/api/v1/contacts/${contact.id}`, {
|
|
1000
|
+
method: "DELETE",
|
|
1001
|
+
headers: {
|
|
1002
|
+
"Authorization": `Bearer ${this.token}`
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
if (!response.ok) {
|
|
1006
|
+
const error = await response.text();
|
|
1007
|
+
throw new Error(`Broadcast API error: ${response.status} - ${error}`);
|
|
1008
|
+
}
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
throw new EmailProviderError(
|
|
1011
|
+
`Failed to remove contact from Broadcast: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
1012
|
+
"broadcast",
|
|
1013
|
+
error
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
// src/providers/index.ts
|
|
1020
|
+
var EmailService = class {
|
|
1021
|
+
constructor(config) {
|
|
1022
|
+
this.provider = this.createProvider(config);
|
|
1023
|
+
}
|
|
1024
|
+
createProvider(config) {
|
|
1025
|
+
const baseConfig = {
|
|
1026
|
+
fromAddress: config.fromAddress,
|
|
1027
|
+
fromName: config.fromName
|
|
1028
|
+
};
|
|
1029
|
+
switch (config.provider) {
|
|
1030
|
+
case "resend":
|
|
1031
|
+
if (!config.resend) {
|
|
1032
|
+
throw new Error("Resend configuration is required when using Resend provider");
|
|
1033
|
+
}
|
|
1034
|
+
return new ResendProvider({
|
|
1035
|
+
...config.resend,
|
|
1036
|
+
...baseConfig
|
|
1037
|
+
});
|
|
1038
|
+
case "broadcast":
|
|
1039
|
+
if (!config.broadcast) {
|
|
1040
|
+
throw new Error("Broadcast configuration is required when using Broadcast provider");
|
|
1041
|
+
}
|
|
1042
|
+
return new BroadcastProvider({
|
|
1043
|
+
...config.broadcast,
|
|
1044
|
+
...baseConfig
|
|
1045
|
+
});
|
|
1046
|
+
default:
|
|
1047
|
+
throw new Error(`Unknown email provider: ${config.provider}`);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
async send(params) {
|
|
1051
|
+
return this.provider.send(params);
|
|
1052
|
+
}
|
|
1053
|
+
async addContact(contact) {
|
|
1054
|
+
return this.provider.addContact(contact);
|
|
1055
|
+
}
|
|
1056
|
+
async updateContact(contact) {
|
|
1057
|
+
return this.provider.updateContact(contact);
|
|
1058
|
+
}
|
|
1059
|
+
async removeContact(email) {
|
|
1060
|
+
return this.provider.removeContact(email);
|
|
1061
|
+
}
|
|
1062
|
+
getProvider() {
|
|
1063
|
+
return this.provider.getProvider();
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Update the provider configuration
|
|
1067
|
+
* Useful when settings are changed in the admin UI
|
|
1068
|
+
*/
|
|
1069
|
+
updateConfig(config) {
|
|
1070
|
+
this.provider = this.createProvider(config);
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
function createEmailService(config) {
|
|
1074
|
+
return new EmailService(config);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// src/utils/validation.ts
|
|
1078
|
+
var import_isomorphic_dompurify = __toESM(require("isomorphic-dompurify"), 1);
|
|
1079
|
+
function isValidEmail(email) {
|
|
1080
|
+
if (!email || typeof email !== "string") return false;
|
|
1081
|
+
const trimmed = email.trim();
|
|
1082
|
+
if (trimmed.length > 255) return false;
|
|
1083
|
+
if (trimmed.includes("<") || trimmed.includes(">")) return false;
|
|
1084
|
+
if (trimmed.includes("javascript:")) return false;
|
|
1085
|
+
if (trimmed.includes("data:")) return false;
|
|
1086
|
+
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
1087
|
+
if (!emailRegex.test(trimmed)) return false;
|
|
1088
|
+
const parts = trimmed.split("@");
|
|
1089
|
+
if (parts.length !== 2) return false;
|
|
1090
|
+
const [localPart, domain] = parts;
|
|
1091
|
+
if (localPart.length > 64 || localPart.length === 0) return false;
|
|
1092
|
+
if (localPart.startsWith(".") || localPart.endsWith(".")) return false;
|
|
1093
|
+
if (domain.startsWith(".") || domain.endsWith(".")) return false;
|
|
1094
|
+
if (domain.includes("..")) return false;
|
|
1095
|
+
if (localPart.includes("..")) return false;
|
|
1096
|
+
return true;
|
|
1097
|
+
}
|
|
1098
|
+
function isDomainAllowed(email, allowedDomains) {
|
|
1099
|
+
if (!isValidEmail(email)) {
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
if (!allowedDomains || allowedDomains.length === 0) {
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
1105
|
+
const domain = email.split("@")[1]?.toLowerCase();
|
|
1106
|
+
if (!domain) return false;
|
|
1107
|
+
return allowedDomains.some(
|
|
1108
|
+
(allowedDomain) => domain === allowedDomain.toLowerCase()
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
function sanitizeInput(input) {
|
|
1112
|
+
if (!input) return "";
|
|
1113
|
+
let cleaned = import_isomorphic_dompurify.default.sanitize(input, {
|
|
1114
|
+
ALLOWED_TAGS: [],
|
|
1115
|
+
ALLOWED_ATTR: [],
|
|
1116
|
+
KEEP_CONTENT: true
|
|
1117
|
+
});
|
|
1118
|
+
cleaned = cleaned.replace(/javascript:/gi, "").replace(/data:/gi, "").replace(/vbscript:/gi, "").replace(/file:\/\//gi, "").replace(/onload/gi, "").replace(/onerror/gi, "").replace(/onclick/gi, "").replace(/onmouseover/gi, "").replace(/alert\(/gi, "").replace(/prompt\(/gi, "").replace(/confirm\(/gi, "").replace(/\|/g, "").replace(/;/g, "").replace(/`/g, "").replace(/&&/g, "").replace(/\$\(/g, "").replace(/\.\./g, "").replace(/\/..\//g, "").replace(/\0/g, "");
|
|
1119
|
+
return cleaned.trim();
|
|
1120
|
+
}
|
|
1121
|
+
function extractUTMParams(searchParams) {
|
|
1122
|
+
const utmParams = {};
|
|
1123
|
+
const utmKeys = ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"];
|
|
1124
|
+
utmKeys.forEach((key) => {
|
|
1125
|
+
const value = searchParams.get(key);
|
|
1126
|
+
if (value) {
|
|
1127
|
+
const shortKey = key.replace("utm_", "");
|
|
1128
|
+
utmParams[shortKey] = value;
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
return utmParams;
|
|
1132
|
+
}
|
|
1133
|
+
function isValidSource(source) {
|
|
1134
|
+
if (!source || typeof source !== "string") return false;
|
|
1135
|
+
const allowedSources = [
|
|
1136
|
+
"website",
|
|
1137
|
+
"api",
|
|
1138
|
+
"import",
|
|
1139
|
+
"admin",
|
|
1140
|
+
"signup-form",
|
|
1141
|
+
"magic-link",
|
|
1142
|
+
"preferences",
|
|
1143
|
+
"external"
|
|
1144
|
+
];
|
|
1145
|
+
return allowedSources.includes(source);
|
|
1146
|
+
}
|
|
1147
|
+
function validateSubscriberData(data) {
|
|
1148
|
+
const errors = [];
|
|
1149
|
+
if (!data.email) {
|
|
1150
|
+
errors.push("Email is required");
|
|
1151
|
+
} else if (!isValidEmail(data.email)) {
|
|
1152
|
+
errors.push("Invalid email format");
|
|
1153
|
+
}
|
|
1154
|
+
if (data.name && data.name.length > 100) {
|
|
1155
|
+
errors.push("Name is too long (max 100 characters)");
|
|
1156
|
+
}
|
|
1157
|
+
if (data.source !== void 0) {
|
|
1158
|
+
if (!data.source || data.source.length === 0) {
|
|
1159
|
+
errors.push("Source cannot be empty");
|
|
1160
|
+
} else if (data.source.length > 50) {
|
|
1161
|
+
errors.push("Source is too long (max 50 characters)");
|
|
1162
|
+
} else if (!isValidSource(data.source)) {
|
|
1163
|
+
errors.push("Invalid source value");
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return {
|
|
1167
|
+
valid: errors.length === 0,
|
|
1168
|
+
errors
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/endpoints/subscribe.ts
|
|
1173
|
+
var createSubscribeEndpoint = (config) => {
|
|
1174
|
+
return {
|
|
1175
|
+
path: "/newsletter/subscribe",
|
|
1176
|
+
method: "post",
|
|
1177
|
+
handler: async (req, res) => {
|
|
1178
|
+
try {
|
|
1179
|
+
const {
|
|
1180
|
+
email,
|
|
1181
|
+
name,
|
|
1182
|
+
source,
|
|
1183
|
+
preferences,
|
|
1184
|
+
leadMagnet,
|
|
1185
|
+
surveyResponses,
|
|
1186
|
+
metadata = {}
|
|
1187
|
+
} = req.body;
|
|
1188
|
+
const trimmedEmail = email?.trim();
|
|
1189
|
+
const validation = validateSubscriberData({ email: trimmedEmail, name, source });
|
|
1190
|
+
if (!validation.valid) {
|
|
1191
|
+
return res.status(400).json({
|
|
1192
|
+
success: false,
|
|
1193
|
+
errors: validation.errors
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
const settingsResult = await req.payload.find({
|
|
1197
|
+
collection: config.settingsSlug || "newsletter-settings",
|
|
1198
|
+
where: {
|
|
1199
|
+
active: {
|
|
1200
|
+
equals: true
|
|
1201
|
+
}
|
|
1202
|
+
},
|
|
1203
|
+
limit: 1,
|
|
1204
|
+
overrideAccess: false
|
|
1205
|
+
// No user context for public endpoint
|
|
1206
|
+
});
|
|
1207
|
+
const settings = settingsResult.docs[0];
|
|
1208
|
+
const allowedDomains = settings?.subscriptionSettings?.allowedDomains?.map((d) => d.domain) || [];
|
|
1209
|
+
if (!isDomainAllowed(trimmedEmail, allowedDomains)) {
|
|
1210
|
+
return res.status(400).json({
|
|
1211
|
+
success: false,
|
|
1212
|
+
error: "Email domain not allowed"
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
const existing = await req.payload.find({
|
|
1216
|
+
collection: config.subscribersSlug || "subscribers",
|
|
1217
|
+
where: {
|
|
1218
|
+
email: {
|
|
1219
|
+
equals: trimmedEmail.toLowerCase()
|
|
1220
|
+
}
|
|
1221
|
+
},
|
|
1222
|
+
overrideAccess: true
|
|
1223
|
+
// Need to check for duplicates in public endpoint
|
|
1224
|
+
});
|
|
1225
|
+
if (existing.docs.length > 0) {
|
|
1226
|
+
const subscriber2 = existing.docs[0];
|
|
1227
|
+
if (subscriber2.subscriptionStatus === "unsubscribed") {
|
|
1228
|
+
return res.status(400).json({
|
|
1229
|
+
success: false,
|
|
1230
|
+
error: "This email has been unsubscribed. Please contact support to resubscribe."
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
return res.status(400).json({
|
|
1234
|
+
success: false,
|
|
1235
|
+
error: "Already subscribed",
|
|
1236
|
+
subscriber: {
|
|
1237
|
+
id: subscriber2.id,
|
|
1238
|
+
email: subscriber2.email,
|
|
1239
|
+
subscriptionStatus: subscriber2.subscriptionStatus
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
const ipAddress = req.ip || req.connection.remoteAddress;
|
|
1244
|
+
const maxPerIP = settings?.subscriptionSettings?.maxSubscribersPerIP || 10;
|
|
1245
|
+
const ipSubscribers = await req.payload.find({
|
|
1246
|
+
collection: config.subscribersSlug || "subscribers",
|
|
1247
|
+
where: {
|
|
1248
|
+
"signupMetadata.ipAddress": {
|
|
1249
|
+
equals: ipAddress
|
|
1250
|
+
}
|
|
1251
|
+
},
|
|
1252
|
+
overrideAccess: true
|
|
1253
|
+
// Need to check IP limits in public endpoint
|
|
1254
|
+
});
|
|
1255
|
+
if (ipSubscribers.docs.length >= maxPerIP) {
|
|
1256
|
+
return res.status(429).json({
|
|
1257
|
+
success: false,
|
|
1258
|
+
error: "Too many subscriptions from this IP address"
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
const referer = req.headers.referer || req.headers.referrer || "";
|
|
1262
|
+
let utmParams = {};
|
|
1263
|
+
if (referer) {
|
|
1264
|
+
try {
|
|
1265
|
+
utmParams = extractUTMParams(new URL(referer).searchParams);
|
|
1266
|
+
} catch {
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
const subscriberData = {
|
|
1270
|
+
email: trimmedEmail.toLowerCase(),
|
|
1271
|
+
name: name ? sanitizeInput(name) : void 0,
|
|
1272
|
+
locale: metadata.locale || config.i18n?.defaultLocale || "en",
|
|
1273
|
+
subscriptionStatus: settings?.subscriptionSettings?.requireDoubleOptIn ? "pending" : "active",
|
|
1274
|
+
source: source || "api",
|
|
1275
|
+
emailPreferences: {
|
|
1276
|
+
newsletter: true,
|
|
1277
|
+
announcements: true,
|
|
1278
|
+
...preferences || {}
|
|
1279
|
+
},
|
|
1280
|
+
signupMetadata: {
|
|
1281
|
+
ipAddress,
|
|
1282
|
+
userAgent: req.headers["user-agent"],
|
|
1283
|
+
referrer: referer,
|
|
1284
|
+
signupPage: metadata.signupPage || referer
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
if (config.features?.utmTracking?.enabled && Object.keys(utmParams).length > 0) {
|
|
1288
|
+
subscriberData.utmParameters = utmParams;
|
|
1289
|
+
}
|
|
1290
|
+
if (config.features?.leadMagnets?.enabled && leadMagnet) {
|
|
1291
|
+
subscriberData.leadMagnet = leadMagnet;
|
|
1292
|
+
}
|
|
1293
|
+
const subscriber = await req.payload.create({
|
|
1294
|
+
collection: config.subscribersSlug || "subscribers",
|
|
1295
|
+
data: subscriberData,
|
|
1296
|
+
overrideAccess: true
|
|
1297
|
+
// Public endpoint needs to create subscribers
|
|
1298
|
+
});
|
|
1299
|
+
if (config.features?.surveys?.enabled && surveyResponses) {
|
|
1300
|
+
}
|
|
1301
|
+
if (settings?.subscriptionSettings?.requireDoubleOptIn) {
|
|
1302
|
+
}
|
|
1303
|
+
res.json({
|
|
1304
|
+
success: true,
|
|
1305
|
+
subscriber: {
|
|
1306
|
+
id: subscriber.id,
|
|
1307
|
+
email: subscriber.email,
|
|
1308
|
+
subscriptionStatus: subscriber.subscriptionStatus
|
|
1309
|
+
},
|
|
1310
|
+
message: settings?.subscriptionSettings?.requireDoubleOptIn ? "Please check your email to confirm your subscription" : "Successfully subscribed"
|
|
1311
|
+
});
|
|
1312
|
+
} catch {
|
|
1313
|
+
res.status(500).json({
|
|
1314
|
+
success: false,
|
|
1315
|
+
error: "Failed to subscribe. Please try again."
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
};
|
|
1321
|
+
|
|
1322
|
+
// src/utils/jwt.ts
|
|
1323
|
+
var import_jsonwebtoken = __toESM(require("jsonwebtoken"), 1);
|
|
1324
|
+
function getJWTSecret() {
|
|
1325
|
+
const secret = process.env.JWT_SECRET || process.env.PAYLOAD_SECRET;
|
|
1326
|
+
if (!secret) {
|
|
1327
|
+
console.warn(
|
|
1328
|
+
"WARNING: No JWT_SECRET or PAYLOAD_SECRET found in environment variables. Magic link authentication will not work properly. Please set JWT_SECRET in your environment."
|
|
1329
|
+
);
|
|
1330
|
+
return "INSECURE_DEVELOPMENT_SECRET_PLEASE_SET_JWT_SECRET";
|
|
1331
|
+
}
|
|
1332
|
+
return secret;
|
|
1333
|
+
}
|
|
1334
|
+
function verifyMagicLinkToken(token) {
|
|
1335
|
+
try {
|
|
1336
|
+
const payload = import_jsonwebtoken.default.verify(token, getJWTSecret(), {
|
|
1337
|
+
issuer: "payload-newsletter-plugin"
|
|
1338
|
+
});
|
|
1339
|
+
if (payload.type !== "magic-link") {
|
|
1340
|
+
throw new Error("Invalid token type");
|
|
1341
|
+
}
|
|
1342
|
+
return payload;
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
if (error instanceof Error && error.name === "TokenExpiredError") {
|
|
1345
|
+
throw new Error("Magic link has expired. Please request a new one.");
|
|
1346
|
+
}
|
|
1347
|
+
if (error instanceof Error && error.name === "JsonWebTokenError") {
|
|
1348
|
+
throw new Error("Invalid magic link token");
|
|
1349
|
+
}
|
|
1350
|
+
throw error;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
function generateSessionToken(subscriberId, email) {
|
|
1354
|
+
const payload = {
|
|
1355
|
+
subscriberId,
|
|
1356
|
+
email,
|
|
1357
|
+
type: "session"
|
|
1358
|
+
};
|
|
1359
|
+
return import_jsonwebtoken.default.sign(payload, getJWTSecret(), {
|
|
1360
|
+
expiresIn: "30d",
|
|
1361
|
+
issuer: "payload-newsletter-plugin"
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
function verifySessionToken(token) {
|
|
1365
|
+
try {
|
|
1366
|
+
const payload = import_jsonwebtoken.default.verify(token, getJWTSecret(), {
|
|
1367
|
+
issuer: "payload-newsletter-plugin"
|
|
1368
|
+
});
|
|
1369
|
+
if (payload.type !== "session") {
|
|
1370
|
+
throw new Error("Invalid token type");
|
|
1371
|
+
}
|
|
1372
|
+
return payload;
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
if (error instanceof Error && error.name === "TokenExpiredError") {
|
|
1375
|
+
throw new Error("Session has expired. Please sign in again.");
|
|
1376
|
+
}
|
|
1377
|
+
if (error instanceof Error && error.name === "JsonWebTokenError") {
|
|
1378
|
+
throw new Error("Invalid session token");
|
|
1379
|
+
}
|
|
1380
|
+
throw error;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// src/endpoints/verify-magic-link.ts
|
|
1385
|
+
var createVerifyMagicLinkEndpoint = (config) => {
|
|
1386
|
+
return {
|
|
1387
|
+
path: "/newsletter/verify-magic-link",
|
|
1388
|
+
method: "post",
|
|
1389
|
+
handler: async (req, res) => {
|
|
1390
|
+
try {
|
|
1391
|
+
const { token } = req.body;
|
|
1392
|
+
if (!token) {
|
|
1393
|
+
return res.status(400).json({
|
|
1394
|
+
success: false,
|
|
1395
|
+
error: "Token is required"
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
let payload;
|
|
1399
|
+
try {
|
|
1400
|
+
payload = verifyMagicLinkToken(token);
|
|
1401
|
+
} catch (error) {
|
|
1402
|
+
return res.status(401).json({
|
|
1403
|
+
success: false,
|
|
1404
|
+
error: error instanceof Error ? error.message : "Invalid token"
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
const subscriber = await req.payload.findByID({
|
|
1408
|
+
collection: config.subscribersSlug || "subscribers",
|
|
1409
|
+
id: payload.subscriberId
|
|
1410
|
+
// Keep overrideAccess: true for token verification
|
|
1411
|
+
});
|
|
1412
|
+
if (!subscriber) {
|
|
1413
|
+
return res.status(404).json({
|
|
1414
|
+
success: false,
|
|
1415
|
+
error: "Subscriber not found"
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
if (subscriber.email !== payload.email) {
|
|
1419
|
+
return res.status(401).json({
|
|
1420
|
+
success: false,
|
|
1421
|
+
error: "Invalid token"
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
if (subscriber.subscriptionStatus === "unsubscribed") {
|
|
1425
|
+
return res.status(403).json({
|
|
1426
|
+
success: false,
|
|
1427
|
+
error: "This email has been unsubscribed"
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
const syntheticUser = {
|
|
1431
|
+
collection: "subscribers",
|
|
1432
|
+
id: subscriber.id,
|
|
1433
|
+
email: subscriber.email
|
|
1434
|
+
};
|
|
1435
|
+
if (subscriber.subscriptionStatus === "pending") {
|
|
1436
|
+
await req.payload.update({
|
|
1437
|
+
collection: config.subscribersSlug || "subscribers",
|
|
1438
|
+
id: subscriber.id,
|
|
1439
|
+
data: {
|
|
1440
|
+
subscriptionStatus: "active"
|
|
1441
|
+
},
|
|
1442
|
+
overrideAccess: false,
|
|
1443
|
+
user: syntheticUser
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
await req.payload.update({
|
|
1447
|
+
collection: config.subscribersSlug || "subscribers",
|
|
1448
|
+
id: subscriber.id,
|
|
1449
|
+
data: {
|
|
1450
|
+
magicLinkToken: null,
|
|
1451
|
+
magicLinkTokenExpiry: null
|
|
1452
|
+
},
|
|
1453
|
+
overrideAccess: false,
|
|
1454
|
+
user: syntheticUser
|
|
1455
|
+
});
|
|
1456
|
+
const sessionToken = generateSessionToken(
|
|
1457
|
+
String(subscriber.id),
|
|
1458
|
+
subscriber.email
|
|
1459
|
+
);
|
|
1460
|
+
res.json({
|
|
1461
|
+
success: true,
|
|
1462
|
+
sessionToken,
|
|
1463
|
+
subscriber: {
|
|
1464
|
+
id: subscriber.id,
|
|
1465
|
+
email: subscriber.email,
|
|
1466
|
+
name: subscriber.name,
|
|
1467
|
+
locale: subscriber.locale,
|
|
1468
|
+
emailPreferences: subscriber.emailPreferences
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
console.error("Verify magic link error:", error);
|
|
1473
|
+
res.status(500).json({
|
|
1474
|
+
success: false,
|
|
1475
|
+
error: "Failed to verify magic link"
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
};
|
|
1481
|
+
|
|
1482
|
+
// src/endpoints/preferences.ts
|
|
1483
|
+
var createPreferencesEndpoint = (config) => {
|
|
1484
|
+
return {
|
|
1485
|
+
path: "/newsletter/preferences",
|
|
1486
|
+
method: "get",
|
|
1487
|
+
handler: async (req, res) => {
|
|
1488
|
+
try {
|
|
1489
|
+
const authHeader = req.headers.authorization;
|
|
1490
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
1491
|
+
return res.status(401).json({
|
|
1492
|
+
success: false,
|
|
1493
|
+
error: "Authorization required"
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
const token = authHeader.substring(7);
|
|
1497
|
+
let payload;
|
|
1498
|
+
try {
|
|
1499
|
+
payload = verifySessionToken(token);
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
return res.status(401).json({
|
|
1502
|
+
success: false,
|
|
1503
|
+
error: error instanceof Error ? error.message : "Invalid token"
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
const subscriber = await req.payload.findByID({
|
|
1507
|
+
collection: config.subscribersSlug || "subscribers",
|
|
1508
|
+
id: payload.subscriberId,
|
|
1509
|
+
overrideAccess: false,
|
|
1510
|
+
user: {
|
|
1511
|
+
collection: "subscribers",
|
|
1512
|
+
id: payload.subscriberId,
|
|
1513
|
+
email: payload.email
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
if (!subscriber) {
|
|
1517
|
+
return res.status(404).json({
|
|
1518
|
+
success: false,
|
|
1519
|
+
error: "Subscriber not found"
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
res.json({
|
|
1523
|
+
success: true,
|
|
1524
|
+
subscriber: {
|
|
1525
|
+
id: subscriber.id,
|
|
1526
|
+
email: subscriber.email,
|
|
1527
|
+
name: subscriber.name,
|
|
1528
|
+
locale: subscriber.locale,
|
|
1529
|
+
emailPreferences: subscriber.emailPreferences,
|
|
1530
|
+
subscriptionStatus: subscriber.subscriptionStatus
|
|
1531
|
+
}
|
|
1532
|
+
});
|
|
1533
|
+
} catch (error) {
|
|
1534
|
+
console.error("Get preferences error:", error);
|
|
1535
|
+
res.status(500).json({
|
|
1536
|
+
success: false,
|
|
1537
|
+
error: "Failed to get preferences"
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
};
|
|
1543
|
+
var createUpdatePreferencesEndpoint = (config) => {
|
|
1544
|
+
return {
|
|
1545
|
+
path: "/newsletter/preferences",
|
|
1546
|
+
method: "post",
|
|
1547
|
+
handler: async (req, res) => {
|
|
1548
|
+
try {
|
|
1549
|
+
const authHeader = req.headers.authorization;
|
|
1550
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
1551
|
+
return res.status(401).json({
|
|
1552
|
+
success: false,
|
|
1553
|
+
error: "Authorization required"
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
const token = authHeader.substring(7);
|
|
1557
|
+
let payload;
|
|
1558
|
+
try {
|
|
1559
|
+
payload = verifySessionToken(token);
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
return res.status(401).json({
|
|
1562
|
+
success: false,
|
|
1563
|
+
error: error instanceof Error ? error.message : "Invalid token"
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
const { name, locale, emailPreferences } = req.body;
|
|
1567
|
+
const updateData = {};
|
|
1568
|
+
if (name !== void 0) {
|
|
1569
|
+
updateData.name = name;
|
|
1570
|
+
}
|
|
1571
|
+
if (locale !== void 0) {
|
|
1572
|
+
updateData.locale = locale;
|
|
1573
|
+
}
|
|
1574
|
+
if (emailPreferences !== void 0) {
|
|
1575
|
+
updateData.emailPreferences = emailPreferences;
|
|
1576
|
+
}
|
|
1577
|
+
const subscriber = await req.payload.update({
|
|
1578
|
+
collection: config.subscribersSlug || "subscribers",
|
|
1579
|
+
id: payload.subscriberId,
|
|
1580
|
+
data: updateData,
|
|
1581
|
+
overrideAccess: false,
|
|
1582
|
+
user: {
|
|
1583
|
+
collection: "subscribers",
|
|
1584
|
+
id: payload.subscriberId,
|
|
1585
|
+
email: payload.email
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
res.json({
|
|
1589
|
+
success: true,
|
|
1590
|
+
subscriber: {
|
|
1591
|
+
id: subscriber.id,
|
|
1592
|
+
email: subscriber.email,
|
|
1593
|
+
name: subscriber.name,
|
|
1594
|
+
locale: subscriber.locale,
|
|
1595
|
+
emailPreferences: subscriber.emailPreferences,
|
|
1596
|
+
subscriptionStatus: subscriber.subscriptionStatus
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
} catch (error) {
|
|
1600
|
+
console.error("Update preferences error:", error);
|
|
1601
|
+
res.status(500).json({
|
|
1602
|
+
success: false,
|
|
1603
|
+
error: "Failed to update preferences"
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
// src/endpoints/unsubscribe.ts
|
|
1611
|
+
var createUnsubscribeEndpoint = (config) => {
|
|
1612
|
+
return {
|
|
1613
|
+
path: "/newsletter/unsubscribe",
|
|
1614
|
+
method: "post",
|
|
1615
|
+
handler: async (req, res) => {
|
|
1616
|
+
try {
|
|
1617
|
+
const { email, token } = req.body;
|
|
1618
|
+
if (!email && !token) {
|
|
1619
|
+
return res.status(400).json({
|
|
1620
|
+
success: false,
|
|
1621
|
+
error: "Email or token is required"
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
let subscriber;
|
|
1625
|
+
if (token) {
|
|
1626
|
+
try {
|
|
1627
|
+
const jwt2 = await import("jsonwebtoken");
|
|
1628
|
+
const payload = jwt2.verify(
|
|
1629
|
+
token,
|
|
1630
|
+
process.env.JWT_SECRET || process.env.PAYLOAD_SECRET || ""
|
|
1631
|
+
);
|
|
1632
|
+
if (payload.type !== "unsubscribe") {
|
|
1633
|
+
throw new Error("Invalid token type");
|
|
1634
|
+
}
|
|
1635
|
+
subscriber = await req.payload.findByID({
|
|
1636
|
+
collection: config.subscribersSlug || "subscribers",
|
|
1637
|
+
id: payload.subscriberId
|
|
1638
|
+
});
|
|
1639
|
+
} catch {
|
|
1640
|
+
return res.status(401).json({
|
|
1641
|
+
success: false,
|
|
1642
|
+
error: "Invalid or expired unsubscribe link"
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
} else {
|
|
1646
|
+
if (!isValidEmail(email)) {
|
|
1647
|
+
return res.status(400).json({
|
|
1648
|
+
success: false,
|
|
1649
|
+
error: "Invalid email format"
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
const result = await req.payload.find({
|
|
1653
|
+
collection: config.subscribersSlug || "subscribers",
|
|
1654
|
+
where: {
|
|
1655
|
+
email: {
|
|
1656
|
+
equals: email.toLowerCase()
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
if (result.docs.length === 0) {
|
|
1661
|
+
return res.json({
|
|
1662
|
+
success: true,
|
|
1663
|
+
message: "If this email was subscribed, it has been unsubscribed."
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
subscriber = result.docs[0];
|
|
1667
|
+
}
|
|
1668
|
+
if (!subscriber) {
|
|
1669
|
+
return res.json({
|
|
1670
|
+
success: true,
|
|
1671
|
+
message: "If this email was subscribed, it has been unsubscribed."
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
if (subscriber.subscriptionStatus === "unsubscribed") {
|
|
1675
|
+
return res.json({
|
|
1676
|
+
success: true,
|
|
1677
|
+
message: "Already unsubscribed"
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
await req.payload.update({
|
|
1681
|
+
collection: config.subscribersSlug || "subscribers",
|
|
1682
|
+
id: subscriber.id,
|
|
1683
|
+
data: {
|
|
1684
|
+
subscriptionStatus: "unsubscribed",
|
|
1685
|
+
unsubscribedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1686
|
+
},
|
|
1687
|
+
overrideAccess: false,
|
|
1688
|
+
user: {
|
|
1689
|
+
collection: "subscribers",
|
|
1690
|
+
id: subscriber.id,
|
|
1691
|
+
email: subscriber.email
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
res.json({
|
|
1695
|
+
success: true,
|
|
1696
|
+
message: "Successfully unsubscribed"
|
|
1697
|
+
});
|
|
1698
|
+
} catch (error) {
|
|
1699
|
+
console.error("Unsubscribe error:", error);
|
|
1700
|
+
res.status(500).json({
|
|
1701
|
+
success: false,
|
|
1702
|
+
error: "Failed to unsubscribe. Please try again."
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
};
|
|
1707
|
+
};
|
|
1708
|
+
|
|
1709
|
+
// src/endpoints/index.ts
|
|
1710
|
+
function createNewsletterEndpoints(config) {
|
|
1711
|
+
const endpoints = [
|
|
1712
|
+
createSubscribeEndpoint(config),
|
|
1713
|
+
createUnsubscribeEndpoint(config)
|
|
1714
|
+
];
|
|
1715
|
+
if (config.auth?.enabled !== false) {
|
|
1716
|
+
endpoints.push(
|
|
1717
|
+
createVerifyMagicLinkEndpoint(config),
|
|
1718
|
+
createPreferencesEndpoint(config),
|
|
1719
|
+
createUpdatePreferencesEndpoint(config)
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
return endpoints;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// src/fields/newsletterScheduling.ts
|
|
1726
|
+
function createNewsletterSchedulingFields(config) {
|
|
1727
|
+
const groupName = config.features?.newsletterScheduling?.fields?.groupName || "newsletterScheduling";
|
|
1728
|
+
const contentField = config.features?.newsletterScheduling?.fields?.contentField || "content";
|
|
1729
|
+
const createMarkdownField = config.features?.newsletterScheduling?.fields?.createMarkdownField !== false;
|
|
1730
|
+
const fields = [
|
|
1731
|
+
{
|
|
1732
|
+
name: groupName,
|
|
1733
|
+
type: "group",
|
|
1734
|
+
label: "Newsletter Scheduling",
|
|
1735
|
+
admin: {
|
|
1736
|
+
condition: (data, { user }) => user?.collection === "users"
|
|
1737
|
+
// Only show for admin users
|
|
1738
|
+
},
|
|
1739
|
+
fields: [
|
|
1740
|
+
{
|
|
1741
|
+
name: "scheduled",
|
|
1742
|
+
type: "checkbox",
|
|
1743
|
+
label: "Schedule for Newsletter",
|
|
1744
|
+
defaultValue: false,
|
|
1745
|
+
admin: {
|
|
1746
|
+
description: "Schedule this content to be sent as a newsletter"
|
|
1747
|
+
}
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
name: "scheduledDate",
|
|
1751
|
+
type: "date",
|
|
1752
|
+
label: "Send Date",
|
|
1753
|
+
required: true,
|
|
1754
|
+
admin: {
|
|
1755
|
+
date: {
|
|
1756
|
+
pickerAppearance: "dayAndTime"
|
|
1757
|
+
},
|
|
1758
|
+
condition: (data) => data?.[groupName]?.scheduled,
|
|
1759
|
+
description: "When to send this newsletter"
|
|
1760
|
+
}
|
|
1761
|
+
},
|
|
1762
|
+
{
|
|
1763
|
+
name: "sentDate",
|
|
1764
|
+
type: "date",
|
|
1765
|
+
label: "Sent Date",
|
|
1766
|
+
admin: {
|
|
1767
|
+
readOnly: true,
|
|
1768
|
+
condition: (data) => data?.[groupName]?.sendStatus === "sent",
|
|
1769
|
+
description: "When this newsletter was sent"
|
|
1770
|
+
}
|
|
1771
|
+
},
|
|
1772
|
+
{
|
|
1773
|
+
name: "sendStatus",
|
|
1774
|
+
type: "select",
|
|
1775
|
+
label: "Status",
|
|
1776
|
+
options: [
|
|
1777
|
+
{ label: "Draft", value: "draft" },
|
|
1778
|
+
{ label: "Scheduled", value: "scheduled" },
|
|
1779
|
+
{ label: "Sending", value: "sending" },
|
|
1780
|
+
{ label: "Sent", value: "sent" },
|
|
1781
|
+
{ label: "Failed", value: "failed" }
|
|
1782
|
+
],
|
|
1783
|
+
defaultValue: "draft",
|
|
1784
|
+
admin: {
|
|
1785
|
+
readOnly: true,
|
|
1786
|
+
description: "Current send status"
|
|
1787
|
+
}
|
|
1788
|
+
},
|
|
1789
|
+
{
|
|
1790
|
+
name: "emailSubject",
|
|
1791
|
+
type: "text",
|
|
1792
|
+
label: "Email Subject",
|
|
1793
|
+
required: true,
|
|
1794
|
+
admin: {
|
|
1795
|
+
condition: (data) => data?.[groupName]?.scheduled,
|
|
1796
|
+
description: "Subject line for the newsletter email"
|
|
1797
|
+
}
|
|
1798
|
+
},
|
|
1799
|
+
{
|
|
1800
|
+
name: "preheader",
|
|
1801
|
+
type: "text",
|
|
1802
|
+
label: "Email Preheader",
|
|
1803
|
+
admin: {
|
|
1804
|
+
condition: (data) => data?.[groupName]?.scheduled,
|
|
1805
|
+
description: "Preview text that appears after the subject line"
|
|
1806
|
+
}
|
|
1807
|
+
},
|
|
1808
|
+
{
|
|
1809
|
+
name: "segments",
|
|
1810
|
+
type: "select",
|
|
1811
|
+
label: "Target Segments",
|
|
1812
|
+
hasMany: true,
|
|
1813
|
+
options: [
|
|
1814
|
+
{ label: "All Subscribers", value: "all" },
|
|
1815
|
+
...config.i18n?.locales?.map((locale) => ({
|
|
1816
|
+
label: `${locale.toUpperCase()} Subscribers`,
|
|
1817
|
+
value: locale
|
|
1818
|
+
})) || []
|
|
1819
|
+
],
|
|
1820
|
+
defaultValue: ["all"],
|
|
1821
|
+
admin: {
|
|
1822
|
+
condition: (data) => data?.[groupName]?.scheduled,
|
|
1823
|
+
description: "Which subscriber segments to send to"
|
|
1824
|
+
}
|
|
1825
|
+
},
|
|
1826
|
+
{
|
|
1827
|
+
name: "testEmails",
|
|
1828
|
+
type: "array",
|
|
1829
|
+
label: "Test Email Recipients",
|
|
1830
|
+
admin: {
|
|
1831
|
+
condition: (data) => data?.[groupName]?.scheduled && data?.[groupName]?.sendStatus === "draft",
|
|
1832
|
+
description: "Send test emails before scheduling"
|
|
1833
|
+
},
|
|
1834
|
+
fields: [
|
|
1835
|
+
{
|
|
1836
|
+
name: "email",
|
|
1837
|
+
type: "email",
|
|
1838
|
+
required: true
|
|
1839
|
+
}
|
|
1840
|
+
]
|
|
1841
|
+
}
|
|
1842
|
+
]
|
|
1843
|
+
}
|
|
1844
|
+
];
|
|
1845
|
+
if (createMarkdownField) {
|
|
1846
|
+
fields.push(createMarkdownFieldInternal({
|
|
1847
|
+
name: `${contentField}Markdown`,
|
|
1848
|
+
richTextField: contentField,
|
|
1849
|
+
label: "Email Content (Markdown)",
|
|
1850
|
+
admin: {
|
|
1851
|
+
position: "sidebar",
|
|
1852
|
+
condition: (data) => Boolean(data?.[contentField] && data?.[groupName]?.scheduled),
|
|
1853
|
+
description: "Markdown version for email rendering",
|
|
1854
|
+
readOnly: true
|
|
1855
|
+
}
|
|
1856
|
+
}));
|
|
1857
|
+
}
|
|
1858
|
+
return fields;
|
|
1859
|
+
}
|
|
1860
|
+
function createMarkdownFieldInternal(config) {
|
|
1861
|
+
return {
|
|
1862
|
+
name: config.name,
|
|
1863
|
+
type: "textarea",
|
|
1864
|
+
label: config.label || "Markdown",
|
|
1865
|
+
admin: {
|
|
1866
|
+
...config.admin,
|
|
1867
|
+
description: config.admin?.description || "Auto-generated from rich text content"
|
|
1868
|
+
},
|
|
1869
|
+
hooks: {
|
|
1870
|
+
afterRead: [
|
|
1871
|
+
async ({ data }) => {
|
|
1872
|
+
if (data?.[config.richTextField]) {
|
|
1873
|
+
try {
|
|
1874
|
+
const { convertLexicalToMarkdown } = await import("@payloadcms/richtext-lexical");
|
|
1875
|
+
return convertLexicalToMarkdown({
|
|
1876
|
+
data: data[config.richTextField]
|
|
1877
|
+
});
|
|
1878
|
+
} catch {
|
|
1879
|
+
return "";
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
return "";
|
|
1883
|
+
}
|
|
1884
|
+
],
|
|
1885
|
+
beforeChange: [
|
|
1886
|
+
() => {
|
|
1887
|
+
return null;
|
|
1888
|
+
}
|
|
1889
|
+
]
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// src/index.ts
|
|
1895
|
+
var newsletterPlugin = (pluginConfig) => (incomingConfig) => {
|
|
1896
|
+
const config = {
|
|
1897
|
+
enabled: true,
|
|
1898
|
+
subscribersSlug: "subscribers",
|
|
1899
|
+
settingsSlug: "newsletter-settings",
|
|
1900
|
+
auth: {
|
|
1901
|
+
enabled: true,
|
|
1902
|
+
tokenExpiration: "7d",
|
|
1903
|
+
magicLinkPath: "/newsletter/verify",
|
|
1904
|
+
...pluginConfig.auth
|
|
1905
|
+
},
|
|
1906
|
+
...pluginConfig
|
|
1907
|
+
};
|
|
1908
|
+
if (!config.enabled) {
|
|
1909
|
+
return incomingConfig;
|
|
1910
|
+
}
|
|
1911
|
+
const subscribersCollection = createSubscribersCollection(config);
|
|
1912
|
+
const settingsCollection = createNewsletterSettingsCollection(config);
|
|
1913
|
+
let collections = [...incomingConfig.collections || [], subscribersCollection, settingsCollection];
|
|
1914
|
+
if (config.features?.newsletterScheduling?.enabled) {
|
|
1915
|
+
const targetCollections = config.features.newsletterScheduling.collections || "articles";
|
|
1916
|
+
const collectionsToExtend = Array.isArray(targetCollections) ? targetCollections : [targetCollections];
|
|
1917
|
+
const schedulingFields = createNewsletterSchedulingFields(config);
|
|
1918
|
+
collections = collections.map((collection) => {
|
|
1919
|
+
if (collectionsToExtend.includes(collection.slug)) {
|
|
1920
|
+
return {
|
|
1921
|
+
...collection,
|
|
1922
|
+
fields: [
|
|
1923
|
+
...collection.fields,
|
|
1924
|
+
...schedulingFields
|
|
1925
|
+
]
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
return collection;
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
const endpoints = createNewsletterEndpoints(config);
|
|
1932
|
+
const modifiedConfig = {
|
|
1933
|
+
...incomingConfig,
|
|
1934
|
+
collections,
|
|
1935
|
+
globals: [
|
|
1936
|
+
...incomingConfig.globals || []
|
|
1937
|
+
],
|
|
1938
|
+
endpoints: [
|
|
1939
|
+
...incomingConfig.endpoints || [],
|
|
1940
|
+
...endpoints
|
|
1941
|
+
],
|
|
1942
|
+
onInit: async (payload) => {
|
|
1943
|
+
try {
|
|
1944
|
+
const settingsResult = await payload.find({
|
|
1945
|
+
collection: config.settingsSlug || "newsletter-settings",
|
|
1946
|
+
where: {
|
|
1947
|
+
active: {
|
|
1948
|
+
equals: true
|
|
1949
|
+
}
|
|
1950
|
+
},
|
|
1951
|
+
limit: 1
|
|
1952
|
+
});
|
|
1953
|
+
const settings = settingsResult.docs[0];
|
|
1954
|
+
let emailServiceConfig;
|
|
1955
|
+
if (settings) {
|
|
1956
|
+
emailServiceConfig = {
|
|
1957
|
+
provider: settings.provider || config.providers.default,
|
|
1958
|
+
fromAddress: settings.fromAddress || config.providers.resend?.fromAddress || config.providers.broadcast?.fromAddress || "noreply@example.com",
|
|
1959
|
+
fromName: settings.fromName || config.providers.resend?.fromName || config.providers.broadcast?.fromName || "Newsletter",
|
|
1960
|
+
replyTo: settings.replyTo,
|
|
1961
|
+
resend: settings.provider === "resend" ? {
|
|
1962
|
+
apiKey: settings.resendSettings?.apiKey || config.providers.resend?.apiKey || "",
|
|
1963
|
+
audienceIds: settings.resendSettings?.audienceIds?.reduce((acc, item) => {
|
|
1964
|
+
acc[item.locale] = {
|
|
1965
|
+
production: item.production,
|
|
1966
|
+
development: item.development
|
|
1967
|
+
};
|
|
1968
|
+
return acc;
|
|
1969
|
+
}, {}) || config.providers.resend?.audienceIds
|
|
1970
|
+
} : config.providers.resend,
|
|
1971
|
+
broadcast: settings.provider === "broadcast" ? {
|
|
1972
|
+
apiUrl: settings.broadcastSettings?.apiUrl || config.providers.broadcast?.apiUrl || "",
|
|
1973
|
+
tokens: {
|
|
1974
|
+
production: settings.broadcastSettings?.productionToken || config.providers.broadcast?.tokens.production,
|
|
1975
|
+
development: settings.broadcastSettings?.developmentToken || config.providers.broadcast?.tokens.development
|
|
1976
|
+
}
|
|
1977
|
+
} : config.providers.broadcast
|
|
1978
|
+
};
|
|
1979
|
+
} else {
|
|
1980
|
+
emailServiceConfig = {
|
|
1981
|
+
provider: config.providers.default,
|
|
1982
|
+
fromAddress: config.providers.resend?.fromAddress || config.providers.broadcast?.fromAddress || "noreply@example.com",
|
|
1983
|
+
fromName: config.providers.resend?.fromName || config.providers.broadcast?.fromName || "Newsletter",
|
|
1984
|
+
resend: config.providers.resend,
|
|
1985
|
+
broadcast: config.providers.broadcast
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
payload.newsletterEmailService = createEmailService(emailServiceConfig);
|
|
1989
|
+
console.warn("Newsletter plugin initialized with", payload.newsletterEmailService.getProvider(), "provider");
|
|
1990
|
+
} catch (error) {
|
|
1991
|
+
console.error("Failed to initialize newsletter email service:", error);
|
|
1992
|
+
}
|
|
1993
|
+
if (incomingConfig.onInit) {
|
|
1994
|
+
await incomingConfig.onInit(payload);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
return modifiedConfig;
|
|
1999
|
+
};
|
|
2000
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2001
|
+
0 && (module.exports = {
|
|
2002
|
+
newsletterPlugin
|
|
2003
|
+
});
|
|
2004
|
+
//# sourceMappingURL=index.cjs.map
|