payload-plugin-newsletter 0.17.0 → 0.17.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/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## [0.17.2] - 2025-07-29
2
+
3
+ ### Fixed
4
+ - **Broadcast Endpoints Registration** - Fixed endpoints not being accessible in Payload v3
5
+ - Moved broadcast endpoints from global endpoints to collection endpoints
6
+ - Endpoints are now properly registered on the broadcasts collection
7
+ - Fixes 404 errors for all broadcast endpoints (/preview, /test, /send, /schedule)
8
+ - Aligns with Payload v3 architecture where collection endpoints should be defined on the collection
9
+
10
+ ### Changed
11
+ - `createBroadcastManagementEndpoints` now returns empty array (kept for backward compatibility)
12
+ - Broadcast endpoints are defined directly in the broadcasts collection configuration
13
+
14
+ ## [0.17.1] - 2025-07-29
15
+
16
+ ### Fixed
17
+ - **Email Preview Endpoint Path** - Fixed incorrect path for broadcast preview endpoint
18
+ - Removed extra `/api` prefix from preview endpoint path
19
+ - Preview endpoint now correctly registers at `/{collectionSlug}/preview`
20
+ - Fixes 404 error when accessing email preview from the admin UI
21
+
1
22
  ## [0.17.0] - 2025-07-29
2
23
 
3
24
  ### Added
@@ -31,9 +31,19 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
33
  // src/types/newsletter.ts
34
+ var NewsletterProviderError;
34
35
  var init_newsletter = __esm({
35
36
  "src/types/newsletter.ts"() {
36
37
  "use strict";
38
+ NewsletterProviderError = class extends Error {
39
+ constructor(message, code, provider, details) {
40
+ super(message);
41
+ this.code = code;
42
+ this.provider = provider;
43
+ this.details = details;
44
+ this.name = "NewsletterProviderError";
45
+ }
46
+ };
37
47
  }
38
48
  });
39
49
 
@@ -1241,12 +1251,427 @@ async function getBroadcastConfig(req, pluginConfig) {
1241
1251
  }
1242
1252
  }
1243
1253
 
1254
+ // src/endpoints/broadcasts/send.ts
1255
+ init_types();
1256
+
1257
+ // src/utils/access.ts
1258
+ var isAdmin = (user, config) => {
1259
+ if (!user || user.collection !== "users") {
1260
+ return false;
1261
+ }
1262
+ if (config?.access?.isAdmin) {
1263
+ return config.access.isAdmin(user);
1264
+ }
1265
+ if (user.roles?.includes("admin")) {
1266
+ return true;
1267
+ }
1268
+ if (user.isAdmin === true) {
1269
+ return true;
1270
+ }
1271
+ if (user.role === "admin") {
1272
+ return true;
1273
+ }
1274
+ if (user.admin === true) {
1275
+ return true;
1276
+ }
1277
+ return false;
1278
+ };
1279
+ var adminOnly = (config) => ({ req }) => {
1280
+ const user = req.user;
1281
+ return isAdmin(user, config);
1282
+ };
1283
+ var adminOrSelf = (config) => ({ req, id }) => {
1284
+ const user = req.user;
1285
+ if (!user) {
1286
+ if (!id) {
1287
+ return {
1288
+ id: {
1289
+ equals: "unauthorized-no-access"
1290
+ }
1291
+ };
1292
+ }
1293
+ return false;
1294
+ }
1295
+ if (isAdmin(user, config)) {
1296
+ return true;
1297
+ }
1298
+ if (user.collection === "subscribers") {
1299
+ if (!id) {
1300
+ return {
1301
+ id: {
1302
+ equals: user.id
1303
+ }
1304
+ };
1305
+ }
1306
+ return id === user.id;
1307
+ }
1308
+ if (!id) {
1309
+ return {
1310
+ id: {
1311
+ equals: "unauthorized-no-access"
1312
+ }
1313
+ };
1314
+ }
1315
+ return false;
1316
+ };
1317
+
1318
+ // src/utils/auth.ts
1319
+ async function getAuthenticatedUser(req) {
1320
+ try {
1321
+ const me = await req.payload.find({
1322
+ collection: "users",
1323
+ where: {
1324
+ id: {
1325
+ equals: "me"
1326
+ // Special value in Payload to get current user
1327
+ }
1328
+ },
1329
+ limit: 1,
1330
+ depth: 0
1331
+ });
1332
+ return me.docs[0] || null;
1333
+ } catch {
1334
+ return null;
1335
+ }
1336
+ }
1337
+ async function requireAdmin(req, config) {
1338
+ const user = await getAuthenticatedUser(req);
1339
+ if (!user) {
1340
+ return {
1341
+ authorized: false,
1342
+ error: "Authentication required"
1343
+ };
1344
+ }
1345
+ if (!isAdmin(user, config)) {
1346
+ return {
1347
+ authorized: false,
1348
+ error: "Admin access required"
1349
+ };
1350
+ }
1351
+ return {
1352
+ authorized: true,
1353
+ user
1354
+ };
1355
+ }
1356
+
1357
+ // src/endpoints/broadcasts/send.ts
1358
+ var createSendBroadcastEndpoint = (config, collectionSlug) => {
1359
+ return {
1360
+ path: `/${collectionSlug}/:id/send`,
1361
+ method: "post",
1362
+ handler: async (req) => {
1363
+ try {
1364
+ const auth = await requireAdmin(req, config);
1365
+ if (!auth.authorized) {
1366
+ return Response.json({
1367
+ success: false,
1368
+ error: auth.error
1369
+ }, { status: 401 });
1370
+ }
1371
+ if (!config.features?.newsletterManagement?.enabled) {
1372
+ return Response.json({
1373
+ success: false,
1374
+ error: "Broadcast management is not enabled"
1375
+ }, { status: 400 });
1376
+ }
1377
+ const url = new URL(req.url || "", `http://localhost`);
1378
+ const pathParts = url.pathname.split("/");
1379
+ const id = pathParts[pathParts.length - 2];
1380
+ if (!id) {
1381
+ return Response.json({
1382
+ success: false,
1383
+ error: "Broadcast ID is required"
1384
+ }, { status: 400 });
1385
+ }
1386
+ const data = await (req.json?.() || Promise.resolve({}));
1387
+ const broadcastDoc = await req.payload.findByID({
1388
+ collection: collectionSlug,
1389
+ id,
1390
+ user: auth.user
1391
+ });
1392
+ if (!broadcastDoc || !broadcastDoc.providerId) {
1393
+ return Response.json({
1394
+ success: false,
1395
+ error: "Broadcast not found or not synced with provider"
1396
+ }, { status: 404 });
1397
+ }
1398
+ const providerConfig = await getBroadcastConfig(req, config);
1399
+ if (!providerConfig || !providerConfig.token) {
1400
+ return Response.json({
1401
+ success: false,
1402
+ error: "Broadcast provider not configured in Newsletter Settings or environment variables"
1403
+ }, { status: 500 });
1404
+ }
1405
+ const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
1406
+ const provider = new BroadcastApiProvider2(providerConfig);
1407
+ const broadcast = await provider.send(broadcastDoc.providerId, data);
1408
+ await req.payload.update({
1409
+ collection: collectionSlug,
1410
+ id,
1411
+ data: {
1412
+ sendStatus: "sending" /* SENDING */,
1413
+ sentAt: (/* @__PURE__ */ new Date()).toISOString()
1414
+ },
1415
+ user: auth.user
1416
+ });
1417
+ return Response.json({
1418
+ success: true,
1419
+ message: "Broadcast sent successfully",
1420
+ broadcast
1421
+ });
1422
+ } catch (error) {
1423
+ console.error("Failed to send broadcast:", error);
1424
+ if (error instanceof NewsletterProviderError) {
1425
+ return Response.json({
1426
+ success: false,
1427
+ error: error.message,
1428
+ code: error.code
1429
+ }, { status: error.code === "NOT_SUPPORTED" ? 501 : 500 });
1430
+ }
1431
+ return Response.json({
1432
+ success: false,
1433
+ error: "Failed to send broadcast"
1434
+ }, { status: 500 });
1435
+ }
1436
+ }
1437
+ };
1438
+ };
1439
+
1440
+ // src/endpoints/broadcasts/schedule.ts
1441
+ init_types();
1442
+ var createScheduleBroadcastEndpoint = (config, collectionSlug) => {
1443
+ return {
1444
+ path: `/${collectionSlug}/:id/schedule`,
1445
+ method: "post",
1446
+ handler: async (req) => {
1447
+ try {
1448
+ const auth = await requireAdmin(req, config);
1449
+ if (!auth.authorized) {
1450
+ return Response.json({
1451
+ success: false,
1452
+ error: auth.error
1453
+ }, { status: 401 });
1454
+ }
1455
+ if (!config.features?.newsletterManagement?.enabled) {
1456
+ return Response.json({
1457
+ success: false,
1458
+ error: "Broadcast management is not enabled"
1459
+ }, { status: 400 });
1460
+ }
1461
+ const url = new URL(req.url || "", `http://localhost`);
1462
+ const pathParts = url.pathname.split("/");
1463
+ const id = pathParts[pathParts.length - 2];
1464
+ if (!id) {
1465
+ return Response.json({
1466
+ success: false,
1467
+ error: "Broadcast ID is required"
1468
+ }, { status: 400 });
1469
+ }
1470
+ const data = await (req.json?.() || Promise.resolve({}));
1471
+ const { scheduledAt } = data;
1472
+ if (!scheduledAt) {
1473
+ return Response.json({
1474
+ success: false,
1475
+ error: "scheduledAt is required"
1476
+ }, { status: 400 });
1477
+ }
1478
+ const scheduledDate = new Date(scheduledAt);
1479
+ if (isNaN(scheduledDate.getTime())) {
1480
+ return Response.json({
1481
+ success: false,
1482
+ error: "Invalid scheduledAt date"
1483
+ }, { status: 400 });
1484
+ }
1485
+ if (scheduledDate <= /* @__PURE__ */ new Date()) {
1486
+ return Response.json({
1487
+ success: false,
1488
+ error: "scheduledAt must be in the future"
1489
+ }, { status: 400 });
1490
+ }
1491
+ const broadcastDoc = await req.payload.findByID({
1492
+ collection: collectionSlug,
1493
+ id,
1494
+ user: auth.user
1495
+ });
1496
+ if (!broadcastDoc || !broadcastDoc.providerId) {
1497
+ return Response.json({
1498
+ success: false,
1499
+ error: "Broadcast not found or not synced with provider"
1500
+ }, { status: 404 });
1501
+ }
1502
+ const providerConfig = config.providers?.broadcast;
1503
+ if (!providerConfig) {
1504
+ return Response.json({
1505
+ success: false,
1506
+ error: "Broadcast provider not configured"
1507
+ }, { status: 500 });
1508
+ }
1509
+ const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
1510
+ const provider = new BroadcastApiProvider2(providerConfig);
1511
+ const broadcast = await provider.schedule(broadcastDoc.providerId, scheduledDate);
1512
+ await req.payload.update({
1513
+ collection: collectionSlug,
1514
+ id,
1515
+ data: {
1516
+ sendStatus: "scheduled" /* SCHEDULED */,
1517
+ scheduledAt: scheduledDate.toISOString()
1518
+ },
1519
+ user: auth.user
1520
+ });
1521
+ return Response.json({
1522
+ success: true,
1523
+ message: `Broadcast scheduled for ${scheduledDate.toISOString()}`,
1524
+ broadcast
1525
+ });
1526
+ } catch (error) {
1527
+ console.error("Failed to schedule broadcast:", error);
1528
+ if (error instanceof NewsletterProviderError) {
1529
+ return Response.json({
1530
+ success: false,
1531
+ error: error.message,
1532
+ code: error.code
1533
+ }, { status: error.code === "NOT_SUPPORTED" ? 501 : 500 });
1534
+ }
1535
+ return Response.json({
1536
+ success: false,
1537
+ error: "Failed to schedule broadcast"
1538
+ }, { status: 500 });
1539
+ }
1540
+ }
1541
+ };
1542
+ };
1543
+
1544
+ // src/endpoints/broadcasts/test.ts
1545
+ var createTestBroadcastEndpoint = (config, collectionSlug) => {
1546
+ return {
1547
+ path: `/${collectionSlug}/:id/test`,
1548
+ method: "post",
1549
+ handler: async (req) => {
1550
+ try {
1551
+ const auth = await requireAdmin(req, config);
1552
+ if (!auth.authorized) {
1553
+ return Response.json({
1554
+ success: false,
1555
+ error: auth.error
1556
+ }, { status: 401 });
1557
+ }
1558
+ const url = new URL(req.url || "", `http://localhost`);
1559
+ const pathParts = url.pathname.split("/");
1560
+ const id = pathParts[pathParts.length - 2];
1561
+ if (!id) {
1562
+ return Response.json({
1563
+ success: false,
1564
+ error: "Broadcast ID is required"
1565
+ }, { status: 400 });
1566
+ }
1567
+ const data = await (req.json?.() || Promise.resolve({}));
1568
+ const testEmail = data.email || auth.user.email;
1569
+ if (!testEmail) {
1570
+ return Response.json({
1571
+ success: false,
1572
+ error: "No email address available for test send"
1573
+ }, { status: 400 });
1574
+ }
1575
+ const broadcast = await req.payload.findByID({
1576
+ collection: collectionSlug,
1577
+ id,
1578
+ user: auth.user
1579
+ });
1580
+ if (!broadcast) {
1581
+ return Response.json({
1582
+ success: false,
1583
+ error: "Broadcast not found"
1584
+ }, { status: 404 });
1585
+ }
1586
+ const htmlContent = await convertToEmailSafeHtml(broadcast.content, {
1587
+ wrapInTemplate: true,
1588
+ preheader: broadcast.preheader,
1589
+ customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
1590
+ });
1591
+ const emailService = req.payload.newsletterEmailService;
1592
+ if (!emailService) {
1593
+ return Response.json({
1594
+ success: false,
1595
+ error: "Email service is not configured"
1596
+ }, { status: 500 });
1597
+ }
1598
+ const providerConfig = config.providers.default === "resend" ? config.providers.resend : config.providers.broadcast;
1599
+ const fromEmail = providerConfig?.fromAddress || providerConfig?.fromEmail || "noreply@example.com";
1600
+ const fromName = providerConfig?.fromName || "Newsletter";
1601
+ const replyTo = broadcast.settings?.replyTo || providerConfig?.replyTo;
1602
+ await emailService.send({
1603
+ to: testEmail,
1604
+ from: fromEmail,
1605
+ fromName,
1606
+ replyTo,
1607
+ subject: `[TEST] ${broadcast.subject}`,
1608
+ html: htmlContent,
1609
+ trackOpens: false,
1610
+ trackClicks: false
1611
+ });
1612
+ return Response.json({
1613
+ success: true,
1614
+ message: `Test email sent to ${testEmail}`
1615
+ });
1616
+ } catch (error) {
1617
+ console.error("Failed to send test broadcast:", error);
1618
+ return Response.json({
1619
+ success: false,
1620
+ error: "Failed to send test email"
1621
+ }, { status: 500 });
1622
+ }
1623
+ }
1624
+ };
1625
+ };
1626
+
1627
+ // src/endpoints/broadcasts/preview.ts
1628
+ var createBroadcastPreviewEndpoint = (config, collectionSlug) => {
1629
+ return {
1630
+ path: `/${collectionSlug}/preview`,
1631
+ method: "post",
1632
+ handler: async (req) => {
1633
+ try {
1634
+ const data = await (req.json?.() || Promise.resolve({}));
1635
+ const { content, preheader, subject } = data;
1636
+ if (!content) {
1637
+ return Response.json({
1638
+ success: false,
1639
+ error: "Content is required for preview"
1640
+ }, { status: 400 });
1641
+ }
1642
+ const mediaUrl = req.payload.config.serverURL ? `${req.payload.config.serverURL}/api/media` : "/api/media";
1643
+ const htmlContent = await convertToEmailSafeHtml(content, {
1644
+ wrapInTemplate: true,
1645
+ preheader,
1646
+ mediaUrl,
1647
+ customBlockConverter: config.customizations?.broadcasts?.customBlockConverter
1648
+ });
1649
+ return Response.json({
1650
+ success: true,
1651
+ preview: {
1652
+ subject: subject || "Preview",
1653
+ preheader: preheader || "",
1654
+ html: htmlContent
1655
+ }
1656
+ });
1657
+ } catch (error) {
1658
+ console.error("Failed to generate email preview:", error);
1659
+ return Response.json({
1660
+ success: false,
1661
+ error: "Failed to generate email preview"
1662
+ }, { status: 500 });
1663
+ }
1664
+ }
1665
+ };
1666
+ };
1667
+
1244
1668
  // src/collections/Broadcasts.ts
1245
1669
  var createBroadcastsCollection = (pluginConfig) => {
1246
1670
  const hasProviders = !!(pluginConfig.providers?.broadcast || pluginConfig.providers?.resend);
1247
1671
  const customizations = pluginConfig.customizations?.broadcasts;
1672
+ const collectionSlug = "broadcasts";
1248
1673
  return {
1249
- slug: "broadcasts",
1674
+ slug: collectionSlug,
1250
1675
  access: {
1251
1676
  read: () => true,
1252
1677
  // Public read access
@@ -1275,6 +1700,12 @@ var createBroadcastsCollection = (pluginConfig) => {
1275
1700
  description: "Individual email campaigns sent to subscribers",
1276
1701
  defaultColumns: ["subject", "_status", "sendStatus", "sentAt", "recipientCount"]
1277
1702
  },
1703
+ endpoints: [
1704
+ createSendBroadcastEndpoint(pluginConfig, collectionSlug),
1705
+ createScheduleBroadcastEndpoint(pluginConfig, collectionSlug),
1706
+ createTestBroadcastEndpoint(pluginConfig, collectionSlug),
1707
+ createBroadcastPreviewEndpoint(pluginConfig, collectionSlug)
1708
+ ],
1278
1709
  fields: [
1279
1710
  {
1280
1711
  name: "subject",
@@ -1820,67 +2251,6 @@ var createBroadcastsCollection = (pluginConfig) => {
1820
2251
  };
1821
2252
  };
1822
2253
 
1823
- // src/utils/access.ts
1824
- var isAdmin = (user, config) => {
1825
- if (!user || user.collection !== "users") {
1826
- return false;
1827
- }
1828
- if (config?.access?.isAdmin) {
1829
- return config.access.isAdmin(user);
1830
- }
1831
- if (user.roles?.includes("admin")) {
1832
- return true;
1833
- }
1834
- if (user.isAdmin === true) {
1835
- return true;
1836
- }
1837
- if (user.role === "admin") {
1838
- return true;
1839
- }
1840
- if (user.admin === true) {
1841
- return true;
1842
- }
1843
- return false;
1844
- };
1845
- var adminOnly = (config) => ({ req }) => {
1846
- const user = req.user;
1847
- return isAdmin(user, config);
1848
- };
1849
- var adminOrSelf = (config) => ({ req, id }) => {
1850
- const user = req.user;
1851
- if (!user) {
1852
- if (!id) {
1853
- return {
1854
- id: {
1855
- equals: "unauthorized-no-access"
1856
- }
1857
- };
1858
- }
1859
- return false;
1860
- }
1861
- if (isAdmin(user, config)) {
1862
- return true;
1863
- }
1864
- if (user.collection === "subscribers") {
1865
- if (!id) {
1866
- return {
1867
- id: {
1868
- equals: user.id
1869
- }
1870
- };
1871
- }
1872
- return id === user.id;
1873
- }
1874
- if (!id) {
1875
- return {
1876
- id: {
1877
- equals: "unauthorized-no-access"
1878
- }
1879
- };
1880
- }
1881
- return false;
1882
- };
1883
-
1884
2254
  // src/emails/render.tsx
1885
2255
  var import_render = require("@react-email/render");
1886
2256