payload-plugin-newsletter 0.25.1 → 0.25.3
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 +15 -0
- package/dist/collections.cjs +77 -129
- package/dist/collections.cjs.map +1 -1
- package/dist/collections.js +77 -129
- package/dist/collections.js.map +1 -1
- package/dist/server.js +93 -145
- package/package.json +1 -1
- package/plugin-api-key-recommendations.md +0 -117
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
|
|
|
@@ -4059,7 +4059,7 @@ var createSendBroadcastEndpoint = (config, collectionSlug) => {
|
|
|
4059
4059
|
return {
|
|
4060
4060
|
path: "/:id/send",
|
|
4061
4061
|
method: "post",
|
|
4062
|
-
handler: async (req) => {
|
|
4062
|
+
handler: (async (req) => {
|
|
4063
4063
|
try {
|
|
4064
4064
|
const auth = await requireAdmin(req, config);
|
|
4065
4065
|
if (!auth.authorized) {
|
|
@@ -4133,7 +4133,7 @@ var createSendBroadcastEndpoint = (config, collectionSlug) => {
|
|
|
4133
4133
|
error: "Failed to send broadcast"
|
|
4134
4134
|
}, { status: 500 });
|
|
4135
4135
|
}
|
|
4136
|
-
}
|
|
4136
|
+
})
|
|
4137
4137
|
};
|
|
4138
4138
|
};
|
|
4139
4139
|
|
|
@@ -4142,7 +4142,7 @@ var createScheduleBroadcastEndpoint = (config, collectionSlug) => {
|
|
|
4142
4142
|
return {
|
|
4143
4143
|
path: "/:id/schedule",
|
|
4144
4144
|
method: "post",
|
|
4145
|
-
handler: async (req) => {
|
|
4145
|
+
handler: (async (req) => {
|
|
4146
4146
|
try {
|
|
4147
4147
|
const auth = await requireAdmin(req, config);
|
|
4148
4148
|
if (!auth.authorized) {
|
|
@@ -4236,7 +4236,7 @@ var createScheduleBroadcastEndpoint = (config, collectionSlug) => {
|
|
|
4236
4236
|
error: "Failed to schedule broadcast"
|
|
4237
4237
|
}, { status: 500 });
|
|
4238
4238
|
}
|
|
4239
|
-
}
|
|
4239
|
+
})
|
|
4240
4240
|
};
|
|
4241
4241
|
};
|
|
4242
4242
|
|
|
@@ -4245,7 +4245,7 @@ var createTestBroadcastEndpoint = (config, collectionSlug) => {
|
|
|
4245
4245
|
return {
|
|
4246
4246
|
path: "/:id/test",
|
|
4247
4247
|
method: "post",
|
|
4248
|
-
handler: async (req) => {
|
|
4248
|
+
handler: (async (req) => {
|
|
4249
4249
|
try {
|
|
4250
4250
|
const auth = await requireAdmin(req, config);
|
|
4251
4251
|
if (!auth.authorized) {
|
|
@@ -4319,7 +4319,7 @@ var createTestBroadcastEndpoint = (config, collectionSlug) => {
|
|
|
4319
4319
|
error: "Failed to send test email"
|
|
4320
4320
|
}, { status: 500 });
|
|
4321
4321
|
}
|
|
4322
|
-
}
|
|
4322
|
+
})
|
|
4323
4323
|
};
|
|
4324
4324
|
};
|
|
4325
4325
|
|
|
@@ -4395,6 +4395,10 @@ async function populateBlockMediaFields(node, payload, config) {
|
|
|
4395
4395
|
}
|
|
4396
4396
|
}
|
|
4397
4397
|
}
|
|
4398
|
+
if (field.type === "richText" && node.fields[field.name]) {
|
|
4399
|
+
await populateRichTextUploads(node.fields[field.name], payload);
|
|
4400
|
+
payload.logger?.info(`Processed rich text field ${field.name} for upload nodes`);
|
|
4401
|
+
}
|
|
4398
4402
|
}
|
|
4399
4403
|
}
|
|
4400
4404
|
}
|
|
@@ -4404,11 +4408,51 @@ async function populateBlockMediaFields(node, payload, config) {
|
|
|
4404
4408
|
}
|
|
4405
4409
|
}
|
|
4406
4410
|
}
|
|
4411
|
+
async function populateRichTextUploads(content, payload) {
|
|
4412
|
+
if (!content || typeof content !== "object") return;
|
|
4413
|
+
if (content.root?.children) {
|
|
4414
|
+
await processNodeArray(content.root.children);
|
|
4415
|
+
}
|
|
4416
|
+
if (Array.isArray(content)) {
|
|
4417
|
+
await processNodeArray(content);
|
|
4418
|
+
}
|
|
4419
|
+
async function processNodeArray(nodes) {
|
|
4420
|
+
await Promise.all(nodes.map(processNode));
|
|
4421
|
+
}
|
|
4422
|
+
async function processNode(node) {
|
|
4423
|
+
if (!node || typeof node !== "object") return;
|
|
4424
|
+
if (node.type === "upload" && node.relationTo === "media" && typeof node.value === "string" && node.value.match(/^[a-f0-9]{24}$/i)) {
|
|
4425
|
+
try {
|
|
4426
|
+
const media = await payload.findByID({
|
|
4427
|
+
collection: "media",
|
|
4428
|
+
id: node.value,
|
|
4429
|
+
depth: 0
|
|
4430
|
+
});
|
|
4431
|
+
if (media) {
|
|
4432
|
+
node.value = media;
|
|
4433
|
+
payload.logger?.info(`Populated rich text upload node:`, {
|
|
4434
|
+
mediaId: node.value,
|
|
4435
|
+
mediaUrl: media.url,
|
|
4436
|
+
filename: media.filename
|
|
4437
|
+
});
|
|
4438
|
+
}
|
|
4439
|
+
} catch (error) {
|
|
4440
|
+
payload.logger?.error(`Failed to populate rich text upload ${node.value}:`, error);
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
if (node.children && Array.isArray(node.children)) {
|
|
4444
|
+
await processNodeArray(node.children);
|
|
4445
|
+
}
|
|
4446
|
+
if (node.root?.children && Array.isArray(node.root.children)) {
|
|
4447
|
+
await processNodeArray(node.root.children);
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4407
4451
|
var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
|
|
4408
4452
|
return {
|
|
4409
4453
|
path: "/preview",
|
|
4410
4454
|
method: "post",
|
|
4411
|
-
handler: async (req) => {
|
|
4455
|
+
handler: (async (req) => {
|
|
4412
4456
|
try {
|
|
4413
4457
|
const data = await (req.json?.() || Promise.resolve({}));
|
|
4414
4458
|
const { content, preheader, subject, documentData } = data;
|
|
@@ -4448,7 +4492,7 @@ var createBroadcastPreviewEndpoint = (config, _collectionSlug) => {
|
|
|
4448
4492
|
error: "Failed to generate email preview"
|
|
4449
4493
|
}, { status: 500 });
|
|
4450
4494
|
}
|
|
4451
|
-
}
|
|
4495
|
+
})
|
|
4452
4496
|
};
|
|
4453
4497
|
};
|
|
4454
4498
|
|
|
@@ -4793,10 +4837,6 @@ var createBroadcastsCollection = (pluginConfig) => {
|
|
|
4793
4837
|
async ({ doc, operation, req, previousDoc }) => {
|
|
4794
4838
|
if (!hasProviders) return doc;
|
|
4795
4839
|
if (operation === "create") {
|
|
4796
|
-
if (!doc.subject || !doc.contentSection?.content) {
|
|
4797
|
-
req.payload.logger.info("Skipping provider sync - broadcast has no subject or content yet");
|
|
4798
|
-
return doc;
|
|
4799
|
-
}
|
|
4800
4840
|
try {
|
|
4801
4841
|
const providerConfig = await getBroadcastConfig(req, pluginConfig);
|
|
4802
4842
|
if (!providerConfig || !providerConfig.token) {
|
|
@@ -4805,45 +4845,32 @@ var createBroadcastsCollection = (pluginConfig) => {
|
|
|
4805
4845
|
}
|
|
4806
4846
|
const { BroadcastApiProvider: BroadcastApiProvider2 } = await import("./broadcast-VMCYSZRY.js");
|
|
4807
4847
|
const provider = new BroadcastApiProvider2(providerConfig);
|
|
4808
|
-
|
|
4809
|
-
const
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
if (!htmlContent || htmlContent.trim() === "") {
|
|
4821
|
-
req.payload.logger.info("Skipping provider sync - content is empty after conversion");
|
|
4822
|
-
return doc;
|
|
4823
|
-
}
|
|
4848
|
+
const subject = doc.subject || `Draft Broadcast ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
4849
|
+
const htmlContent = doc.contentSection?.content ? await convertToEmailSafeHtml(
|
|
4850
|
+
await populateMediaFields(doc.contentSection.content, req.payload, pluginConfig),
|
|
4851
|
+
{
|
|
4852
|
+
wrapInTemplate: pluginConfig.customizations?.broadcasts?.emailPreview?.wrapInTemplate ?? true,
|
|
4853
|
+
customWrapper: pluginConfig.customizations?.broadcasts?.emailPreview?.customWrapper,
|
|
4854
|
+
preheader: doc.contentSection?.preheader,
|
|
4855
|
+
subject,
|
|
4856
|
+
documentData: doc,
|
|
4857
|
+
customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
|
|
4858
|
+
}
|
|
4859
|
+
) : "<p>Draft content - to be updated</p>";
|
|
4824
4860
|
const createData = {
|
|
4825
|
-
name:
|
|
4826
|
-
// Use subject as name
|
|
4827
|
-
subject
|
|
4828
|
-
preheader: doc.contentSection?.preheader,
|
|
4861
|
+
name: subject,
|
|
4862
|
+
// Use subject as name
|
|
4863
|
+
subject,
|
|
4864
|
+
preheader: doc.contentSection?.preheader || "",
|
|
4829
4865
|
content: htmlContent,
|
|
4830
|
-
trackOpens: doc.settings?.trackOpens,
|
|
4831
|
-
trackClicks: doc.settings?.trackClicks,
|
|
4866
|
+
trackOpens: doc.settings?.trackOpens ?? true,
|
|
4867
|
+
trackClicks: doc.settings?.trackClicks ?? true,
|
|
4832
4868
|
replyTo: doc.settings?.replyTo || providerConfig.replyTo,
|
|
4833
|
-
audienceIds: doc.audienceIds?.map((a) => a.audienceId)
|
|
4869
|
+
audienceIds: doc.audienceIds?.map((a) => a.audienceId) || []
|
|
4834
4870
|
};
|
|
4835
|
-
req.payload.logger.info("Creating broadcast with data
|
|
4836
|
-
name: createData.name,
|
|
4871
|
+
req.payload.logger.info("Creating broadcast in provider with minimal data to establish association", {
|
|
4837
4872
|
subject: createData.subject,
|
|
4838
|
-
|
|
4839
|
-
contentLength: htmlContent ? htmlContent.length : 0,
|
|
4840
|
-
contentPreview: htmlContent ? htmlContent.substring(0, 100) + "..." : "EMPTY",
|
|
4841
|
-
trackOpens: createData.trackOpens,
|
|
4842
|
-
trackClicks: createData.trackClicks,
|
|
4843
|
-
replyTo: createData.replyTo,
|
|
4844
|
-
audienceIds: createData.audienceIds || [],
|
|
4845
|
-
apiUrl: providerConfig.apiUrl,
|
|
4846
|
-
hasToken: !!providerConfig.token
|
|
4873
|
+
hasActualContent: !!doc.contentSection?.content
|
|
4847
4874
|
});
|
|
4848
4875
|
const providerBroadcast = await provider.create(createData);
|
|
4849
4876
|
await req.payload.update({
|
|
@@ -4855,40 +4882,14 @@ var createBroadcastsCollection = (pluginConfig) => {
|
|
|
4855
4882
|
},
|
|
4856
4883
|
req
|
|
4857
4884
|
});
|
|
4885
|
+
req.payload.logger.info(`Broadcast ${doc.id} created in provider with ID ${providerBroadcast.id}`);
|
|
4858
4886
|
return {
|
|
4859
4887
|
...doc,
|
|
4860
4888
|
providerId: providerBroadcast.id,
|
|
4861
4889
|
providerData: providerBroadcast.providerData
|
|
4862
4890
|
};
|
|
4863
4891
|
} catch (error) {
|
|
4864
|
-
req.payload.logger.error("
|
|
4865
|
-
req.payload.logger.error(error);
|
|
4866
|
-
if (error instanceof Error) {
|
|
4867
|
-
req.payload.logger.error("Error is instance of Error:", {
|
|
4868
|
-
message: error.message,
|
|
4869
|
-
stack: error.stack,
|
|
4870
|
-
name: error.name,
|
|
4871
|
-
// If it's a BroadcastProviderError, it might have additional details
|
|
4872
|
-
...error.details,
|
|
4873
|
-
// Check if it's a fetch response error
|
|
4874
|
-
...error.response,
|
|
4875
|
-
...error.data,
|
|
4876
|
-
...error.status,
|
|
4877
|
-
...error.statusText
|
|
4878
|
-
});
|
|
4879
|
-
} else if (typeof error === "string") {
|
|
4880
|
-
req.payload.logger.error("Error is a string:", error);
|
|
4881
|
-
} else if (error && typeof error === "object") {
|
|
4882
|
-
req.payload.logger.error("Error is an object:", JSON.stringify(error, null, 2));
|
|
4883
|
-
} else {
|
|
4884
|
-
req.payload.logger.error("Unknown error type:", typeof error);
|
|
4885
|
-
}
|
|
4886
|
-
req.payload.logger.error("Failed broadcast document:", {
|
|
4887
|
-
id: doc.id,
|
|
4888
|
-
subject: doc.subject,
|
|
4889
|
-
hasContent: !!doc.contentSection?.content,
|
|
4890
|
-
contentType: doc.contentSection?.content ? typeof doc.contentSection.content : "none"
|
|
4891
|
-
});
|
|
4892
|
+
req.payload.logger.error("Failed to create broadcast in provider during initial creation:", error);
|
|
4892
4893
|
return doc;
|
|
4893
4894
|
}
|
|
4894
4895
|
}
|
|
@@ -4908,61 +4909,8 @@ var createBroadcastsCollection = (pluginConfig) => {
|
|
|
4908
4909
|
const { BroadcastApiProvider: BroadcastApiProvider2 } = await import("./broadcast-VMCYSZRY.js");
|
|
4909
4910
|
const provider = new BroadcastApiProvider2(providerConfig);
|
|
4910
4911
|
if (!doc.providerId) {
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
return doc;
|
|
4914
|
-
}
|
|
4915
|
-
req.payload.logger.info("Creating broadcast in provider (deferred from initial create)...");
|
|
4916
|
-
const populatedContent = await populateMediaFields(doc.contentSection?.content, req.payload, pluginConfig);
|
|
4917
|
-
const emailPreviewConfig = pluginConfig.customizations?.broadcasts?.emailPreview;
|
|
4918
|
-
const htmlContent = await convertToEmailSafeHtml(populatedContent, {
|
|
4919
|
-
wrapInTemplate: emailPreviewConfig?.wrapInTemplate ?? true,
|
|
4920
|
-
customWrapper: emailPreviewConfig?.customWrapper,
|
|
4921
|
-
preheader: doc.contentSection?.preheader,
|
|
4922
|
-
subject: doc.subject,
|
|
4923
|
-
documentData: doc,
|
|
4924
|
-
// Pass entire document
|
|
4925
|
-
customBlockConverter: pluginConfig.customizations?.broadcasts?.customBlockConverter
|
|
4926
|
-
});
|
|
4927
|
-
if (!htmlContent || htmlContent.trim() === "") {
|
|
4928
|
-
req.payload.logger.info("Skipping provider sync - content is empty after conversion");
|
|
4929
|
-
return doc;
|
|
4930
|
-
}
|
|
4931
|
-
const createData = {
|
|
4932
|
-
name: doc.subject,
|
|
4933
|
-
subject: doc.subject,
|
|
4934
|
-
preheader: doc.contentSection?.preheader,
|
|
4935
|
-
content: htmlContent,
|
|
4936
|
-
trackOpens: doc.settings?.trackOpens,
|
|
4937
|
-
trackClicks: doc.settings?.trackClicks,
|
|
4938
|
-
replyTo: doc.settings?.replyTo || providerConfig.replyTo,
|
|
4939
|
-
audienceIds: doc.audienceIds?.map((a) => a.audienceId)
|
|
4940
|
-
};
|
|
4941
|
-
req.payload.logger.info("Creating broadcast with data:", {
|
|
4942
|
-
name: createData.name,
|
|
4943
|
-
subject: createData.subject,
|
|
4944
|
-
preheader: createData.preheader || "NONE",
|
|
4945
|
-
contentLength: htmlContent ? htmlContent.length : 0,
|
|
4946
|
-
contentPreview: htmlContent ? htmlContent.substring(0, 100) + "..." : "EMPTY",
|
|
4947
|
-
apiUrl: providerConfig.apiUrl,
|
|
4948
|
-
hasToken: !!providerConfig.token
|
|
4949
|
-
});
|
|
4950
|
-
const providerBroadcast = await provider.create(createData);
|
|
4951
|
-
await req.payload.update({
|
|
4952
|
-
collection: "broadcasts",
|
|
4953
|
-
id: doc.id,
|
|
4954
|
-
data: {
|
|
4955
|
-
providerId: providerBroadcast.id,
|
|
4956
|
-
providerData: providerBroadcast.providerData
|
|
4957
|
-
},
|
|
4958
|
-
req
|
|
4959
|
-
});
|
|
4960
|
-
req.payload.logger.info(`Broadcast ${doc.id} created in provider successfully (deferred)`);
|
|
4961
|
-
return {
|
|
4962
|
-
...doc,
|
|
4963
|
-
providerId: providerBroadcast.id,
|
|
4964
|
-
providerData: providerBroadcast.providerData
|
|
4965
|
-
};
|
|
4912
|
+
req.payload.logger.warn(`Broadcast ${doc.id} has no providerId - provider sync skipped. This shouldn't happen with immediate creation.`);
|
|
4913
|
+
return doc;
|
|
4966
4914
|
}
|
|
4967
4915
|
if (doc.providerId) {
|
|
4968
4916
|
const capabilities = provider.getCapabilities();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payload-plugin-newsletter",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.3",
|
|
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.
|