payload-plugin-newsletter 0.6.2 → 0.8.0

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/index.cjs CHANGED
@@ -31,7 +31,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
33
  default: () => newsletterPlugin,
34
- newsletterPlugin: () => newsletterPlugin
34
+ getServerSideAuth: () => getServerSideAuth,
35
+ getTokenFromRequest: () => getTokenFromRequest,
36
+ isAuthenticated: () => isAuthenticated,
37
+ newsletterPlugin: () => newsletterPlugin,
38
+ requireAuth: () => requireAuth,
39
+ verifyToken: () => verifyToken
35
40
  });
36
41
  module.exports = __toCommonJS(src_exports);
37
42
 
@@ -96,6 +101,238 @@ var adminOrSelf = (config) => ({ req, id }) => {
96
101
  return false;
97
102
  };
98
103
 
104
+ // src/emails/render.tsx
105
+ var import_render = require("@react-email/render");
106
+
107
+ // src/emails/MagicLink.tsx
108
+ var import_components = require("@react-email/components");
109
+
110
+ // src/emails/styles.ts
111
+ var styles = {
112
+ main: {
113
+ backgroundColor: "#f6f9fc",
114
+ fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif'
115
+ },
116
+ container: {
117
+ backgroundColor: "#ffffff",
118
+ border: "1px solid #f0f0f0",
119
+ borderRadius: "5px",
120
+ margin: "0 auto",
121
+ padding: "45px",
122
+ marginBottom: "64px",
123
+ maxWidth: "500px"
124
+ },
125
+ heading: {
126
+ fontSize: "24px",
127
+ letterSpacing: "-0.5px",
128
+ lineHeight: "1.3",
129
+ fontWeight: "600",
130
+ color: "#484848",
131
+ margin: "0 0 20px",
132
+ padding: "0"
133
+ },
134
+ text: {
135
+ fontSize: "16px",
136
+ lineHeight: "26px",
137
+ fontWeight: "400",
138
+ color: "#484848",
139
+ margin: "16px 0"
140
+ },
141
+ button: {
142
+ backgroundColor: "#000000",
143
+ borderRadius: "5px",
144
+ color: "#fff",
145
+ fontSize: "16px",
146
+ fontWeight: "bold",
147
+ textDecoration: "none",
148
+ textAlign: "center",
149
+ display: "block",
150
+ width: "100%",
151
+ padding: "14px 20px",
152
+ margin: "30px 0"
153
+ },
154
+ link: {
155
+ color: "#2754C5",
156
+ fontSize: "14px",
157
+ textDecoration: "underline",
158
+ wordBreak: "break-all"
159
+ },
160
+ hr: {
161
+ borderColor: "#e6ebf1",
162
+ margin: "30px 0"
163
+ },
164
+ footer: {
165
+ fontSize: "14px",
166
+ lineHeight: "24px",
167
+ color: "#9ca2ac",
168
+ textAlign: "center",
169
+ margin: "0"
170
+ },
171
+ code: {
172
+ display: "inline-block",
173
+ padding: "16px",
174
+ width: "100%",
175
+ backgroundColor: "#f4f4f4",
176
+ borderRadius: "5px",
177
+ border: "1px solid #eee",
178
+ fontSize: "14px",
179
+ fontFamily: "monospace",
180
+ textAlign: "center",
181
+ margin: "24px 0"
182
+ }
183
+ };
184
+
185
+ // src/emails/MagicLink.tsx
186
+ var import_jsx_runtime = require("react/jsx-runtime");
187
+ var MagicLinkEmail = ({
188
+ magicLink,
189
+ email,
190
+ siteName = "Newsletter",
191
+ expiresIn = "24 hours"
192
+ }) => {
193
+ const previewText = `Sign in to ${siteName}`;
194
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Html, { children: [
195
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Head, {}),
196
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Preview, { children: previewText }),
197
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Body, { style: styles.main, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Container, { style: styles.container, children: [
198
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Text, { style: styles.heading, children: [
199
+ "Sign in to ",
200
+ siteName
201
+ ] }),
202
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Text, { style: styles.text, children: [
203
+ "Hi ",
204
+ email.split("@")[0],
205
+ ","
206
+ ] }),
207
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Text, { style: styles.text, children: [
208
+ "We received a request to sign in to your ",
209
+ siteName,
210
+ " account. Click the button below to complete your sign in:"
211
+ ] }),
212
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Button, { href: magicLink, style: styles.button, children: [
213
+ "Sign in to ",
214
+ siteName
215
+ ] }),
216
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Text, { style: styles.text, children: "Or copy and paste this URL into your browser:" }),
217
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { style: styles.code, children: magicLink }),
218
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_components.Hr, { style: styles.hr }),
219
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_components.Text, { style: styles.footer, children: [
220
+ "This link will expire in ",
221
+ expiresIn,
222
+ ". If you didn't request this email, you can safely ignore it."
223
+ ] })
224
+ ] }) })
225
+ ] });
226
+ };
227
+
228
+ // src/emails/Welcome.tsx
229
+ var import_components2 = require("@react-email/components");
230
+ var import_jsx_runtime2 = require("react/jsx-runtime");
231
+ var WelcomeEmail = ({
232
+ email,
233
+ siteName = "Newsletter",
234
+ preferencesUrl
235
+ }) => {
236
+ const previewText = `Welcome to ${siteName}!`;
237
+ const firstName = email.split("@")[0];
238
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Html, { children: [
239
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Head, {}),
240
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Preview, { children: previewText }),
241
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Body, { style: styles.main, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Container, { style: styles.container, children: [
242
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Text, { style: styles.heading, children: [
243
+ "Welcome to ",
244
+ siteName,
245
+ "! \u{1F389}"
246
+ ] }),
247
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Text, { style: styles.text, children: [
248
+ "Hi ",
249
+ firstName,
250
+ ","
251
+ ] }),
252
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Text, { style: styles.text, children: [
253
+ "Thanks for subscribing to ",
254
+ siteName,
255
+ "! We're excited to have you as part of our community."
256
+ ] }),
257
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Text, { style: styles.text, children: "You'll receive our newsletter based on your preferences. Speaking of which, you can update your preferences anytime:" }),
258
+ preferencesUrl && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Button, { href: preferencesUrl, style: styles.button, children: "Manage Preferences" }),
259
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Text, { style: styles.text, children: "Here's what you can expect from us:" }),
260
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_components2.Text, { style: styles.text, children: [
261
+ "\u2022 Regular updates based on your chosen frequency",
262
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("br", {}),
263
+ "\u2022 Content tailored to your interests",
264
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("br", {}),
265
+ "\u2022 Easy unsubscribe options in every email",
266
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("br", {}),
267
+ "\u2022 Your privacy respected always"
268
+ ] }),
269
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Hr, { style: styles.hr }),
270
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_components2.Text, { style: styles.footer, children: "If you have any questions, feel free to reply to this email. We're here to help!" })
271
+ ] }) })
272
+ ] });
273
+ };
274
+
275
+ // src/emails/SignIn.tsx
276
+ var import_jsx_runtime3 = require("react/jsx-runtime");
277
+ var SignInEmail = (props) => {
278
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MagicLinkEmail, { ...props });
279
+ };
280
+
281
+ // src/emails/render.tsx
282
+ var import_jsx_runtime4 = require("react/jsx-runtime");
283
+ async function renderEmail(template, data) {
284
+ try {
285
+ switch (template) {
286
+ case "magic-link": {
287
+ const magicLinkData = data;
288
+ return (0, import_render.render)(
289
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
290
+ MagicLinkEmail,
291
+ {
292
+ magicLink: magicLinkData.magicLink || magicLinkData.verificationUrl || magicLinkData.magicLinkUrl || "",
293
+ email: magicLinkData.email || "",
294
+ siteName: magicLinkData.siteName,
295
+ expiresIn: magicLinkData.expiresIn
296
+ }
297
+ )
298
+ );
299
+ }
300
+ case "signin": {
301
+ const signinData = data;
302
+ return (0, import_render.render)(
303
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
304
+ SignInEmail,
305
+ {
306
+ magicLink: signinData.magicLink || signinData.verificationUrl || signinData.magicLinkUrl || "",
307
+ email: signinData.email || "",
308
+ siteName: signinData.siteName,
309
+ expiresIn: signinData.expiresIn
310
+ }
311
+ )
312
+ );
313
+ }
314
+ case "welcome": {
315
+ const welcomeData = data;
316
+ return (0, import_render.render)(
317
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
318
+ WelcomeEmail,
319
+ {
320
+ email: welcomeData.email || "",
321
+ siteName: welcomeData.siteName,
322
+ preferencesUrl: welcomeData.preferencesUrl
323
+ }
324
+ )
325
+ );
326
+ }
327
+ default:
328
+ throw new Error(`Unknown email template: ${template}`);
329
+ }
330
+ } catch (error) {
331
+ console.error(`Failed to render email template ${template}:`, error);
332
+ throw error;
333
+ }
334
+ }
335
+
99
336
  // src/collections/Subscribers.ts
100
337
  var createSubscribersCollection = (pluginConfig) => {
101
338
  const slug = pluginConfig.subscribersSlug || "subscribers";
@@ -305,7 +542,24 @@ var createSubscribersCollection = (pluginConfig) => {
305
542
  }
306
543
  if (doc.subscriptionStatus === "active" && emailService) {
307
544
  try {
308
- } catch {
545
+ const settings = await req.payload.findGlobal({
546
+ slug: pluginConfig.settingsSlug || "newsletter-settings"
547
+ });
548
+ const serverURL = req.payload.config.serverURL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "";
549
+ const html = await renderEmail("welcome", {
550
+ email: doc.email,
551
+ siteName: settings?.brandSettings?.siteName || "Newsletter",
552
+ preferencesUrl: `${serverURL}/account/preferences`
553
+ // This could be customized
554
+ });
555
+ await emailService.send({
556
+ to: doc.email,
557
+ subject: settings?.brandSettings?.siteName ? `Welcome to ${settings.brandSettings.siteName}!` : "Welcome!",
558
+ html
559
+ });
560
+ console.log(`Welcome email sent to: ${doc.email}`);
561
+ } catch (error) {
562
+ console.error("Failed to send welcome email:", error);
309
563
  }
310
564
  }
311
565
  if (pluginConfig.hooks?.afterSubscribe) {
@@ -1110,6 +1364,86 @@ function validateSubscriberData(data) {
1110
1364
  };
1111
1365
  }
1112
1366
 
1367
+ // src/utils/jwt.ts
1368
+ var import_jsonwebtoken = __toESM(require("jsonwebtoken"), 1);
1369
+ function getJWTSecret() {
1370
+ const secret = process.env.JWT_SECRET || process.env.PAYLOAD_SECRET;
1371
+ if (!secret) {
1372
+ console.warn(
1373
+ "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."
1374
+ );
1375
+ return "INSECURE_DEVELOPMENT_SECRET_PLEASE_SET_JWT_SECRET";
1376
+ }
1377
+ return secret;
1378
+ }
1379
+ function generateMagicLinkToken(subscriberId, email, config) {
1380
+ const payload = {
1381
+ subscriberId,
1382
+ email,
1383
+ type: "magic-link"
1384
+ };
1385
+ const expiresIn = config.auth?.tokenExpiration || "7d";
1386
+ return import_jsonwebtoken.default.sign(payload, getJWTSecret(), {
1387
+ expiresIn,
1388
+ issuer: "payload-newsletter-plugin"
1389
+ });
1390
+ }
1391
+ function verifyMagicLinkToken(token) {
1392
+ try {
1393
+ const payload = import_jsonwebtoken.default.verify(token, getJWTSecret(), {
1394
+ issuer: "payload-newsletter-plugin"
1395
+ });
1396
+ if (payload.type !== "magic-link") {
1397
+ throw new Error("Invalid token type");
1398
+ }
1399
+ return payload;
1400
+ } catch (error) {
1401
+ if (error instanceof Error && error.name === "TokenExpiredError") {
1402
+ throw new Error("Magic link has expired. Please request a new one.");
1403
+ }
1404
+ if (error instanceof Error && error.name === "JsonWebTokenError") {
1405
+ throw new Error("Invalid magic link token");
1406
+ }
1407
+ throw error;
1408
+ }
1409
+ }
1410
+ function generateSessionToken(subscriberId, email) {
1411
+ const payload = {
1412
+ subscriberId,
1413
+ email,
1414
+ type: "session"
1415
+ };
1416
+ return import_jsonwebtoken.default.sign(payload, getJWTSecret(), {
1417
+ expiresIn: "30d",
1418
+ issuer: "payload-newsletter-plugin"
1419
+ });
1420
+ }
1421
+ function verifySessionToken(token) {
1422
+ try {
1423
+ const payload = import_jsonwebtoken.default.verify(token, getJWTSecret(), {
1424
+ issuer: "payload-newsletter-plugin"
1425
+ });
1426
+ if (payload.type !== "session") {
1427
+ throw new Error("Invalid token type");
1428
+ }
1429
+ return payload;
1430
+ } catch (error) {
1431
+ if (error instanceof Error && error.name === "TokenExpiredError") {
1432
+ throw new Error("Session has expired. Please sign in again.");
1433
+ }
1434
+ if (error instanceof Error && error.name === "JsonWebTokenError") {
1435
+ throw new Error("Invalid session token");
1436
+ }
1437
+ throw error;
1438
+ }
1439
+ }
1440
+ function generateMagicLinkURL(token, baseURL, config) {
1441
+ const path = config.auth?.magicLinkPath || "/newsletter/verify";
1442
+ const url = new URL(path, baseURL);
1443
+ url.searchParams.set("token", token);
1444
+ return url.toString();
1445
+ }
1446
+
1113
1447
  // src/endpoints/subscribe.ts
1114
1448
  var createSubscribeEndpoint = (config) => {
1115
1449
  return {
@@ -1233,6 +1567,33 @@ var createSubscribeEndpoint = (config) => {
1233
1567
  if (config.features?.surveys?.enabled && surveyResponses) {
1234
1568
  }
1235
1569
  if (settings?.subscriptionSettings?.requireDoubleOptIn) {
1570
+ try {
1571
+ const token = generateMagicLinkToken(
1572
+ String(subscriber.id),
1573
+ subscriber.email,
1574
+ config
1575
+ );
1576
+ const serverURL = req.payload.config.serverURL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "";
1577
+ const magicLinkURL = generateMagicLinkURL(token, serverURL, config);
1578
+ const emailService = req.payload.newsletterEmailService;
1579
+ if (emailService) {
1580
+ const html = await renderEmail("magic-link", {
1581
+ magicLink: magicLinkURL,
1582
+ email: subscriber.email,
1583
+ siteName: settings?.brandSettings?.siteName || "Newsletter",
1584
+ expiresIn: config.auth?.tokenExpiration || "7d"
1585
+ });
1586
+ await emailService.send({
1587
+ to: subscriber.email,
1588
+ subject: settings?.brandSettings?.siteName ? `Verify your email for ${settings.brandSettings.siteName}` : "Verify your email",
1589
+ html
1590
+ });
1591
+ } else {
1592
+ console.warn("Email service not initialized, cannot send magic link");
1593
+ }
1594
+ } catch (error) {
1595
+ console.error("Failed to send magic link email:", error);
1596
+ }
1236
1597
  }
1237
1598
  res.json({
1238
1599
  success: true,
@@ -1253,68 +1614,6 @@ var createSubscribeEndpoint = (config) => {
1253
1614
  };
1254
1615
  };
1255
1616
 
1256
- // src/utils/jwt.ts
1257
- var import_jsonwebtoken = __toESM(require("jsonwebtoken"), 1);
1258
- function getJWTSecret() {
1259
- const secret = process.env.JWT_SECRET || process.env.PAYLOAD_SECRET;
1260
- if (!secret) {
1261
- console.warn(
1262
- "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."
1263
- );
1264
- return "INSECURE_DEVELOPMENT_SECRET_PLEASE_SET_JWT_SECRET";
1265
- }
1266
- return secret;
1267
- }
1268
- function verifyMagicLinkToken(token) {
1269
- try {
1270
- const payload = import_jsonwebtoken.default.verify(token, getJWTSecret(), {
1271
- issuer: "payload-newsletter-plugin"
1272
- });
1273
- if (payload.type !== "magic-link") {
1274
- throw new Error("Invalid token type");
1275
- }
1276
- return payload;
1277
- } catch (error) {
1278
- if (error instanceof Error && error.name === "TokenExpiredError") {
1279
- throw new Error("Magic link has expired. Please request a new one.");
1280
- }
1281
- if (error instanceof Error && error.name === "JsonWebTokenError") {
1282
- throw new Error("Invalid magic link token");
1283
- }
1284
- throw error;
1285
- }
1286
- }
1287
- function generateSessionToken(subscriberId, email) {
1288
- const payload = {
1289
- subscriberId,
1290
- email,
1291
- type: "session"
1292
- };
1293
- return import_jsonwebtoken.default.sign(payload, getJWTSecret(), {
1294
- expiresIn: "30d",
1295
- issuer: "payload-newsletter-plugin"
1296
- });
1297
- }
1298
- function verifySessionToken(token) {
1299
- try {
1300
- const payload = import_jsonwebtoken.default.verify(token, getJWTSecret(), {
1301
- issuer: "payload-newsletter-plugin"
1302
- });
1303
- if (payload.type !== "session") {
1304
- throw new Error("Invalid token type");
1305
- }
1306
- return payload;
1307
- } catch (error) {
1308
- if (error instanceof Error && error.name === "TokenExpiredError") {
1309
- throw new Error("Session has expired. Please sign in again.");
1310
- }
1311
- if (error instanceof Error && error.name === "JsonWebTokenError") {
1312
- throw new Error("Invalid session token");
1313
- }
1314
- throw error;
1315
- }
1316
- }
1317
-
1318
1617
  // src/endpoints/verify-magic-link.ts
1319
1618
  var createVerifyMagicLinkEndpoint = (config) => {
1320
1619
  return {
@@ -1366,6 +1665,7 @@ var createVerifyMagicLinkEndpoint = (config) => {
1366
1665
  id: subscriber.id,
1367
1666
  email: subscriber.email
1368
1667
  };
1668
+ let isNewlyActivated = false;
1369
1669
  if (subscriber.subscriptionStatus === "pending") {
1370
1670
  await req.payload.update({
1371
1671
  collection: config.subscribersSlug || "subscribers",
@@ -1376,6 +1676,7 @@ var createVerifyMagicLinkEndpoint = (config) => {
1376
1676
  overrideAccess: false,
1377
1677
  user: syntheticUser
1378
1678
  });
1679
+ isNewlyActivated = true;
1379
1680
  }
1380
1681
  await req.payload.update({
1381
1682
  collection: config.subscribersSlug || "subscribers",
@@ -1391,6 +1692,40 @@ var createVerifyMagicLinkEndpoint = (config) => {
1391
1692
  String(subscriber.id),
1392
1693
  subscriber.email
1393
1694
  );
1695
+ if (isNewlyActivated) {
1696
+ try {
1697
+ const emailService = req.payload.newsletterEmailService;
1698
+ if (emailService) {
1699
+ const settings = await req.payload.findGlobal({
1700
+ slug: config.settingsSlug || "newsletter-settings"
1701
+ });
1702
+ const serverURL = req.payload.config.serverURL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "";
1703
+ const html = await renderEmail("welcome", {
1704
+ email: subscriber.email,
1705
+ siteName: settings?.brandSettings?.siteName || "Newsletter",
1706
+ preferencesUrl: `${serverURL}/account/preferences`
1707
+ // This could be customized
1708
+ });
1709
+ await emailService.send({
1710
+ to: subscriber.email,
1711
+ subject: settings?.brandSettings?.siteName ? `Welcome to ${settings.brandSettings.siteName}!` : "Welcome!",
1712
+ html
1713
+ });
1714
+ } else {
1715
+ console.warn("Email service not initialized, cannot send welcome email");
1716
+ }
1717
+ } catch (error) {
1718
+ console.error("Failed to send welcome email:", error);
1719
+ }
1720
+ }
1721
+ res.cookie("newsletter-auth", sessionToken, {
1722
+ httpOnly: true,
1723
+ secure: process.env.NODE_ENV === "production",
1724
+ sameSite: "lax",
1725
+ path: "/",
1726
+ maxAge: 30 * 24 * 60 * 60 * 1e3
1727
+ // 30 days
1728
+ });
1394
1729
  res.json({
1395
1730
  success: true,
1396
1731
  sessionToken,
@@ -1558,8 +1893,8 @@ var createUnsubscribeEndpoint = (config) => {
1558
1893
  let subscriber;
1559
1894
  if (token) {
1560
1895
  try {
1561
- const jwt2 = await import("jsonwebtoken");
1562
- const payload = jwt2.verify(
1896
+ const jwt3 = await import("jsonwebtoken");
1897
+ const payload = jwt3.verify(
1563
1898
  token,
1564
1899
  process.env.JWT_SECRET || process.env.PAYLOAD_SECRET || ""
1565
1900
  );
@@ -1640,6 +1975,224 @@ var createUnsubscribeEndpoint = (config) => {
1640
1975
  };
1641
1976
  };
1642
1977
 
1978
+ // src/utils/rate-limiter.ts
1979
+ var RateLimiter = class {
1980
+ constructor(options) {
1981
+ this.attempts = /* @__PURE__ */ new Map();
1982
+ this.options = options;
1983
+ }
1984
+ async checkLimit(key) {
1985
+ const now = Date.now();
1986
+ const record = this.attempts.get(key);
1987
+ if (!record || record.resetTime < now) {
1988
+ this.attempts.set(key, {
1989
+ count: 1,
1990
+ resetTime: now + this.options.windowMs
1991
+ });
1992
+ return true;
1993
+ }
1994
+ if (record.count >= this.options.maxAttempts) {
1995
+ return false;
1996
+ }
1997
+ record.count++;
1998
+ return true;
1999
+ }
2000
+ async incrementAttempt(key) {
2001
+ const now = Date.now();
2002
+ const record = this.attempts.get(key);
2003
+ if (!record || record.resetTime < now) {
2004
+ this.attempts.set(key, {
2005
+ count: 1,
2006
+ resetTime: now + this.options.windowMs
2007
+ });
2008
+ } else {
2009
+ record.count++;
2010
+ }
2011
+ }
2012
+ async reset(key) {
2013
+ this.attempts.delete(key);
2014
+ }
2015
+ async resetAll() {
2016
+ this.attempts.clear();
2017
+ }
2018
+ };
2019
+
2020
+ // src/endpoints/signin.ts
2021
+ var signinRateLimiter = new RateLimiter({
2022
+ maxAttempts: 5,
2023
+ windowMs: 15 * 60 * 1e3,
2024
+ // 15 minutes
2025
+ prefix: "signin"
2026
+ });
2027
+ var createSigninEndpoint = (config) => {
2028
+ return {
2029
+ path: "/newsletter/signin",
2030
+ method: "post",
2031
+ handler: async (req, res) => {
2032
+ try {
2033
+ const { email } = req.body;
2034
+ const validation = validateSubscriberData({ email });
2035
+ if (!validation.valid) {
2036
+ return res.status(400).json({
2037
+ success: false,
2038
+ errors: validation.errors
2039
+ });
2040
+ }
2041
+ const rateLimitKey = `signin:${email.toLowerCase()}`;
2042
+ const allowed = await signinRateLimiter.checkLimit(rateLimitKey);
2043
+ if (!allowed) {
2044
+ return res.status(429).json({
2045
+ success: false,
2046
+ error: "Too many sign-in attempts. Please try again later."
2047
+ });
2048
+ }
2049
+ const result = await req.payload.find({
2050
+ collection: config.subscribersSlug || "subscribers",
2051
+ where: {
2052
+ email: { equals: email.toLowerCase() },
2053
+ subscriptionStatus: { equals: "active" }
2054
+ },
2055
+ limit: 1,
2056
+ overrideAccess: true
2057
+ // Need to check subscriber exists
2058
+ });
2059
+ if (result.docs.length === 0) {
2060
+ return res.status(404).json({
2061
+ success: false,
2062
+ error: "Email not found. Please subscribe first."
2063
+ });
2064
+ }
2065
+ const subscriber = result.docs[0];
2066
+ const token = generateMagicLinkToken(
2067
+ String(subscriber.id),
2068
+ subscriber.email,
2069
+ config
2070
+ );
2071
+ const serverURL = req.payload.config.serverURL || process.env.PAYLOAD_PUBLIC_SERVER_URL || "";
2072
+ const magicLinkURL = generateMagicLinkURL(token, serverURL, config);
2073
+ const emailService = req.payload.newsletterEmailService;
2074
+ if (emailService) {
2075
+ const settings = await req.payload.findGlobal({
2076
+ slug: config.settingsSlug || "newsletter-settings"
2077
+ });
2078
+ const html = await renderEmail("signin", {
2079
+ magicLink: magicLinkURL,
2080
+ email: subscriber.email,
2081
+ siteName: settings?.brandSettings?.siteName || "Newsletter",
2082
+ expiresIn: config.auth?.tokenExpiration || "7d"
2083
+ });
2084
+ await emailService.send({
2085
+ to: subscriber.email,
2086
+ subject: settings?.brandSettings?.siteName ? `Sign in to ${settings.brandSettings.siteName}` : "Sign in to your account",
2087
+ html
2088
+ });
2089
+ } else {
2090
+ console.warn("Email service not initialized, cannot send sign-in link");
2091
+ }
2092
+ res.json({
2093
+ success: true,
2094
+ message: "Check your email for the sign-in link"
2095
+ });
2096
+ } catch (error) {
2097
+ console.error("Sign-in error:", error);
2098
+ res.status(500).json({
2099
+ success: false,
2100
+ error: "Failed to process sign-in request"
2101
+ });
2102
+ }
2103
+ }
2104
+ };
2105
+ };
2106
+
2107
+ // src/endpoints/me.ts
2108
+ var createMeEndpoint = (config) => {
2109
+ return {
2110
+ path: "/newsletter/me",
2111
+ method: "get",
2112
+ handler: async (req, res) => {
2113
+ try {
2114
+ const token = req.cookies?.["newsletter-auth"];
2115
+ if (!token) {
2116
+ return res.status(401).json({
2117
+ success: false,
2118
+ error: "Not authenticated"
2119
+ });
2120
+ }
2121
+ let payload;
2122
+ try {
2123
+ payload = verifySessionToken(token);
2124
+ } catch {
2125
+ return res.status(401).json({
2126
+ success: false,
2127
+ error: "Invalid or expired session"
2128
+ });
2129
+ }
2130
+ const subscriber = await req.payload.findByID({
2131
+ collection: config.subscribersSlug || "subscribers",
2132
+ id: payload.subscriberId,
2133
+ overrideAccess: true
2134
+ // Need to get subscriber data
2135
+ });
2136
+ if (!subscriber || subscriber.subscriptionStatus !== "active") {
2137
+ return res.status(401).json({
2138
+ success: false,
2139
+ error: "Not authenticated"
2140
+ });
2141
+ }
2142
+ res.json({
2143
+ success: true,
2144
+ subscriber: {
2145
+ id: subscriber.id,
2146
+ email: subscriber.email,
2147
+ name: subscriber.name,
2148
+ status: subscriber.subscriptionStatus,
2149
+ preferences: {
2150
+ frequency: subscriber.emailPreferences?.frequency,
2151
+ categories: subscriber.emailPreferences?.categories
2152
+ },
2153
+ createdAt: subscriber.createdAt,
2154
+ updatedAt: subscriber.updatedAt
2155
+ }
2156
+ });
2157
+ } catch (error) {
2158
+ console.error("Me endpoint error:", error);
2159
+ res.status(500).json({
2160
+ success: false,
2161
+ error: "Internal server error"
2162
+ });
2163
+ }
2164
+ }
2165
+ };
2166
+ };
2167
+
2168
+ // src/endpoints/signout.ts
2169
+ var createSignoutEndpoint = (_config) => {
2170
+ return {
2171
+ path: "/newsletter/signout",
2172
+ method: "post",
2173
+ handler: (req, res) => {
2174
+ try {
2175
+ res.clearCookie("newsletter-auth", {
2176
+ httpOnly: true,
2177
+ secure: process.env.NODE_ENV === "production",
2178
+ sameSite: "lax",
2179
+ path: "/"
2180
+ });
2181
+ res.json({
2182
+ success: true,
2183
+ message: "Signed out successfully"
2184
+ });
2185
+ } catch (error) {
2186
+ console.error("Signout error:", error);
2187
+ res.status(500).json({
2188
+ success: false,
2189
+ error: "Failed to sign out"
2190
+ });
2191
+ }
2192
+ }
2193
+ };
2194
+ };
2195
+
1643
2196
  // src/endpoints/index.ts
1644
2197
  function createNewsletterEndpoints(config) {
1645
2198
  const endpoints = [
@@ -1650,7 +2203,10 @@ function createNewsletterEndpoints(config) {
1650
2203
  endpoints.push(
1651
2204
  createVerifyMagicLinkEndpoint(config),
1652
2205
  createPreferencesEndpoint(config),
1653
- createUpdatePreferencesEndpoint(config)
2206
+ createUpdatePreferencesEndpoint(config),
2207
+ createSigninEndpoint(config),
2208
+ createMeEndpoint(config),
2209
+ createSignoutEndpoint(config)
1654
2210
  );
1655
2211
  }
1656
2212
  return endpoints;
@@ -1825,6 +2381,83 @@ function createMarkdownFieldInternal(config) {
1825
2381
  };
1826
2382
  }
1827
2383
 
2384
+ // src/utilities/session.ts
2385
+ var import_jsonwebtoken2 = __toESM(require("jsonwebtoken"), 1);
2386
+ var getTokenFromRequest = (req) => {
2387
+ const cookies = req.cookies || req.headers?.cookie;
2388
+ if (!cookies) return null;
2389
+ if (typeof cookies === "string") {
2390
+ const parsed = cookies.split(";").reduce((acc, cookie) => {
2391
+ const [key, value] = cookie.trim().split("=");
2392
+ acc[key] = value;
2393
+ return acc;
2394
+ }, {});
2395
+ return parsed["newsletter-auth"] || null;
2396
+ }
2397
+ return cookies["newsletter-auth"] || null;
2398
+ };
2399
+ var verifyToken = (token, secret) => {
2400
+ try {
2401
+ const decoded = import_jsonwebtoken2.default.verify(token, secret);
2402
+ return decoded;
2403
+ } catch {
2404
+ return null;
2405
+ }
2406
+ };
2407
+ var getServerSideAuth = async (context, secret) => {
2408
+ const token = getTokenFromRequest(context.req);
2409
+ if (!token) {
2410
+ return { subscriber: null, isAuthenticated: false };
2411
+ }
2412
+ const payloadSecret = secret || process.env.PAYLOAD_SECRET;
2413
+ if (!payloadSecret) {
2414
+ console.error("No secret provided for token verification");
2415
+ return { subscriber: null, isAuthenticated: false };
2416
+ }
2417
+ const decoded = verifyToken(token, payloadSecret);
2418
+ if (!decoded) {
2419
+ return { subscriber: null, isAuthenticated: false };
2420
+ }
2421
+ return {
2422
+ subscriber: decoded,
2423
+ isAuthenticated: true
2424
+ };
2425
+ };
2426
+ var requireAuth = (gssp) => {
2427
+ return async (context) => {
2428
+ const { isAuthenticated: isAuthenticated2, subscriber } = await getServerSideAuth(context);
2429
+ if (!isAuthenticated2) {
2430
+ return {
2431
+ redirect: {
2432
+ destination: "/auth/signin",
2433
+ permanent: false
2434
+ }
2435
+ };
2436
+ }
2437
+ if (gssp) {
2438
+ const result = await gssp(context);
2439
+ return {
2440
+ ...result,
2441
+ props: {
2442
+ ...result.props,
2443
+ subscriber
2444
+ }
2445
+ };
2446
+ }
2447
+ return {
2448
+ props: {
2449
+ subscriber
2450
+ }
2451
+ };
2452
+ };
2453
+ };
2454
+ var isAuthenticated = (req, secret) => {
2455
+ const token = getTokenFromRequest(req);
2456
+ if (!token) return false;
2457
+ const decoded = verifyToken(token, secret);
2458
+ return !!decoded;
2459
+ };
2460
+
1828
2461
  // src/index.ts
1829
2462
  var newsletterPlugin = (pluginConfig) => (incomingConfig) => {
1830
2463
  const config = {
@@ -1927,6 +2560,11 @@ var newsletterPlugin = (pluginConfig) => (incomingConfig) => {
1927
2560
  };
1928
2561
  // Annotate the CommonJS export names for ESM import in node:
1929
2562
  0 && (module.exports = {
1930
- newsletterPlugin
2563
+ getServerSideAuth,
2564
+ getTokenFromRequest,
2565
+ isAuthenticated,
2566
+ newsletterPlugin,
2567
+ requireAuth,
2568
+ verifyToken
1931
2569
  });
1932
2570
  //# sourceMappingURL=index.cjs.map