payload-plugin-newsletter 0.25.0 → 0.25.2

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/dist/server.js CHANGED
@@ -1602,7 +1602,7 @@ var createSubscribeEndpoint = (config) => {
1602
1602
  return {
1603
1603
  path: "/newsletter/subscribe",
1604
1604
  method: "post",
1605
- handler: async (req) => {
1605
+ handler: (async (req) => {
1606
1606
  try {
1607
1607
  const data = await req.json();
1608
1608
  const {
@@ -1837,7 +1837,7 @@ var createSubscribeEndpoint = (config) => {
1837
1837
  error: "Failed to subscribe. Please try again."
1838
1838
  }, { status: 500 });
1839
1839
  }
1840
- }
1840
+ })
1841
1841
  };
1842
1842
  };
1843
1843
 
@@ -1846,7 +1846,7 @@ var createVerifyMagicLinkEndpoint = (config) => {
1846
1846
  return {
1847
1847
  path: "/newsletter/verify-magic-link",
1848
1848
  method: "post",
1849
- handler: async (req) => {
1849
+ handler: (async (req) => {
1850
1850
  try {
1851
1851
  const data = await req.json();
1852
1852
  const { token } = data;
@@ -1966,7 +1966,7 @@ var createVerifyMagicLinkEndpoint = (config) => {
1966
1966
  error: "Failed to verify magic link"
1967
1967
  }, { status: 500 });
1968
1968
  }
1969
- }
1969
+ })
1970
1970
  };
1971
1971
  };
1972
1972
 
@@ -1975,7 +1975,7 @@ var createPreferencesEndpoint = (config) => {
1975
1975
  return {
1976
1976
  path: "/newsletter/preferences",
1977
1977
  method: "get",
1978
- handler: async (req) => {
1978
+ handler: (async (req) => {
1979
1979
  try {
1980
1980
  const authHeader = req.headers.get("authorization");
1981
1981
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
@@ -2028,14 +2028,14 @@ var createPreferencesEndpoint = (config) => {
2028
2028
  error: "Failed to get preferences"
2029
2029
  }, { status: 500 });
2030
2030
  }
2031
- }
2031
+ })
2032
2032
  };
2033
2033
  };
2034
2034
  var createUpdatePreferencesEndpoint = (config) => {
2035
2035
  return {
2036
2036
  path: "/newsletter/preferences",
2037
2037
  method: "post",
2038
- handler: async (req) => {
2038
+ handler: (async (req) => {
2039
2039
  try {
2040
2040
  const authHeader = req.headers.get("authorization");
2041
2041
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
@@ -2095,7 +2095,7 @@ var createUpdatePreferencesEndpoint = (config) => {
2095
2095
  error: "Failed to update preferences"
2096
2096
  }, { status: 500 });
2097
2097
  }
2098
- }
2098
+ })
2099
2099
  };
2100
2100
  };
2101
2101
 
@@ -2104,7 +2104,7 @@ var createUnsubscribeEndpoint = (config) => {
2104
2104
  return {
2105
2105
  path: "/newsletter/unsubscribe",
2106
2106
  method: "post",
2107
- handler: async (req) => {
2107
+ handler: (async (req) => {
2108
2108
  try {
2109
2109
  const data = await req.json();
2110
2110
  const { email, token } = data;
@@ -2195,7 +2195,7 @@ var createUnsubscribeEndpoint = (config) => {
2195
2195
  error: "Failed to unsubscribe. Please try again."
2196
2196
  }, { status: 500 });
2197
2197
  }
2198
- }
2198
+ })
2199
2199
  };
2200
2200
  };
2201
2201
 
@@ -2252,7 +2252,7 @@ var createSigninEndpoint = (config) => {
2252
2252
  return {
2253
2253
  path: "/newsletter/signin",
2254
2254
  method: "post",
2255
- handler: async (req) => {
2255
+ handler: (async (req) => {
2256
2256
  try {
2257
2257
  const data = await req.json();
2258
2258
  const { email, redirectUrl } = data;
@@ -2339,7 +2339,7 @@ var createSigninEndpoint = (config) => {
2339
2339
  error: "Failed to process sign-in request"
2340
2340
  }, { status: 500 });
2341
2341
  }
2342
- }
2342
+ })
2343
2343
  };
2344
2344
  };
2345
2345
 
@@ -2348,7 +2348,7 @@ var createMeEndpoint = (config) => {
2348
2348
  return {
2349
2349
  path: "/newsletter/me",
2350
2350
  method: "get",
2351
- handler: async (req) => {
2351
+ handler: (async (req) => {
2352
2352
  try {
2353
2353
  const cookieHeader = req.headers.get("cookie") || "";
2354
2354
  const cookies = Object.fromEntries(
@@ -2407,7 +2407,7 @@ var createMeEndpoint = (config) => {
2407
2407
  error: "Internal server error"
2408
2408
  }, { status: 500 });
2409
2409
  }
2410
- }
2410
+ })
2411
2411
  };
2412
2412
  };
2413
2413
 
@@ -2416,7 +2416,7 @@ var createSignoutEndpoint = (_config) => {
2416
2416
  return {
2417
2417
  path: "/newsletter/signout",
2418
2418
  method: "post",
2419
- handler: (_req) => {
2419
+ handler: ((_req) => {
2420
2420
  try {
2421
2421
  const headers = new Headers();
2422
2422
  headers.append("Set-Cookie", `newsletter-auth=; HttpOnly; Secure=${process.env.NODE_ENV === "production"}; SameSite=Lax; Path=/; Max-Age=0`);
@@ -2431,7 +2431,7 @@ var createSignoutEndpoint = (_config) => {
2431
2431
  error: "Failed to sign out"
2432
2432
  }, { status: 500 });
2433
2433
  }
2434
- }
2434
+ })
2435
2435
  };
2436
2436
  };
2437
2437
 
@@ -2637,8 +2637,8 @@ async function handleSubscriberCreated(event, payload, subscribersSlug) {
2637
2637
  data: {
2638
2638
  email: data.email,
2639
2639
  name: data.name,
2640
- subscriptionStatus: "pending",
2641
- // New subscribers start as pending
2640
+ subscriptionStatus: "active",
2641
+ // Match Broadcast's active status
2642
2642
  externalId: data.id,
2643
2643
  source: data.source,
2644
2644
  importedFromProvider: true,
@@ -2907,16 +2907,6 @@ var createBroadcastWebhookEndpoint = (config) => {
2907
2907
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2908
2908
  });
2909
2909
  await routeWebhookEvent(data, req, config);
2910
- await req.payload.updateGlobal({
2911
- slug: config.settingsSlug || "newsletter-settings",
2912
- data: {
2913
- broadcastSettings: {
2914
- ...settings?.broadcastSettings || {},
2915
- lastWebhookReceived: (/* @__PURE__ */ new Date()).toISOString(),
2916
- webhookStatus: "verified"
2917
- }
2918
- }
2919
- });
2920
2910
  return Response.json({ success: true }, { status: 200 });
2921
2911
  } catch (error) {
2922
2912
  console.error("[Broadcast Webhook] Error processing webhook:", error);
@@ -4069,7 +4059,7 @@ var createSendBroadcastEndpoint = (config, collectionSlug) => {
4069
4059
  return {
4070
4060
  path: "/:id/send",
4071
4061
  method: "post",
4072
- handler: async (req) => {
4062
+ handler: (async (req) => {
4073
4063
  try {
4074
4064
  const auth = await requireAdmin(req, config);
4075
4065
  if (!auth.authorized) {
@@ -4143,7 +4133,7 @@ var createSendBroadcastEndpoint = (config, collectionSlug) => {
4143
4133
  error: "Failed to send broadcast"
4144
4134
  }, { status: 500 });
4145
4135
  }
4146
- }
4136
+ })
4147
4137
  };
4148
4138
  };
4149
4139
 
@@ -4152,7 +4142,7 @@ var createScheduleBroadcastEndpoint = (config, collectionSlug) => {
4152
4142
  return {
4153
4143
  path: "/:id/schedule",
4154
4144
  method: "post",
4155
- handler: async (req) => {
4145
+ handler: (async (req) => {
4156
4146
  try {
4157
4147
  const auth = await requireAdmin(req, config);
4158
4148
  if (!auth.authorized) {
@@ -4246,7 +4236,7 @@ var createScheduleBroadcastEndpoint = (config, collectionSlug) => {
4246
4236
  error: "Failed to schedule broadcast"
4247
4237
  }, { status: 500 });
4248
4238
  }
4249
- }
4239
+ })
4250
4240
  };
4251
4241
  };
4252
4242
 
@@ -4255,7 +4245,7 @@ var createTestBroadcastEndpoint = (config, collectionSlug) => {
4255
4245
  return {
4256
4246
  path: "/:id/test",
4257
4247
  method: "post",
4258
- handler: async (req) => {
4248
+ handler: (async (req) => {
4259
4249
  try {
4260
4250
  const auth = await requireAdmin(req, config);
4261
4251
  if (!auth.authorized) {
@@ -4329,7 +4319,7 @@ var createTestBroadcastEndpoint = (config, collectionSlug) => {
4329
4319
  error: "Failed to send test email"
4330
4320
  }, { status: 500 });
4331
4321
  }
4332
- }
4322
+ })
4333
4323
  };
4334
4324
  };
4335
4325
 
@@ -4418,7 +4408,7 @@ var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
4418
4408
  return {
4419
4409
  path: "/preview",
4420
4410
  method: "post",
4421
- handler: async (req) => {
4411
+ handler: (async (req) => {
4422
4412
  try {
4423
4413
  const data = await (req.json?.() || Promise.resolve({}));
4424
4414
  const { content, preheader, subject, documentData } = data;
@@ -4458,7 +4448,7 @@ var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
4458
4448
  error: "Failed to generate email preview"
4459
4449
  }, { status: 500 });
4460
4450
  }
4461
- }
4451
+ })
4462
4452
  };
4463
4453
  };
4464
4454
 
@@ -4803,10 +4793,6 @@ var createBroadcastsCollection = (pluginConfig) => {
4803
4793
  async ({ doc, operation, req, previousDoc }) => {
4804
4794
  if (!hasProviders) return doc;
4805
4795
  if (operation === "create") {
4806
- if (!doc.subject || !doc.contentSection?.content) {
4807
- req.payload.logger.info("Skipping provider sync - broadcast has no subject or content yet");
4808
- return doc;
4809
- }
4810
4796
  try {
4811
4797
  const providerConfig = await getBroadcastConfig(req, pluginConfig);
4812
4798
  if (!providerConfig || !providerConfig.token) {
@@ -4815,45 +4801,32 @@ var createBroadcastsCollection = (pluginConfig) => {
4815
4801
  }
4816
4802
  const { BroadcastApiProvider: BroadcastApiProvider2 } = await import("./broadcast-VMCYSZRY.js");
4817
4803
  const provider = new BroadcastApiProvider2(providerConfig);
4818
- req.payload.logger.info("Populating media fields and converting content to HTML...");
4819
- const populatedContent = await populateMediaFields(doc.contentSection?.content, req.payload, pluginConfig);
4820
- const emailPreviewConfig = pluginConfig.customizations?.broadcasts?.emailPreview;
4821
- const htmlContent = await convertToEmailSafeHtml(populatedContent, {
4822
- wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
4823
- customWrapper: emailPreviewConfig?.customWrapper,
4824
- preheader: doc.contentSection?.preheader,
4825
- subject: doc.subject,
4826
- documentData: doc,
4827
- // Pass entire document
4828
- customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
4829
- });
4830
- if (!htmlContent || htmlContent.trim() === "") {
4831
- req.payload.logger.info("Skipping provider sync - content is empty after conversion");
4832
- return doc;
4833
- }
4804
+ const subject = doc.subject || `Draft Broadcast ${(/* @__PURE__ */ new Date()).toISOString()}`;
4805
+ const htmlContent = doc.contentSection?.content ? await convertToEmailSafeHtml(
4806
+ await populateMediaFields(doc.contentSection.content, req.payload, pluginConfig),
4807
+ {
4808
+ wrapInTemplate: pluginConfig.customizations?.broadcasts?.emailPreview?.wrapInTemplate ?? true,
4809
+ customWrapper: pluginConfig.customizations?.broadcasts?.emailPreview?.customWrapper,
4810
+ preheader: doc.contentSection?.preheader,
4811
+ subject,
4812
+ documentData: doc,
4813
+ customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
4814
+ }
4815
+ ) : "<p>Draft content - to be updated</p>";
4834
4816
  const createData = {
4835
- name: doc.subject,
4836
- // Use subject as name since we removed the name field
4837
- subject: doc.subject,
4838
- preheader: doc.contentSection?.preheader,
4817
+ name: subject,
4818
+ // Use subject as name
4819
+ subject,
4820
+ preheader: doc.contentSection?.preheader || "",
4839
4821
  content: htmlContent,
4840
- trackOpens: doc.settings?.trackOpens,
4841
- trackClicks: doc.settings?.trackClicks,
4822
+ trackOpens: doc.settings?.trackOpens ?? true,
4823
+ trackClicks: doc.settings?.trackClicks ?? true,
4842
4824
  replyTo: doc.settings?.replyTo || providerConfig.replyTo,
4843
- audienceIds: doc.audienceIds?.map((a) => a.audienceId)
4825
+ audienceIds: doc.audienceIds?.map((a) => a.audienceId) || []
4844
4826
  };
4845
- req.payload.logger.info("Creating broadcast with data:", {
4846
- name: createData.name,
4827
+ req.payload.logger.info("Creating broadcast in provider with minimal data to establish association", {
4847
4828
  subject: createData.subject,
4848
- preheader: createData.preheader || "NONE",
4849
- contentLength: htmlContent ? htmlContent.length : 0,
4850
- contentPreview: htmlContent ? htmlContent.substring(0, 100) + "..." : "EMPTY",
4851
- trackOpens: createData.trackOpens,
4852
- trackClicks: createData.trackClicks,
4853
- replyTo: createData.replyTo,
4854
- audienceIds: createData.audienceIds || [],
4855
- apiUrl: providerConfig.apiUrl,
4856
- hasToken: !!providerConfig.token
4829
+ hasActualContent: !!doc.contentSection?.content
4857
4830
  });
4858
4831
  const providerBroadcast = await provider.create(createData);
4859
4832
  await req.payload.update({
@@ -4865,40 +4838,14 @@ var createBroadcastsCollection = (pluginConfig) => {
4865
4838
  },
4866
4839
  req
4867
4840
  });
4841
+ req.payload.logger.info(`Broadcast ${doc.id} created in provider with ID ${providerBroadcast.id}`);
4868
4842
  return {
4869
4843
  ...doc,
4870
4844
  providerId: providerBroadcast.id,
4871
4845
  providerData: providerBroadcast.providerData
4872
4846
  };
4873
4847
  } catch (error) {
4874
- req.payload.logger.error("Raw error from broadcast provider:");
4875
- req.payload.logger.error(error);
4876
- if (error instanceof Error) {
4877
- req.payload.logger.error("Error is instance of Error:", {
4878
- message: error.message,
4879
- stack: error.stack,
4880
- name: error.name,
4881
- // If it's a BroadcastProviderError, it might have additional details
4882
- ...error.details,
4883
- // Check if it's a fetch response error
4884
- ...error.response,
4885
- ...error.data,
4886
- ...error.status,
4887
- ...error.statusText
4888
- });
4889
- } else if (typeof error === "string") {
4890
- req.payload.logger.error("Error is a string:", error);
4891
- } else if (error && typeof error === "object") {
4892
- req.payload.logger.error("Error is an object:", JSON.stringify(error, null, 2));
4893
- } else {
4894
- req.payload.logger.error("Unknown error type:", typeof error);
4895
- }
4896
- req.payload.logger.error("Failed broadcast document:", {
4897
- id: doc.id,
4898
- subject: doc.subject,
4899
- hasContent: !!doc.contentSection?.content,
4900
- contentType: doc.contentSection?.content ? typeof doc.contentSection.content : "none"
4901
- });
4848
+ req.payload.logger.error("Failed to create broadcast in provider during initial creation:", error);
4902
4849
  return doc;
4903
4850
  }
4904
4851
  }
@@ -4918,61 +4865,8 @@ var createBroadcastsCollection = (pluginConfig) => {
4918
4865
  const { BroadcastApiProvider: BroadcastApiProvider2 } = await import("./broadcast-VMCYSZRY.js");
4919
4866
  const provider = new BroadcastApiProvider2(providerConfig);
4920
4867
  if (!doc.providerId) {
4921
- if (!doc.subject || !doc.contentSection?.content) {
4922
- req.payload.logger.info("Still missing required fields for provider sync");
4923
- return doc;
4924
- }
4925
- req.payload.logger.info("Creating broadcast in provider (deferred from initial create)...");
4926
- const populatedContent = await populateMediaFields(doc.contentSection?.content, req.payload, pluginConfig);
4927
- const emailPreviewConfig = pluginConfig.customizations?.broadcasts?.emailPreview;
4928
- const htmlContent = await convertToEmailSafeHtml(populatedContent, {
4929
- wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
4930
- customWrapper: emailPreviewConfig?.customWrapper,
4931
- preheader: doc.contentSection?.preheader,
4932
- subject: doc.subject,
4933
- documentData: doc,
4934
- // Pass entire document
4935
- customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
4936
- });
4937
- if (!htmlContent || htmlContent.trim() === "") {
4938
- req.payload.logger.info("Skipping provider sync - content is empty after conversion");
4939
- return doc;
4940
- }
4941
- const createData = {
4942
- name: doc.subject,
4943
- subject: doc.subject,
4944
- preheader: doc.contentSection?.preheader,
4945
- content: htmlContent,
4946
- trackOpens: doc.settings?.trackOpens,
4947
- trackClicks: doc.settings?.trackClicks,
4948
- replyTo: doc.settings?.replyTo || providerConfig.replyTo,
4949
- audienceIds: doc.audienceIds?.map((a) => a.audienceId)
4950
- };
4951
- req.payload.logger.info("Creating broadcast with data:", {
4952
- name: createData.name,
4953
- subject: createData.subject,
4954
- preheader: createData.preheader || "NONE",
4955
- contentLength: htmlContent ? htmlContent.length : 0,
4956
- contentPreview: htmlContent ? htmlContent.substring(0, 100) + "..." : "EMPTY",
4957
- apiUrl: providerConfig.apiUrl,
4958
- hasToken: !!providerConfig.token
4959
- });
4960
- const providerBroadcast = await provider.create(createData);
4961
- await req.payload.update({
4962
- collection: "broadcasts",
4963
- id: doc.id,
4964
- data: {
4965
- providerId: providerBroadcast.id,
4966
- providerData: providerBroadcast.providerData
4967
- },
4968
- req
4969
- });
4970
- req.payload.logger.info(`Broadcast ${doc.id} created in provider successfully (deferred)`);
4971
- return {
4972
- ...doc,
4973
- providerId: providerBroadcast.id,
4974
- providerData: providerBroadcast.providerData
4975
- };
4868
+ req.payload.logger.warn(`Broadcast ${doc.id} has no providerId - provider sync skipped. This shouldn't happen with immediate creation.`);
4869
+ return doc;
4976
4870
  }
4977
4871
  if (doc.providerId) {
4978
4872
  const capabilities = provider.getCapabilities();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-plugin-newsletter",
3
- "version": "0.25.0",
3
+ "version": "0.25.2",
4
4
  "description": "Complete newsletter management plugin for Payload CMS with subscriber management, magic link authentication, and email service integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,117 +0,0 @@
1
- # Newsletter Plugin API Key Support Recommendations
2
-
3
- ## Current Issue
4
- The broadcasts collection access control doesn't properly handle API key authentication. When users authenticate with `users API-Key {key}`, the `req.user` is undefined, causing 403 errors.
5
-
6
- ## Recommended Changes
7
-
8
- ### 1. Update Broadcasts Collection Access Control
9
-
10
- In `src/collections/Broadcasts.ts`, update the access control to check for API key authentication:
11
-
12
- ```typescript
13
- access: {
14
- read: () => true,
15
- create: ({ req }) => {
16
- // Check for standard user authentication
17
- if (req.user) return true
18
-
19
- // Check for API key authentication
20
- const authHeader = req.headers?.get?.('authorization') ||
21
- req.headers?.authorization ||
22
- req.headers?.['authorization']
23
-
24
- if (authHeader && typeof authHeader === 'string') {
25
- // Payload uses "users API-Key {key}" format for API key auth
26
- if (authHeader.startsWith('users API-Key ')) {
27
- // When authenticated via API key, req.user might be undefined
28
- // but the request is still authenticated
29
- return true
30
- }
31
- }
32
-
33
- return false
34
- },
35
- update: ({ req }) => {
36
- // Same logic as create
37
- if (req.user) return true
38
-
39
- const authHeader = req.headers?.get?.('authorization') ||
40
- req.headers?.authorization ||
41
- req.headers?.['authorization']
42
-
43
- if (authHeader && typeof authHeader === 'string' &&
44
- authHeader.startsWith('users API-Key ')) {
45
- return true
46
- }
47
-
48
- return false
49
- },
50
- delete: ({ req }) => {
51
- // Same logic as create/update
52
- if (req.user) return true
53
-
54
- const authHeader = req.headers?.get?.('authorization') ||
55
- req.headers?.authorization ||
56
- req.headers?.['authorization']
57
-
58
- if (authHeader && typeof authHeader === 'string' &&
59
- authHeader.startsWith('users API-Key ')) {
60
- return true
61
- }
62
-
63
- return false
64
- },
65
- },
66
- ```
67
-
68
- ### 2. Alternative: Create a Utility Function
69
-
70
- Create a utility function in `src/utils/checkApiKeyAuth.ts`:
71
-
72
- ```typescript
73
- export function hasApiKeyAuth(req: any): boolean {
74
- // Multiple ways to access headers depending on the request type
75
- const authHeader = req.headers?.get?.('authorization') ||
76
- req.headers?.authorization ||
77
- req.headers?.['authorization']
78
-
79
- if (authHeader && typeof authHeader === 'string') {
80
- return authHeader.startsWith('users API-Key ')
81
- }
82
-
83
- return false
84
- }
85
-
86
- export function isAuthenticated(req: any): boolean {
87
- return !!req.user || hasApiKeyAuth(req)
88
- }
89
- ```
90
-
91
- Then use it in the access control:
92
-
93
- ```typescript
94
- import { isAuthenticated } from '../utils/checkApiKeyAuth'
95
-
96
- // In Broadcasts collection
97
- access: {
98
- read: () => true,
99
- create: ({ req }) => isAuthenticated(req),
100
- update: ({ req }) => isAuthenticated(req),
101
- delete: ({ req }) => isAuthenticated(req),
102
- },
103
- ```
104
-
105
- ### 3. Version Bump
106
- After implementing these changes, bump the version to v0.16.5 with the changelog:
107
- - Fixed: API key authentication support for broadcasts collection
108
- - Fixed: Access control now properly handles requests authenticated via API keys
109
-
110
- ## Testing
111
- The changes can be tested by:
112
- 1. Creating an API key in Payload admin
113
- 2. Making a POST request to `/api/broadcasts` with header `Authorization: users API-Key {key}`
114
- 3. Verifying the broadcast is created successfully
115
-
116
- ## Note
117
- The Payload CMS documentation doesn't clearly state that `req.user` is undefined for API key authenticated requests, but this is the observed behavior. The API key authentication is valid, but the user object isn't populated in the request context.