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