payload-subscribers-plugin 0.0.8 → 0.0.9

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.
@@ -1,14 +1,14 @@
1
- import crypto from 'crypto';
2
1
  import { defaultCollectionSlug } from '../collections/Subscribers.js';
3
- import { getTokenAndHash } from '../helpers/token.js';
2
+ import { getHmacHash, getTokenAndHash } from '../helpers/token.js';
4
3
  /**
5
4
  * Factory that creates the request-magic-link endpoint config and handler.
6
5
  * Sends a magic-link email to the given address (creates a pending subscriber if needed).
7
6
  *
8
7
  * @param options - Config options for the endpoint
9
8
  * @param options.subscribersCollectionSlug - Collection slug for subscribers (default from Subscribers collection)
9
+ * @param options.unsubscribeUrl - The URL to use for unsubscribe links
10
10
  * @returns Payload Endpoint config for POST /emailToken
11
- */ function createEndpointRequestMagicLink({ subscribersCollectionSlug = defaultCollectionSlug }) {
11
+ */ function createEndpointRequestMagicLink({ subscribersCollectionSlug = defaultCollectionSlug, unsubscribeUrl }) {
12
12
  /**
13
13
  * Handler for POST /emailToken. Accepts email and verifyUrl, creates/updates a pending
14
14
  * subscriber with a verification token, and sends a magic-link email.
@@ -63,15 +63,15 @@ import { getTokenAndHash } from '../helpers/token.js';
63
63
  }
64
64
  }
65
65
  // Update user with verificationToken
66
- const token = crypto.randomBytes(32).toString('hex');
67
- const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
68
- const expiresAt = new Date(Date.now() + 15 * 60 * 1000) // 15 mins
69
- ;
66
+ // const token = crypto.randomBytes(32).toString('hex')
67
+ // const tokenHash = crypto.createHash('sha256').update(token).digest('hex')
68
+ // const expiresAt = new Date(Date.now() + 15 * 60 * 1000) // 15 mins
69
+ const { expiresAt, token, tokenHash } = getTokenAndHash(15 * 60 * 1000);
70
70
  await req.payload.update({
71
71
  collection: subscribersCollectionSlug,
72
72
  data: {
73
73
  verificationToken: tokenHash,
74
- verificationTokenExpires: expiresAt.toISOString()
74
+ verificationTokenExpires: expiresAt?.toISOString()
75
75
  },
76
76
  where: {
77
77
  email: {
@@ -79,12 +79,15 @@ import { getTokenAndHash } from '../helpers/token.js';
79
79
  }
80
80
  }
81
81
  });
82
+ const { hashToken: unsubscribeHash } = getHmacHash(email);
82
83
  // Send email
83
84
  const magicLink = `${verifyUrl}${verifyUrl.search ? '&' : '?'}token=${token}&email=${email}`;
85
+ const unsubscribeLink = !unsubscribeUrl ? undefined : `${unsubscribeUrl?.href}${unsubscribeUrl?.search ? '&' : '?'}email=${email}&hash=${unsubscribeHash}`;
84
86
  const subject = data.subject || 'Your Magic Login Link';
85
87
  const message = `
86
- ${data.message || '<p>Use this link to log in:</p>'}
87
- <p><a href="${magicLink}"><b>Login</b></a></p>
88
+ ${data.message || '<p>You requested a magic link to log in. Click the button below</p>'}
89
+ <p><a href="${magicLink}"><button><b>Login</b></button></a></p>
90
+ ${unsubscribeLink ? `<p>Click here to <a href="${unsubscribeLink}">unsubscribe</a></p>` : ``}
88
91
  `;
89
92
  const emailResult = await req.payload.sendEmail({
90
93
  html: message,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/requestMagicLink.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, PayloadHandler, PayloadRequest, TypedUser } from 'payload'\n\nimport crypto from 'crypto'\n\nimport { defaultCollectionSlug } from '../collections/Subscribers.js'\nimport { getTokenAndHash } from '../helpers/token.js'\n\nexport type RequestMagicLinkResponse =\n | {\n emailResult: any\n now: string\n }\n | {\n error: string\n now: string\n }\n\n/**\n * Factory that creates the request-magic-link endpoint config and handler.\n * Sends a magic-link email to the given address (creates a pending subscriber if needed).\n *\n * @param options - Config options for the endpoint\n * @param options.subscribersCollectionSlug - Collection slug for subscribers (default from Subscribers collection)\n * @returns Payload Endpoint config for POST /emailToken\n */\nfunction createEndpointRequestMagicLink({\n subscribersCollectionSlug = defaultCollectionSlug,\n}: {\n subscribersCollectionSlug: CollectionSlug\n}): Endpoint {\n /**\n * Handler for POST /emailToken. Accepts email and verifyUrl, creates/updates a pending\n * subscriber with a verification token, and sends a magic-link email.\n *\n * @param req - Payload request; body must include `email` and `verifyUrl`\n * @returns 200 with `emailResult` and `now` on success; 400 with `error` and `now` on bad data or email failure\n */\n const requestMagicLinkHandler: PayloadHandler = async (req: PayloadRequest) => {\n const data = req?.json ? await req.json() : {}\n const { email, verifyUrl } = data // if by POST data\n // const { email } = req.routeParams // if by path\n\n if (!email || !verifyUrl) {\n return Response.json(\n { error: 'Bad data', now: new Date().toISOString() } as RequestMagicLinkResponse,\n { status: 400 },\n )\n }\n\n const userResults = await req.payload.find({\n collection: subscribersCollectionSlug,\n where: {\n email: { equals: email },\n },\n })\n const user = userResults.docs[0] as TypedUser\n\n if (!user) {\n //\n // Create subscriber with status 'pending',\n // and an invisible unknowable password,\n //\n const { tokenHash: tokenHash2 } = getTokenAndHash() // Unknowable\n const createResult = await req.payload.create({\n collection: subscribersCollectionSlug,\n data: {\n email,\n password: tokenHash2,\n status: 'pending',\n },\n draft: false,\n })\n if (!createResult) {\n return Response.json(\n { error: 'Bad data', now: new Date().toISOString() } as RequestMagicLinkResponse,\n { status: 400 },\n )\n }\n }\n\n // Update user with verificationToken\n const token = crypto.randomBytes(32).toString('hex')\n const tokenHash = crypto.createHash('sha256').update(token).digest('hex')\n const expiresAt = new Date(Date.now() + 15 * 60 * 1000) // 15 mins\n await req.payload.update({\n collection: subscribersCollectionSlug,\n data: {\n verificationToken: tokenHash,\n verificationTokenExpires: expiresAt.toISOString(),\n },\n where: {\n email: { equals: user.email },\n },\n })\n\n // Send email\n const magicLink = `${verifyUrl}${verifyUrl.search ? '&' : '?'}token=${token}&email=${email}`\n const subject = data.subject || 'Your Magic Login Link'\n const message = `\n ${data.message || '<p>Use this link to log in:</p>'}\n <p><a href=\"${magicLink}\"><b>Login</b></a></p>\n `\n const emailResult = await req.payload.sendEmail({\n html: message,\n subject,\n to: user.email,\n })\n // req.payload.logger.info(`email result: ${JSON.stringify(emailResult)}`)\n // return data; // Return data to allow normal submission if needed\n if (!emailResult) {\n return Response.json(\n {\n error: 'Unknown email result',\n now: new Date().toISOString(),\n } as RequestMagicLinkResponse,\n { status: 400 },\n )\n }\n req.payload.logger.info(`requestMagicLinkHandler email sent \\n ${magicLink}`)\n return Response.json({\n emailResult,\n now: new Date().toISOString(),\n } as RequestMagicLinkResponse)\n }\n\n /** Endpoint config for requesting a magic link. Mount as POST /emailToken. */\n const requestMagicLinkEndpoint: Endpoint = {\n handler: requestMagicLinkHandler,\n method: 'post',\n path: '/emailToken',\n }\n\n return requestMagicLinkEndpoint\n}\n\nexport default createEndpointRequestMagicLink\n"],"names":["crypto","defaultCollectionSlug","getTokenAndHash","createEndpointRequestMagicLink","subscribersCollectionSlug","requestMagicLinkHandler","req","data","json","email","verifyUrl","Response","error","now","Date","toISOString","status","userResults","payload","find","collection","where","equals","user","docs","tokenHash","tokenHash2","createResult","create","password","draft","token","randomBytes","toString","createHash","update","digest","expiresAt","verificationToken","verificationTokenExpires","magicLink","search","subject","message","emailResult","sendEmail","html","to","logger","info","requestMagicLinkEndpoint","handler","method","path"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/endpoints/requestMagicLink.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, PayloadHandler, PayloadRequest, TypedUser } from 'payload'\n\nimport crypto from 'crypto'\n\nimport { defaultCollectionSlug } from '../collections/Subscribers.js'\nimport { getHmacHash, getTokenAndHash } from '../helpers/token.js'\n\nexport type RequestMagicLinkResponse =\n | {\n emailResult: any\n now: string\n }\n | {\n error: string\n now: string\n }\n\n/**\n * Factory that creates the request-magic-link endpoint config and handler.\n * Sends a magic-link email to the given address (creates a pending subscriber if needed).\n *\n * @param options - Config options for the endpoint\n * @param options.subscribersCollectionSlug - Collection slug for subscribers (default from Subscribers collection)\n * @param options.unsubscribeUrl - The URL to use for unsubscribe links\n * @returns Payload Endpoint config for POST /emailToken\n */\nfunction createEndpointRequestMagicLink({\n subscribersCollectionSlug = defaultCollectionSlug,\n unsubscribeUrl,\n}: {\n subscribersCollectionSlug: CollectionSlug\n unsubscribeUrl?: URL\n}): Endpoint {\n /**\n * Handler for POST /emailToken. Accepts email and verifyUrl, creates/updates a pending\n * subscriber with a verification token, and sends a magic-link email.\n *\n * @param req - Payload request; body must include `email` and `verifyUrl`\n * @returns 200 with `emailResult` and `now` on success; 400 with `error` and `now` on bad data or email failure\n */\n const requestMagicLinkHandler: PayloadHandler = async (req: PayloadRequest) => {\n const data = req?.json ? await req.json() : {}\n const { email, verifyUrl } = data // if by POST data\n // const { email } = req.routeParams // if by path\n\n if (!email || !verifyUrl) {\n return Response.json(\n { error: 'Bad data', now: new Date().toISOString() } as RequestMagicLinkResponse,\n { status: 400 },\n )\n }\n\n const userResults = await req.payload.find({\n collection: subscribersCollectionSlug,\n where: {\n email: { equals: email },\n },\n })\n const user = userResults.docs[0] as TypedUser\n\n if (!user) {\n //\n // Create subscriber with status 'pending',\n // and an invisible unknowable password,\n //\n const { tokenHash: tokenHash2 } = getTokenAndHash() // Unknowable\n const createResult = await req.payload.create({\n collection: subscribersCollectionSlug,\n data: {\n email,\n password: tokenHash2,\n status: 'pending',\n },\n draft: false,\n })\n if (!createResult) {\n return Response.json(\n { error: 'Bad data', now: new Date().toISOString() } as RequestMagicLinkResponse,\n { status: 400 },\n )\n }\n }\n\n // Update user with verificationToken\n // const token = crypto.randomBytes(32).toString('hex')\n // const tokenHash = crypto.createHash('sha256').update(token).digest('hex')\n // const expiresAt = new Date(Date.now() + 15 * 60 * 1000) // 15 mins\n const { expiresAt, token, tokenHash } = getTokenAndHash(15 * 60 * 1000)\n await req.payload.update({\n collection: subscribersCollectionSlug,\n data: {\n verificationToken: tokenHash,\n verificationTokenExpires: expiresAt?.toISOString(),\n },\n where: {\n email: { equals: user.email },\n },\n })\n const { hashToken: unsubscribeHash } = getHmacHash(email)\n\n // Send email\n const magicLink = `${verifyUrl}${verifyUrl.search ? '&' : '?'}token=${token}&email=${email}`\n const unsubscribeLink = !unsubscribeUrl\n ? undefined\n : `${unsubscribeUrl?.href}${unsubscribeUrl?.search ? '&' : '?'}email=${email}&hash=${unsubscribeHash}`\n const subject = data.subject || 'Your Magic Login Link'\n const message = `\n ${data.message || '<p>You requested a magic link to log in. Click the button below</p>'}\n <p><a href=\"${magicLink}\"><button><b>Login</b></button></a></p>\n ${unsubscribeLink ? `<p>Click here to <a href=\"${unsubscribeLink}\">unsubscribe</a></p>` : ``}\n `\n\n const emailResult = await req.payload.sendEmail({\n html: message,\n subject,\n to: user.email,\n })\n // req.payload.logger.info(`email result: ${JSON.stringify(emailResult)}`)\n // return data; // Return data to allow normal submission if needed\n if (!emailResult) {\n return Response.json(\n {\n error: 'Unknown email result',\n now: new Date().toISOString(),\n } as RequestMagicLinkResponse,\n { status: 400 },\n )\n }\n req.payload.logger.info(`requestMagicLinkHandler email sent \\n ${magicLink}`)\n return Response.json({\n emailResult,\n now: new Date().toISOString(),\n } as RequestMagicLinkResponse)\n }\n\n /** Endpoint config for requesting a magic link. Mount as POST /emailToken. */\n const requestMagicLinkEndpoint: Endpoint = {\n handler: requestMagicLinkHandler,\n method: 'post',\n path: '/emailToken',\n }\n\n return requestMagicLinkEndpoint\n}\n\nexport default createEndpointRequestMagicLink\n"],"names":["defaultCollectionSlug","getHmacHash","getTokenAndHash","createEndpointRequestMagicLink","subscribersCollectionSlug","unsubscribeUrl","requestMagicLinkHandler","req","data","json","email","verifyUrl","Response","error","now","Date","toISOString","status","userResults","payload","find","collection","where","equals","user","docs","tokenHash","tokenHash2","createResult","create","password","draft","expiresAt","token","update","verificationToken","verificationTokenExpires","hashToken","unsubscribeHash","magicLink","search","unsubscribeLink","undefined","href","subject","message","emailResult","sendEmail","html","to","logger","info","requestMagicLinkEndpoint","handler","method","path"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { defaultCollectionSlug } from '../collections/Subscribers.js';
2
- import { getTokenAndHash } from '../helpers/token.js';
2
+ import { getHmacHash, getTokenAndHash } from '../helpers/token.js';
3
3
  import { verifyOptIns } from '../helpers/verifyOptIns.js';
4
4
  /**
5
5
  * Factory that creates the subscribe endpoint config and handler.
@@ -18,9 +18,11 @@ import { verifyOptIns } from '../helpers/verifyOptIns.js';
18
18
  * @returns 200 with `emailResult`/`now`, or `email`/`optIns`/`now` when opt-ins updated; 400 with `error`/`now` on failure
19
19
  */ const subscribeHandler = async (req)=>{
20
20
  const data = req?.json ? await req.json() : {};
21
- const { email, optIns, verifyUrl } = data // if by POST data
21
+ const { email, optIns, unsubscribeUrl, verifyUrl } = data // if by POST data
22
22
  ;
23
23
  // const { email } = req.routeParams // if by path
24
+ const verifyUrlObj = new URL(verifyUrl);
25
+ const unsubscribeUrlObj = unsubscribeUrl ? new URL(unsubscribeUrl) : undefined;
24
26
  //
25
27
  // HELPERS
26
28
  // Some of these functions make use of the scope within handler,
@@ -55,9 +57,12 @@ import { verifyOptIns } from '../helpers/verifyOptIns.js';
55
57
  });
56
58
  return updateResults;
57
59
  };
58
- const sendVerifyEmail = async ({ email, linkText, message, subject, token, verifyUrl })=>{
59
- const magicLink = `${verifyUrl}${verifyUrl?.search ? '&' : '?'}token=${token}&email=${email}`;
60
- const html = message + `<p><a href="${magicLink}">${linkText}</a></p>`;
60
+ const sendVerifyEmail = async ({ email, linkText, message, subject, token, unsubscribeHash, unsubscribeUrl, verifyUrl })=>{
61
+ const magicLink = `${verifyUrl.href}${verifyUrl?.search ? '&' : '?'}token=${token}&email=${email}`;
62
+ const unsubscribeLink = unsubscribeUrl ? `${unsubscribeUrl.href}${unsubscribeUrl.search ? '&' : '?'}email=${email}&hash=${unsubscribeHash}` : undefined;
63
+ const html = `
64
+ ${message}<p><a href="${magicLink}">${linkText}</a></p>
65
+ ${unsubscribeLink ? `<p>Click here to <a href="${unsubscribeLink}"><b>unsubscribe</b></a></p>` : ``}`;
61
66
  const emailResult = await req.payload.sendEmail({
62
67
  html,
63
68
  subject,
@@ -112,6 +117,9 @@ import { verifyOptIns } from '../helpers/verifyOptIns.js';
112
117
  // Now we have a subscriber and validatedOptIns
113
118
  // Handle scenarios
114
119
  //
120
+ // Create the hash for an unsubscribe link
121
+ const { hashToken: unsubscribeHash } = getHmacHash(email);
122
+ //
115
123
  // ********************************************************
116
124
  //
117
125
  if (req.user && req.user.email != email) {
@@ -159,7 +167,9 @@ import { verifyOptIns } from '../helpers/verifyOptIns.js';
159
167
  message: data.message || `<p>Click here to verify your subscription:</p>`,
160
168
  subject: data.subject || 'Please verify your subscription',
161
169
  token,
162
- verifyUrl
170
+ unsubscribeHash,
171
+ unsubscribeUrl: unsubscribeUrlObj,
172
+ verifyUrl: verifyUrlObj
163
173
  });
164
174
  if (!emailResult) {
165
175
  req.payload.logger.error(JSON.stringify({
@@ -215,7 +225,9 @@ import { verifyOptIns } from '../helpers/verifyOptIns.js';
215
225
  message: data.message || `<h1>Click here to verify your subscription:</h1>`,
216
226
  subject: data.subject || 'Please verify your subscription',
217
227
  token,
218
- verifyUrl
228
+ unsubscribeHash,
229
+ unsubscribeUrl: unsubscribeUrlObj,
230
+ verifyUrl: verifyUrlObj
219
231
  });
220
232
  if (!emailResult) {
221
233
  req.payload.logger.error(JSON.stringify({
@@ -268,7 +280,9 @@ import { verifyOptIns } from '../helpers/verifyOptIns.js';
268
280
  message: data.message || `<h1>Click here to verify your email:</h1>`,
269
281
  subject: data.subject || 'Please verify your subscription',
270
282
  token,
271
- verifyUrl
283
+ unsubscribeHash,
284
+ unsubscribeUrl: unsubscribeUrlObj,
285
+ verifyUrl: verifyUrlObj
272
286
  });
273
287
  if (!emailResult) {
274
288
  req.payload.logger.error(JSON.stringify({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/subscribe.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, PayloadHandler } from 'payload'\nimport type { Subscriber } from 'src/copied/payload-types.js'\n\nimport { defaultCollectionSlug } from '../collections/Subscribers.js'\nimport { getTokenAndHash } from '../helpers/token.js'\nimport { verifyOptIns } from '../helpers/verifyOptIns.js'\n\nexport type SubscribeResponse =\n // When subscriber optIns are updated...\n | {\n email: string\n now: string\n optIns: string[]\n }\n // When a verify link is emailed...\n | {\n emailResult: any\n now: string\n }\n // When any error occurs...\n | {\n error: string\n now: string\n }\n\n/**\n * Factory that creates the subscribe endpoint config and handler.\n * Handles new subscriptions (pending + verify email), magic-link resends, and updating\n * opt-ins for already-verified subscribers.\n *\n * @param options - Config options for the endpoint\n * @param options.subscribersCollectionSlug - Collection slug for subscribers (default from Subscribers collection)\n * @returns Payload Endpoint config for POST /subscribe\n */\nfunction createEndpointSubscribe({\n subscribersCollectionSlug = defaultCollectionSlug,\n}: {\n subscribersCollectionSlug: CollectionSlug\n}): Endpoint {\n /**\n * Handler for POST /subscribe. Accepts email, optIns, and verifyUrl. Creates pending\n * subscribers and sends verify emails, or updates opt-ins for authenticated subscribers.\n *\n * @param req - Payload request; body: `email`, `optIns` (channel IDs), `verifyUrl`\n * @returns 200 with `emailResult`/`now`, or `email`/`optIns`/`now` when opt-ins updated; 400 with `error`/`now` on failure\n */\n const subscribeHandler: PayloadHandler = async (req) => {\n const data = req?.json ? await req.json() : {}\n const { email, optIns, verifyUrl }: { email: string; optIns: string[]; verifyUrl: string } =\n data // if by POST data\n // const { email } = req.routeParams // if by path\n\n //\n // HELPERS\n // Some of these functions make use of the scope within handler,\n // and would have to be refactored if moved out.\n //\n const createSubscriber = async ({\n optIns,\n password,\n status,\n verificationToken,\n verificationTokenExpires,\n }: {\n email: string\n optIns?: string[]\n password?: string\n status?: 'pending' | 'subscribed' | 'unsubscribed'\n verificationToken?: string\n verificationTokenExpires?: Date\n }) => {\n await req.payload.create({\n collection: subscribersCollectionSlug,\n data: {\n email,\n optIns,\n password,\n status: status || 'pending',\n verificationToken,\n verificationTokenExpires: verificationTokenExpires?.toISOString(),\n },\n draft: false,\n })\n }\n const updateSubscriber = async ({\n id,\n optIns,\n password,\n status,\n verificationToken,\n verificationTokenExpires,\n }: {\n id: string\n optIns?: string[]\n password?: string\n status?: 'pending' | 'subscribed' | 'unsubscribed'\n verificationToken?: string\n verificationTokenExpires?: Date | null\n }) => {\n const updateResults = await req.payload.update({\n id,\n collection: subscribersCollectionSlug,\n data: {\n optIns,\n password,\n status,\n verificationToken,\n verificationTokenExpires: verificationTokenExpires?.toISOString() || null,\n },\n depth: 0,\n })\n return updateResults\n }\n const sendVerifyEmail = async ({\n email,\n linkText,\n message,\n subject,\n token,\n verifyUrl,\n }: {\n email: string\n linkText: string\n message: string\n subject: string\n token: string\n verifyUrl?: string\n }) => {\n const magicLink = `${verifyUrl}${verifyUrl?.search ? '&' : '?'}token=${token}&email=${email}`\n const html = message + `<p><a href=\"${magicLink}\">${linkText}</a></p>`\n const emailResult = await req.payload.sendEmail({\n html,\n subject,\n to: email,\n })\n req.payload.logger.info(`subscribe email sent \\n ${magicLink}`)\n return emailResult\n }\n\n //\n // VALIDATE INPUT\n //\n // Require email\n if (!email) {\n req.payload.logger.error(\n JSON.stringify(\n { error: 'Bad data', now: new Date().toISOString() } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n { error: 'Bad data', now: new Date().toISOString() } as SubscribeResponse,\n { status: 400 },\n )\n }\n\n //\n // Validate OptInChannels\n const { invalidOptInsInput, verifiedOptInIDs } = await verifyOptIns(req.payload, optIns)\n\n if (invalidOptInsInput) {\n req.payload.logger.error(\n JSON.stringify(\n {\n error: 'Invalid input: ' + JSON.stringify(optIns),\n now: new Date().toISOString(),\n } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n {\n error: 'Invalid input: ' + JSON.stringify(optIns),\n now: new Date().toISOString(),\n } as SubscribeResponse,\n { status: 400 },\n )\n }\n\n //\n // Verify subscriber exists\n const userResults = await req.payload.find({\n collection: subscribersCollectionSlug,\n where: {\n email: { equals: email },\n },\n })\n const subscriber = userResults.docs[0] as Subscriber\n\n //\n // Now we have a subscriber and validatedOptIns\n // Handle scenarios\n //\n // ********************************************************\n //\n if (req.user && req.user.email != email) {\n //\n // Error: Auth-ed user doesn't match subscriber email\n //\n req.payload.logger.error(\n JSON.stringify(\n {\n error: 'Unauthorized: ' + email,\n now: new Date().toISOString(),\n } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n {\n error: 'Unauthorized: ' + email,\n now: new Date().toISOString(),\n } as SubscribeResponse,\n { status: 400 },\n )\n }\n\n //\n // ********************************************************\n //\n if (!subscriber) {\n //\n // Create subscriber with status 'pending',\n // and an invisible unknowable password,\n // and send a verify email\n // Pass all optIns through verify link\n //\n const { expiresAt, token, tokenHash } = getTokenAndHash(15 * 60 * 1000) // Use for magic link\n const { tokenHash: tokenHash2 } = getTokenAndHash() // Unknowable\n await createSubscriber({\n email,\n optIns,\n password: tokenHash2,\n status: 'pending',\n verificationToken: tokenHash,\n verificationTokenExpires: expiresAt,\n })\n\n //\n // Send email\n const emailResult = await sendVerifyEmail({\n email,\n linkText: '<b>Verify</b>',\n message: data.message || `<p>Click here to verify your subscription:</p>`,\n subject: data.subject || 'Please verify your subscription',\n token,\n verifyUrl,\n })\n if (!emailResult) {\n req.payload.logger.error(\n JSON.stringify(\n { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,\n { status: 400 },\n )\n }\n return Response.json({ emailResult, now: new Date().toISOString() } as SubscribeResponse)\n //\n }\n //\n // ********************************************************\n //\n if (!req.user && subscriber) {\n //\n // Send magic link to log the user in\n // Pass all optIns through verify link\n //\n const { expiresAt, token, tokenHash } = getTokenAndHash(15 * 60 * 1000) // Use for magic link\n // Update subscriber with token for pending email\n const updateResults = await updateSubscriber({\n id: subscriber.id,\n verificationToken: tokenHash,\n verificationTokenExpires: expiresAt,\n })\n if (!updateResults) {\n req.payload.logger.error(\n JSON.stringify(\n { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,\n { status: 400 },\n )\n }\n\n //\n // Send email\n const emailResult = await sendVerifyEmail({\n email,\n linkText: 'Verify',\n message: data.message || `<h1>Click here to verify your subscription:</h1>`,\n subject: data.subject || 'Please verify your subscription',\n token,\n verifyUrl,\n })\n if (!emailResult) {\n req.payload.logger.error(\n JSON.stringify(\n { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,\n { status: 400 },\n )\n }\n return Response.json({ emailResult, now: new Date().toISOString() } as SubscribeResponse)\n }\n //\n // ********************************************************\n //\n if (req.user && subscriber && subscriber.status == 'pending') {\n //\n // Send magic link to verify the email and log the user in\n // Pass all optIns through verify link\n //\n const { expiresAt, token, tokenHash } = getTokenAndHash(15 * 60 * 1000) // Use for magic link\n // Create subscriber with token for pending email\n const updateResults = await updateSubscriber({\n id: subscriber.id,\n verificationToken: tokenHash,\n verificationTokenExpires: expiresAt,\n })\n if (!updateResults) {\n req.payload.logger.error(\n JSON.stringify(\n { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,\n { status: 400 },\n )\n }\n\n const emailResult = await sendVerifyEmail({\n email,\n linkText: 'Verify',\n message: data.message || `<h1>Click here to verify your email:</h1>`,\n subject: data.subject || 'Please verify your subscription',\n token,\n verifyUrl,\n })\n if (!emailResult) {\n req.payload.logger.error(\n JSON.stringify(\n { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,\n { status: 400 },\n )\n }\n return Response.json({ emailResult, now: new Date().toISOString() } as SubscribeResponse)\n }\n\n //\n // ********************************************************\n //\n if (req.user && subscriber && subscriber.status != 'pending') {\n //\n // Update subscriber with status 'subscribed',\n // an invisible unknowable password,\n // and if any optIns input exists, set subscriber optIns\n // to EXACTLY verifiedOptInIDs (potentially unsubscribing from any not in verifiedOptInIDs)\n //\n const { tokenHash } = getTokenAndHash() // Use for magic link\n // Update subscriber with optIns\n const updateResults = (await updateSubscriber({\n id: subscriber.id,\n optIns: verifiedOptInIDs,\n password: tokenHash,\n status: 'subscribed',\n verificationToken: '',\n verificationTokenExpires: null,\n })) as Subscriber\n\n // Return results, including the verified optIns\n return Response.json({\n email: updateResults.email,\n now: new Date().toISOString(),\n optIns: updateResults.optIns,\n } as SubscribeResponse)\n }\n //\n // Uncaught case\n //\n req.payload.logger.error(\n JSON.stringify(\n { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,\n { status: 400 },\n )\n }\n\n /** Endpoint config for subscription and opt-in updates. Mount as POST /subscribe. */\n const subscribeEndpoint: Endpoint = {\n handler: subscribeHandler,\n method: 'post',\n path: '/subscribe',\n }\n\n return subscribeEndpoint\n}\n\nexport default createEndpointSubscribe\n"],"names":["defaultCollectionSlug","getTokenAndHash","verifyOptIns","createEndpointSubscribe","subscribersCollectionSlug","subscribeHandler","req","data","json","email","optIns","verifyUrl","createSubscriber","password","status","verificationToken","verificationTokenExpires","payload","create","collection","toISOString","draft","updateSubscriber","id","updateResults","update","depth","sendVerifyEmail","linkText","message","subject","token","magicLink","search","html","emailResult","sendEmail","to","logger","info","error","JSON","stringify","now","Date","undefined","Response","invalidOptInsInput","verifiedOptInIDs","userResults","find","where","equals","subscriber","docs","user","expiresAt","tokenHash","tokenHash2","subscribeEndpoint","handler","method","path"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/endpoints/subscribe.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, PayloadHandler } from 'payload'\nimport type { Subscriber } from 'src/copied/payload-types.js'\n\nimport { defaultCollectionSlug } from '../collections/Subscribers.js'\nimport { getHmacHash, getTokenAndHash } from '../helpers/token.js'\nimport { verifyOptIns } from '../helpers/verifyOptIns.js'\n\nexport type SubscribeResponse =\n // When subscriber optIns are updated...\n | {\n email: string\n now: string\n optIns: string[]\n }\n // When a verify link is emailed...\n | {\n emailResult: any\n now: string\n }\n // When any error occurs...\n | {\n error: string\n now: string\n }\n\n/**\n * Factory that creates the subscribe endpoint config and handler.\n * Handles new subscriptions (pending + verify email), magic-link resends, and updating\n * opt-ins for already-verified subscribers.\n *\n * @param options - Config options for the endpoint\n * @param options.subscribersCollectionSlug - Collection slug for subscribers (default from Subscribers collection)\n * @returns Payload Endpoint config for POST /subscribe\n */\nfunction createEndpointSubscribe({\n subscribersCollectionSlug = defaultCollectionSlug,\n}: {\n subscribersCollectionSlug: CollectionSlug\n}): Endpoint {\n /**\n * Handler for POST /subscribe. Accepts email, optIns, and verifyUrl. Creates pending\n * subscribers and sends verify emails, or updates opt-ins for authenticated subscribers.\n *\n * @param req - Payload request; body: `email`, `optIns` (channel IDs), `verifyUrl`\n * @returns 200 with `emailResult`/`now`, or `email`/`optIns`/`now` when opt-ins updated; 400 with `error`/`now` on failure\n */\n const subscribeHandler: PayloadHandler = async (req) => {\n const data = req?.json ? await req.json() : {}\n const {\n email,\n optIns,\n unsubscribeUrl,\n verifyUrl,\n }: { email: string; optIns: string[]; unsubscribeUrl?: string; verifyUrl: string } = data // if by POST data\n // const { email } = req.routeParams // if by path\n const verifyUrlObj: URL = new URL(verifyUrl)\n const unsubscribeUrlObj: undefined | URL = unsubscribeUrl ? new URL(unsubscribeUrl) : undefined\n\n //\n // HELPERS\n // Some of these functions make use of the scope within handler,\n // and would have to be refactored if moved out.\n //\n const createSubscriber = async ({\n optIns,\n password,\n status,\n verificationToken,\n verificationTokenExpires,\n }: {\n email: string\n optIns?: string[]\n password?: string\n status?: 'pending' | 'subscribed' | 'unsubscribed'\n verificationToken?: string\n verificationTokenExpires?: Date\n }) => {\n await req.payload.create({\n collection: subscribersCollectionSlug,\n data: {\n email,\n optIns,\n password,\n status: status || 'pending',\n verificationToken,\n verificationTokenExpires: verificationTokenExpires?.toISOString(),\n },\n draft: false,\n })\n }\n const updateSubscriber = async ({\n id,\n optIns,\n password,\n status,\n verificationToken,\n verificationTokenExpires,\n }: {\n id: string\n optIns?: string[]\n password?: string\n status?: 'pending' | 'subscribed' | 'unsubscribed'\n verificationToken?: string\n verificationTokenExpires?: Date | null\n }) => {\n const updateResults = await req.payload.update({\n id,\n collection: subscribersCollectionSlug,\n data: {\n optIns,\n password,\n status,\n verificationToken,\n verificationTokenExpires: verificationTokenExpires?.toISOString() || null,\n },\n depth: 0,\n })\n return updateResults\n }\n const sendVerifyEmail = async ({\n email,\n linkText,\n message,\n subject,\n token,\n unsubscribeHash,\n unsubscribeUrl,\n verifyUrl,\n }: {\n email: string\n linkText: string\n message: string\n subject: string\n token: string\n unsubscribeHash?: string\n unsubscribeUrl?: URL\n verifyUrl: URL\n }) => {\n const magicLink = `${verifyUrl.href}${verifyUrl?.search ? '&' : '?'}token=${token}&email=${email}`\n const unsubscribeLink = unsubscribeUrl\n ? `${unsubscribeUrl.href}${unsubscribeUrl.search ? '&' : '?'}email=${email}&hash=${unsubscribeHash}`\n : undefined\n const html = `\n${message}<p><a href=\"${magicLink}\">${linkText}</a></p>\n${\n unsubscribeLink ? `<p>Click here to <a href=\"${unsubscribeLink}\"><b>unsubscribe</b></a></p>` : ``\n}`\n\n const emailResult = await req.payload.sendEmail({\n html,\n subject,\n to: email,\n })\n req.payload.logger.info(`subscribe email sent \\n ${magicLink}`)\n return emailResult\n }\n\n //\n // VALIDATE INPUT\n //\n // Require email\n if (!email) {\n req.payload.logger.error(\n JSON.stringify(\n { error: 'Bad data', now: new Date().toISOString() } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n { error: 'Bad data', now: new Date().toISOString() } as SubscribeResponse,\n { status: 400 },\n )\n }\n\n //\n // Validate OptInChannels\n const { invalidOptInsInput, verifiedOptInIDs } = await verifyOptIns(req.payload, optIns)\n\n if (invalidOptInsInput) {\n req.payload.logger.error(\n JSON.stringify(\n {\n error: 'Invalid input: ' + JSON.stringify(optIns),\n now: new Date().toISOString(),\n } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n {\n error: 'Invalid input: ' + JSON.stringify(optIns),\n now: new Date().toISOString(),\n } as SubscribeResponse,\n { status: 400 },\n )\n }\n\n //\n // Verify subscriber exists\n const userResults = await req.payload.find({\n collection: subscribersCollectionSlug,\n where: {\n email: { equals: email },\n },\n })\n const subscriber = userResults.docs[0] as Subscriber\n\n //\n // Now we have a subscriber and validatedOptIns\n // Handle scenarios\n\n //\n // Create the hash for an unsubscribe link\n const { hashToken: unsubscribeHash } = getHmacHash(email)\n\n //\n // ********************************************************\n //\n if (req.user && req.user.email != email) {\n //\n // Error: Auth-ed user doesn't match subscriber email\n //\n req.payload.logger.error(\n JSON.stringify(\n {\n error: 'Unauthorized: ' + email,\n now: new Date().toISOString(),\n } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n {\n error: 'Unauthorized: ' + email,\n now: new Date().toISOString(),\n } as SubscribeResponse,\n { status: 400 },\n )\n }\n\n //\n // ********************************************************\n //\n if (!subscriber) {\n //\n // Create subscriber with status 'pending',\n // and an invisible unknowable password,\n // and send a verify email\n // Pass all optIns through verify link\n //\n const { expiresAt, token, tokenHash } = getTokenAndHash(15 * 60 * 1000) // Use for magic link\n const { tokenHash: tokenHash2 } = getTokenAndHash() // Unknowable\n await createSubscriber({\n email,\n optIns,\n password: tokenHash2,\n status: 'pending',\n verificationToken: tokenHash,\n verificationTokenExpires: expiresAt,\n })\n\n //\n // Send email\n const emailResult = await sendVerifyEmail({\n email,\n linkText: '<b>Verify</b>',\n message: data.message || `<p>Click here to verify your subscription:</p>`,\n subject: data.subject || 'Please verify your subscription',\n token,\n unsubscribeHash,\n unsubscribeUrl: unsubscribeUrlObj,\n verifyUrl: verifyUrlObj,\n })\n if (!emailResult) {\n req.payload.logger.error(\n JSON.stringify(\n { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,\n { status: 400 },\n )\n }\n return Response.json({ emailResult, now: new Date().toISOString() } as SubscribeResponse)\n //\n }\n //\n // ********************************************************\n //\n if (!req.user && subscriber) {\n //\n // Send magic link to log the user in\n // Pass all optIns through verify link\n //\n const { expiresAt, token, tokenHash } = getTokenAndHash(15 * 60 * 1000) // Use for magic link\n // Update subscriber with token for pending email\n const updateResults = await updateSubscriber({\n id: subscriber.id,\n verificationToken: tokenHash,\n verificationTokenExpires: expiresAt,\n })\n if (!updateResults) {\n req.payload.logger.error(\n JSON.stringify(\n { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,\n { status: 400 },\n )\n }\n\n //\n // Send email\n const emailResult = await sendVerifyEmail({\n email,\n linkText: 'Verify',\n message: data.message || `<h1>Click here to verify your subscription:</h1>`,\n subject: data.subject || 'Please verify your subscription',\n token,\n unsubscribeHash,\n unsubscribeUrl: unsubscribeUrlObj,\n verifyUrl: verifyUrlObj,\n })\n if (!emailResult) {\n req.payload.logger.error(\n JSON.stringify(\n { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,\n { status: 400 },\n )\n }\n return Response.json({ emailResult, now: new Date().toISOString() } as SubscribeResponse)\n }\n //\n // ********************************************************\n //\n if (req.user && subscriber && subscriber.status == 'pending') {\n //\n // Send magic link to verify the email and log the user in\n // Pass all optIns through verify link\n //\n const { expiresAt, token, tokenHash } = getTokenAndHash(15 * 60 * 1000) // Use for magic link\n // Create subscriber with token for pending email\n const updateResults = await updateSubscriber({\n id: subscriber.id,\n verificationToken: tokenHash,\n verificationTokenExpires: expiresAt,\n })\n if (!updateResults) {\n req.payload.logger.error(\n JSON.stringify(\n { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,\n { status: 400 },\n )\n }\n\n const emailResult = await sendVerifyEmail({\n email,\n linkText: 'Verify',\n message: data.message || `<h1>Click here to verify your email:</h1>`,\n subject: data.subject || 'Please verify your subscription',\n token,\n unsubscribeHash,\n unsubscribeUrl: unsubscribeUrlObj,\n verifyUrl: verifyUrlObj,\n })\n if (!emailResult) {\n req.payload.logger.error(\n JSON.stringify(\n { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,\n { status: 400 },\n )\n }\n return Response.json({ emailResult, now: new Date().toISOString() } as SubscribeResponse)\n }\n\n //\n // ********************************************************\n //\n if (req.user && subscriber && subscriber.status != 'pending') {\n //\n // Update subscriber with status 'subscribed',\n // an invisible unknowable password,\n // and if any optIns input exists, set subscriber optIns\n // to EXACTLY verifiedOptInIDs (potentially unsubscribing from any not in verifiedOptInIDs)\n //\n const { tokenHash } = getTokenAndHash() // Use for magic link\n // Update subscriber with optIns\n const updateResults = (await updateSubscriber({\n id: subscriber.id,\n optIns: verifiedOptInIDs,\n password: tokenHash,\n status: 'subscribed',\n verificationToken: '',\n verificationTokenExpires: null,\n })) as Subscriber\n\n // Return results, including the verified optIns\n return Response.json({\n email: updateResults.email,\n now: new Date().toISOString(),\n optIns: updateResults.optIns,\n } as SubscribeResponse)\n }\n //\n // Uncaught case\n //\n req.payload.logger.error(\n JSON.stringify(\n { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,\n undefined,\n 2,\n ),\n )\n return Response.json(\n { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,\n { status: 400 },\n )\n }\n\n /** Endpoint config for subscription and opt-in updates. Mount as POST /subscribe. */\n const subscribeEndpoint: Endpoint = {\n handler: subscribeHandler,\n method: 'post',\n path: '/subscribe',\n }\n\n return subscribeEndpoint\n}\n\nexport default createEndpointSubscribe\n"],"names":["defaultCollectionSlug","getHmacHash","getTokenAndHash","verifyOptIns","createEndpointSubscribe","subscribersCollectionSlug","subscribeHandler","req","data","json","email","optIns","unsubscribeUrl","verifyUrl","verifyUrlObj","URL","unsubscribeUrlObj","undefined","createSubscriber","password","status","verificationToken","verificationTokenExpires","payload","create","collection","toISOString","draft","updateSubscriber","id","updateResults","update","depth","sendVerifyEmail","linkText","message","subject","token","unsubscribeHash","magicLink","href","search","unsubscribeLink","html","emailResult","sendEmail","to","logger","info","error","JSON","stringify","now","Date","Response","invalidOptInsInput","verifiedOptInIDs","userResults","find","where","equals","subscriber","docs","hashToken","user","expiresAt","tokenHash","tokenHash2","subscribeEndpoint","handler","method","path"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { CollectionSlug, Endpoint } from 'payload';
2
+ export type UnsubscribeResponse = {
3
+ error: string;
4
+ now: string;
5
+ } | {
6
+ message: string;
7
+ now: string;
8
+ };
9
+ /**
10
+ * Factory that creates the unsubscribe endpoint config and handler.
11
+ * Handles completely unsubscribing a subscriber by marking their
12
+ * status as 'unsubscribed'.
13
+ *
14
+ * @param options - Config options for the endpoint
15
+ * @param options.subscribersCollectionSlug - Collection slug for subscribers (default from Subscribers collection)
16
+ * @returns Payload Endpoint config for POST /unsubscribe
17
+ */
18
+ declare function createEndpointUnsubscribe({ subscribersCollectionSlug, }: {
19
+ subscribersCollectionSlug: CollectionSlug;
20
+ }): Endpoint;
21
+ export default createEndpointUnsubscribe;
@@ -0,0 +1,118 @@
1
+ import { defaultCollectionSlug } from '../collections/Subscribers.js';
2
+ import { getHmacHash } from '../helpers/token.js';
3
+ /**
4
+ * Factory that creates the unsubscribe endpoint config and handler.
5
+ * Handles completely unsubscribing a subscriber by marking their
6
+ * status as 'unsubscribed'.
7
+ *
8
+ * @param options - Config options for the endpoint
9
+ * @param options.subscribersCollectionSlug - Collection slug for subscribers (default from Subscribers collection)
10
+ * @returns Payload Endpoint config for POST /unsubscribe
11
+ */ function createEndpointUnsubscribe({ subscribersCollectionSlug = defaultCollectionSlug }) {
12
+ /**
13
+ * Handler for POST /unsubscribe. Accepts email, optIns, and verifyUrl. Creates pending
14
+ * subscribers and sends verify emails, or updates opt-ins for authenticated subscribers.
15
+ *
16
+ * @param req - Payload request; body: `email`, `optIns` (channel IDs), `verifyUrl`
17
+ * @returns 200 with `emailResult`/`now`, or `email`/`optIns`/`now` when opt-ins updated; 400 with `error`/`now` on failure
18
+ */ const unsubscribeHandler = async (req)=>{
19
+ const data = req?.json ? await req.json() : {};
20
+ const { email, unsubscribeToken } = await data // if by POST data
21
+ ;
22
+ // const { email } = req.routeParams // if by path
23
+ //
24
+ // VALIDATE INPUT
25
+ //
26
+ // Require unsubscribeToken
27
+ if (!unsubscribeToken) {
28
+ const result = {
29
+ error: 'Bad data',
30
+ now: new Date().toISOString()
31
+ };
32
+ req.payload.logger.error(`unsubscribe: No unsubscribeToken — ${JSON.stringify(result, undefined, 2)}`);
33
+ return Response.json(result);
34
+ }
35
+ //
36
+ // Verify unsubscribeToken
37
+ const { hashToken: verifyUnsubscribeToken } = getHmacHash(email);
38
+ if (unsubscribeToken != verifyUnsubscribeToken) {
39
+ const result = {
40
+ error: 'Bad data',
41
+ now: new Date().toISOString()
42
+ };
43
+ req.payload.logger.error(`unsubscribe: unsubscribeToken not verified — ${JSON.stringify(result, undefined, 2)}`);
44
+ return Response.json(result);
45
+ }
46
+ //
47
+ // Require subscriber exists
48
+ const userResults = await req.payload.find({
49
+ collection: subscribersCollectionSlug,
50
+ where: {
51
+ email: {
52
+ equals: email
53
+ }
54
+ }
55
+ });
56
+ const subscriber = userResults.docs[0];
57
+ if (!subscriber) {
58
+ const result = {
59
+ error: 'Bad data',
60
+ now: new Date().toISOString()
61
+ };
62
+ req.payload.logger.error(`unsubscribe: No subscriber — ${JSON.stringify(result, undefined, 2)}`);
63
+ return Response.json(result);
64
+ }
65
+ //
66
+ // Require authed user to match incoming email
67
+ if (req.user && req.user.email != subscriber.email) {
68
+ //
69
+ // Error: Auth-ed user doesn't match subscriber email
70
+ //
71
+ const result = {
72
+ error: 'Unauthorized: ' + subscriber.email,
73
+ now: new Date().toISOString()
74
+ };
75
+ req.payload.logger.error(`unsubscribe: Unauthorized — ${JSON.stringify(result, undefined, 2)}`);
76
+ return Response.json(result);
77
+ }
78
+ //
79
+ // Now we have a validated subscriber and unsubscribeToken
80
+ // Mark as unsubscribed
81
+ const updateResults = await req.payload.update({
82
+ id: subscriber.id,
83
+ collection: subscribersCollectionSlug,
84
+ data: {
85
+ status: 'unsubscribed'
86
+ },
87
+ depth: 0
88
+ });
89
+ if (!updateResults) {
90
+ const result = {
91
+ error: 'Unable to unsubscribe. Please try again.',
92
+ now: new Date().toISOString()
93
+ };
94
+ req.payload.logger.error(`unsubscribe: Unknown updateResults — ${JSON.stringify(result, undefined, 2)}`);
95
+ return Response.json(result, {
96
+ status: 400
97
+ });
98
+ }
99
+ //
100
+ // Success
101
+ //
102
+ const result = {
103
+ message: 'Unsubscribed',
104
+ now: new Date().toISOString()
105
+ };
106
+ req.payload.logger.error(`unsubscribe: Unhandled scenario — ${JSON.stringify(result, undefined, 2)}`);
107
+ return Response.json(result);
108
+ };
109
+ /** Endpoint config for subscription and opt-in updates. Mount as POST /subscribe. */ const unsubscribeEndpoint = {
110
+ handler: unsubscribeHandler,
111
+ method: 'post',
112
+ path: '/unsubscribe'
113
+ };
114
+ return unsubscribeEndpoint;
115
+ }
116
+ export default createEndpointUnsubscribe;
117
+
118
+ //# sourceMappingURL=unsubscribe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/unsubscribe.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, PayloadHandler } from 'payload'\nimport type { Subscriber } from 'src/copied/payload-types.js'\n\nimport { defaultCollectionSlug } from '../collections/Subscribers.js'\nimport { getHmacHash } from '../helpers/token.js'\n\nexport type UnsubscribeResponse =\n // When unsubscriber status is set to 'unsubscribed'...\n | {\n error: string\n now: string\n }\n // When any error occurs...\n | {\n message: string\n now: string\n }\n\n/**\n * Factory that creates the unsubscribe endpoint config and handler.\n * Handles completely unsubscribing a subscriber by marking their\n * status as 'unsubscribed'.\n *\n * @param options - Config options for the endpoint\n * @param options.subscribersCollectionSlug - Collection slug for subscribers (default from Subscribers collection)\n * @returns Payload Endpoint config for POST /unsubscribe\n */\nfunction createEndpointUnsubscribe({\n subscribersCollectionSlug = defaultCollectionSlug,\n}: {\n subscribersCollectionSlug: CollectionSlug\n}): Endpoint {\n /**\n * Handler for POST /unsubscribe. Accepts email, optIns, and verifyUrl. Creates pending\n * subscribers and sends verify emails, or updates opt-ins for authenticated subscribers.\n *\n * @param req - Payload request; body: `email`, `optIns` (channel IDs), `verifyUrl`\n * @returns 200 with `emailResult`/`now`, or `email`/`optIns`/`now` when opt-ins updated; 400 with `error`/`now` on failure\n */\n const unsubscribeHandler: PayloadHandler = async (req) => {\n const data = req?.json ? await req.json() : {}\n const { email, unsubscribeToken }: { email: string; unsubscribeToken: string } = await data // if by POST data\n // const { email } = req.routeParams // if by path\n\n //\n // VALIDATE INPUT\n //\n // Require unsubscribeToken\n if (!unsubscribeToken) {\n const result = { error: 'Bad data', now: new Date().toISOString() } as UnsubscribeResponse\n req.payload.logger.error(\n `unsubscribe: No unsubscribeToken — ${JSON.stringify(result, undefined, 2)}`,\n )\n return Response.json(result)\n }\n\n //\n // Verify unsubscribeToken\n const { hashToken: verifyUnsubscribeToken } = getHmacHash(email)\n if (unsubscribeToken != verifyUnsubscribeToken) {\n const result = { error: 'Bad data', now: new Date().toISOString() } as UnsubscribeResponse\n req.payload.logger.error(\n `unsubscribe: unsubscribeToken not verified — ${JSON.stringify(result, undefined, 2)}`,\n )\n return Response.json(result)\n }\n\n //\n // Require subscriber exists\n const userResults = await req.payload.find({\n collection: subscribersCollectionSlug,\n where: {\n email: { equals: email },\n },\n })\n const subscriber = userResults.docs[0] as Subscriber\n\n if (!subscriber) {\n const result = { error: 'Bad data', now: new Date().toISOString() } as UnsubscribeResponse\n req.payload.logger.error(\n `unsubscribe: No subscriber — ${JSON.stringify(result, undefined, 2)}`,\n )\n return Response.json(result)\n }\n\n //\n // Require authed user to match incoming email\n if (req.user && req.user.email != subscriber.email) {\n //\n // Error: Auth-ed user doesn't match subscriber email\n //\n const result = {\n error: 'Unauthorized: ' + subscriber.email,\n now: new Date().toISOString(),\n } as UnsubscribeResponse\n req.payload.logger.error(\n `unsubscribe: Unauthorized — ${JSON.stringify(result, undefined, 2)}`,\n )\n return Response.json(result)\n }\n\n //\n // Now we have a validated subscriber and unsubscribeToken\n // Mark as unsubscribed\n\n const updateResults = await req.payload.update({\n id: subscriber.id,\n collection: subscribersCollectionSlug,\n data: {\n status: 'unsubscribed',\n },\n depth: 0,\n })\n if (!updateResults) {\n const result = {\n error: 'Unable to unsubscribe. Please try again.',\n now: new Date().toISOString(),\n } as UnsubscribeResponse\n req.payload.logger.error(\n `unsubscribe: Unknown updateResults — ${JSON.stringify(result, undefined, 2)}`,\n )\n return Response.json(result, { status: 400 })\n }\n\n //\n // Success\n //\n const result = { message: 'Unsubscribed', now: new Date().toISOString() } as UnsubscribeResponse\n req.payload.logger.error(\n `unsubscribe: Unhandled scenario — ${JSON.stringify(result, undefined, 2)}`,\n )\n return Response.json(result)\n }\n\n /** Endpoint config for subscription and opt-in updates. Mount as POST /subscribe. */\n const unsubscribeEndpoint: Endpoint = {\n handler: unsubscribeHandler,\n method: 'post',\n path: '/unsubscribe',\n }\n\n return unsubscribeEndpoint\n}\n\nexport default createEndpointUnsubscribe\n"],"names":["defaultCollectionSlug","getHmacHash","createEndpointUnsubscribe","subscribersCollectionSlug","unsubscribeHandler","req","data","json","email","unsubscribeToken","result","error","now","Date","toISOString","payload","logger","JSON","stringify","undefined","Response","hashToken","verifyUnsubscribeToken","userResults","find","collection","where","equals","subscriber","docs","user","updateResults","update","id","status","depth","message","unsubscribeEndpoint","handler","method","path"],"mappings":"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"}
@@ -49,7 +49,7 @@ import { getHash, getTokenAndHash } from '../helpers/token.js';
49
49
  // `verifyMagicLinkHandler ${email} \n ${tokenHash} \n ${user.verificationTokenExpires} \n ${user.verificationToken}`,
50
50
  // )
51
51
  if (!user.verificationTokenExpires || tokenHash != user.verificationToken) {
52
- // req.payload.logger.info(`Token not verified: ${tokenHash} != ${user.verificationToken}`)
52
+ req.payload.logger.info(`Token not verified: ${tokenHash} != ${user.verificationToken}`);
53
53
  return Response.json({
54
54
  error: 'Token not verified',
55
55
  now: new Date().toISOString()
@@ -103,11 +103,12 @@ import { getHash, getTokenAndHash } from '../helpers/token.js';
103
103
  });
104
104
  }
105
105
  // console.log('login', headers)
106
+ const status = user?.status == 'pending' ? 'subscribed' : user?.status;
106
107
  const { tokenHash: tokenHash2 } = getTokenAndHash() // Unknowable
107
108
  ;
108
109
  const data = {
109
110
  password: tokenHash2,
110
- status: 'subscribed',
111
+ status,
111
112
  verificationToken: '',
112
113
  verificationTokenExpires: null
113
114
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/verifyMagicLink.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, PayloadHandler } from 'payload'\nimport type { Subscriber } from 'src/copied/payload-types.js'\n\nimport { defaultCollectionSlug } from '../collections/Subscribers.js'\nimport { getHash, getTokenAndHash } from '../helpers/token.js'\n\nexport type VerifyMagicLinkResponse =\n | {\n error: string\n now: string\n }\n | {\n message: string\n now: string\n }\n\n/**\n * Factory that creates the verify-magic-link endpoint config and handler.\n * Validates token from the magic link, marks the subscriber as verified, and logs them in.\n *\n * @param options - Config options for the endpoint\n * @param options.subscribersCollectionSlug - Collection slug for subscribers (default from Subscribers collection)\n * @returns Payload Endpoint config for POST /verifyToken\n */\nfunction createEndpointVerifyMagicLink({\n subscribersCollectionSlug = defaultCollectionSlug,\n}: {\n subscribersCollectionSlug: CollectionSlug\n}): Endpoint {\n /**\n * Handler for POST /verifyToken. Validates email + token from magic link, updates subscriber\n * password and status, and performs login to set auth cookies.\n *\n * @param req - Payload request; body must include `email` and `token`\n * @returns 200 with `message`, `now` and Set-Cookie on success; 400 with `error` and `now` on bad data, invalid token, or expiry\n */\n const verifyMagicLinkHandler: PayloadHandler = async (req) => {\n const reqData = req?.json ? await req.json() : {}\n const { email, token }: { email: string; token: string } = reqData // if by POST reqData\n // const { email, token } = req.routeParams // if by path\n\n if (!email || !token) {\n return Response.json(\n { error: 'Bad data', now: new Date().toISOString() } as VerifyMagicLinkResponse,\n { status: 400 },\n )\n }\n\n const userResults = await req.payload.find({\n collection: subscribersCollectionSlug,\n where: {\n email: { equals: email },\n },\n })\n\n type SubscriberType = {\n // @ts-expect-error Why is this not correct, isn't it how Payload does it?\n collection: subscribersCollectionSlug\n } & Subscriber\n\n const user = userResults.docs[0] as SubscriberType\n\n if (!user) {\n return Response.json(\n { error: 'Bad data', now: new Date().toISOString() } as VerifyMagicLinkResponse,\n { status: 400 },\n )\n }\n\n const { tokenHash } = getHash(token)\n\n // req.payload.logger.info(\n // `verifyMagicLinkHandler ${email} \\n ${tokenHash} \\n ${user.verificationTokenExpires} \\n ${user.verificationToken}`,\n // )\n if (!user.verificationTokenExpires || tokenHash != user.verificationToken) {\n // req.payload.logger.info(`Token not verified: ${tokenHash} != ${user.verificationToken}`)\n return Response.json(\n { error: 'Token not verified', now: new Date().toISOString() } as VerifyMagicLinkResponse,\n { status: 400 },\n )\n }\n\n if (new Date(Date.now()) > new Date(user.verificationTokenExpires)) {\n return Response.json(\n { error: 'Token expired', now: new Date().toISOString() } as VerifyMagicLinkResponse,\n { status: 400 },\n )\n }\n\n // Update user\n await req.payload.update({\n collection: subscribersCollectionSlug,\n data: {\n password: tokenHash,\n },\n where: {\n email: { equals: user.email },\n },\n })\n\n // Log the user in via Payload headers\n let headers\n try {\n const loginReq = await fetch(\n `${req.payload.config.serverURL}/api/${subscribersCollectionSlug}/login`,\n {\n body: JSON.stringify({\n email,\n password: tokenHash,\n }),\n credentials: 'include',\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n },\n )\n if (loginReq && loginReq.ok) {\n headers = loginReq.headers\n }\n } catch (error) {\n // console.log(error)\n return Response.json({ error } as VerifyMagicLinkResponse, { status: 400 })\n }\n // console.log('login', headers)\n\n const { tokenHash: tokenHash2 } = getTokenAndHash() // Unknowable\n const data = {\n password: tokenHash2,\n status: 'subscribed' as 'pending' | 'subscribed' | 'unsubscribed' | undefined,\n verificationToken: '',\n verificationTokenExpires: null,\n }\n // Update user\n await req.payload.update({\n collection: subscribersCollectionSlug,\n data,\n where: {\n email: { equals: user.email },\n },\n })\n\n return Response.json(\n {\n message: 'Token verified',\n now: new Date().toISOString(),\n } as VerifyMagicLinkResponse,\n { headers },\n )\n }\n\n /** Endpoint config for verifying magic link and logging in. Mount as POST /verifyToken. */\n const verifyMagicLinkEndpoint: Endpoint = {\n handler: verifyMagicLinkHandler,\n method: 'post',\n path: '/verifyToken',\n }\n\n return verifyMagicLinkEndpoint\n}\n\nexport default createEndpointVerifyMagicLink\n"],"names":["defaultCollectionSlug","getHash","getTokenAndHash","createEndpointVerifyMagicLink","subscribersCollectionSlug","verifyMagicLinkHandler","req","reqData","json","email","token","Response","error","now","Date","toISOString","status","userResults","payload","find","collection","where","equals","user","docs","tokenHash","verificationTokenExpires","verificationToken","update","data","password","headers","loginReq","fetch","config","serverURL","body","JSON","stringify","credentials","method","ok","tokenHash2","message","verifyMagicLinkEndpoint","handler","path"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/endpoints/verifyMagicLink.ts"],"sourcesContent":["import type { CollectionSlug, Endpoint, PayloadHandler } from 'payload'\nimport type { Subscriber } from 'src/copied/payload-types.js'\n\nimport { defaultCollectionSlug } from '../collections/Subscribers.js'\nimport { getHash, getTokenAndHash } from '../helpers/token.js'\n\nexport type VerifyMagicLinkResponse =\n | {\n error: string\n now: string\n }\n | {\n message: string\n now: string\n }\n\n/**\n * Factory that creates the verify-magic-link endpoint config and handler.\n * Validates token from the magic link, marks the subscriber as verified, and logs them in.\n *\n * @param options - Config options for the endpoint\n * @param options.subscribersCollectionSlug - Collection slug for subscribers (default from Subscribers collection)\n * @returns Payload Endpoint config for POST /verifyToken\n */\nfunction createEndpointVerifyMagicLink({\n subscribersCollectionSlug = defaultCollectionSlug,\n}: {\n subscribersCollectionSlug: CollectionSlug\n}): Endpoint {\n /**\n * Handler for POST /verifyToken. Validates email + token from magic link, updates subscriber\n * password and status, and performs login to set auth cookies.\n *\n * @param req - Payload request; body must include `email` and `token`\n * @returns 200 with `message`, `now` and Set-Cookie on success; 400 with `error` and `now` on bad data, invalid token, or expiry\n */\n const verifyMagicLinkHandler: PayloadHandler = async (req) => {\n const reqData = req?.json ? await req.json() : {}\n const { email, token }: { email: string; token: string } = reqData // if by POST reqData\n // const { email, token } = req.routeParams // if by path\n\n if (!email || !token) {\n return Response.json(\n { error: 'Bad data', now: new Date().toISOString() } as VerifyMagicLinkResponse,\n { status: 400 },\n )\n }\n\n const userResults = await req.payload.find({\n collection: subscribersCollectionSlug,\n where: {\n email: { equals: email },\n },\n })\n\n type SubscriberType = {\n // @ts-expect-error Why is this not correct, isn't it how Payload does it?\n collection: subscribersCollectionSlug\n } & Subscriber\n\n const user = userResults.docs[0] as SubscriberType\n\n if (!user) {\n return Response.json(\n { error: 'Bad data', now: new Date().toISOString() } as VerifyMagicLinkResponse,\n { status: 400 },\n )\n }\n\n const { tokenHash } = getHash(token)\n\n // req.payload.logger.info(\n // `verifyMagicLinkHandler ${email} \\n ${tokenHash} \\n ${user.verificationTokenExpires} \\n ${user.verificationToken}`,\n // )\n if (!user.verificationTokenExpires || tokenHash != user.verificationToken) {\n req.payload.logger.info(`Token not verified: ${tokenHash} != ${user.verificationToken}`)\n return Response.json(\n { error: 'Token not verified', now: new Date().toISOString() } as VerifyMagicLinkResponse,\n { status: 400 },\n )\n }\n\n if (new Date(Date.now()) > new Date(user.verificationTokenExpires)) {\n return Response.json(\n { error: 'Token expired', now: new Date().toISOString() } as VerifyMagicLinkResponse,\n { status: 400 },\n )\n }\n\n // Update user\n await req.payload.update({\n collection: subscribersCollectionSlug,\n data: {\n password: tokenHash,\n },\n where: {\n email: { equals: user.email },\n },\n })\n\n // Log the user in via Payload headers\n let headers\n try {\n const loginReq = await fetch(\n `${req.payload.config.serverURL}/api/${subscribersCollectionSlug}/login`,\n {\n body: JSON.stringify({\n email,\n password: tokenHash,\n }),\n credentials: 'include',\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n },\n )\n if (loginReq && loginReq.ok) {\n headers = loginReq.headers\n }\n } catch (error) {\n // console.log(error)\n return Response.json({ error } as VerifyMagicLinkResponse, { status: 400 })\n }\n // console.log('login', headers)\n\n const status: 'pending' | 'subscribed' | 'unsubscribed' | undefined =\n user?.status == 'pending' ? 'subscribed' : user?.status\n\n const { tokenHash: tokenHash2 } = getTokenAndHash() // Unknowable\n const data = {\n password: tokenHash2,\n status,\n verificationToken: '',\n verificationTokenExpires: null,\n }\n // Update user\n await req.payload.update({\n collection: subscribersCollectionSlug,\n data,\n where: {\n email: { equals: user.email },\n },\n })\n\n return Response.json(\n {\n message: 'Token verified',\n now: new Date().toISOString(),\n } as VerifyMagicLinkResponse,\n { headers },\n )\n }\n\n /** Endpoint config for verifying magic link and logging in. Mount as POST /verifyToken. */\n const verifyMagicLinkEndpoint: Endpoint = {\n handler: verifyMagicLinkHandler,\n method: 'post',\n path: '/verifyToken',\n }\n\n return verifyMagicLinkEndpoint\n}\n\nexport default createEndpointVerifyMagicLink\n"],"names":["defaultCollectionSlug","getHash","getTokenAndHash","createEndpointVerifyMagicLink","subscribersCollectionSlug","verifyMagicLinkHandler","req","reqData","json","email","token","Response","error","now","Date","toISOString","status","userResults","payload","find","collection","where","equals","user","docs","tokenHash","verificationTokenExpires","verificationToken","logger","info","update","data","password","headers","loginReq","fetch","config","serverURL","body","JSON","stringify","credentials","method","ok","tokenHash2","message","verifyMagicLinkEndpoint","handler","path"],"mappings":"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"}
@@ -4,6 +4,8 @@ export { RequestOrSubscribe } from '../components/app/RequestOrSubscribe.js';
4
4
  export type { SubscribeResponse } from '../components/app/Subscribe.js';
5
5
  export { Subscribe } from '../components/app/Subscribe.js';
6
6
  export { SubscriberMenu } from '../components/app/SubscriberMenu.js';
7
+ export type { UnsubscribeResponse } from '../components/app/Unsubscribe.js';
8
+ export { Unsubscribe } from '../components/app/Unsubscribe.js';
7
9
  export type { VerifyMagicLinkResponse } from '../components/app/VerifyMagicLink.js';
8
10
  export { VerifyMagicLink } from '../components/app/VerifyMagicLink.js';
9
11
  export type { SubscriberContextType } from '../contexts/SubscriberProvider.js';
@@ -2,6 +2,7 @@ export { RequestMagicLink } from '../components/app/RequestMagicLink.js';
2
2
  export { RequestOrSubscribe } from '../components/app/RequestOrSubscribe.js';
3
3
  export { Subscribe } from '../components/app/Subscribe.js';
4
4
  export { SubscriberMenu } from '../components/app/SubscriberMenu.js';
5
+ export { Unsubscribe } from '../components/app/Unsubscribe.js';
5
6
  export { VerifyMagicLink } from '../components/app/VerifyMagicLink.js';
6
7
  export { SubscriberProvider, useSubscriber } from '../contexts/SubscriberProvider.js';
7
8
  export { getServerUrl } from '../server-functions/serverUrl.js';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/exports/ui.ts"],"sourcesContent":["export type { RequestMagicLinkResponse } from '../components/app/RequestMagicLink.js'\nexport { RequestMagicLink } from '../components/app/RequestMagicLink.js'\n\nexport { RequestOrSubscribe } from '../components/app/RequestOrSubscribe.js'\n\nexport type { SubscribeResponse } from '../components/app/Subscribe.js'\nexport { Subscribe } from '../components/app/Subscribe.js'\n\nexport { SubscriberMenu } from '../components/app/SubscriberMenu.js'\n\nexport type { VerifyMagicLinkResponse } from '../components/app/VerifyMagicLink.js'\nexport { VerifyMagicLink } from '../components/app/VerifyMagicLink.js'\n\nexport type { SubscriberContextType } from '../contexts/SubscriberProvider.js'\nexport { SubscriberProvider, useSubscriber } from '../contexts/SubscriberProvider.js'\n\nexport { getServerUrl } from '../server-functions/serverUrl.js'\n"],"names":["RequestMagicLink","RequestOrSubscribe","Subscribe","SubscriberMenu","VerifyMagicLink","SubscriberProvider","useSubscriber","getServerUrl"],"mappings":"AACA,SAASA,gBAAgB,QAAQ,wCAAuC;AAExE,SAASC,kBAAkB,QAAQ,0CAAyC;AAG5E,SAASC,SAAS,QAAQ,iCAAgC;AAE1D,SAASC,cAAc,QAAQ,sCAAqC;AAGpE,SAASC,eAAe,QAAQ,uCAAsC;AAGtE,SAASC,kBAAkB,EAAEC,aAAa,QAAQ,oCAAmC;AAErF,SAASC,YAAY,QAAQ,mCAAkC"}
1
+ {"version":3,"sources":["../../src/exports/ui.ts"],"sourcesContent":["export type { RequestMagicLinkResponse } from '../components/app/RequestMagicLink.js'\nexport { RequestMagicLink } from '../components/app/RequestMagicLink.js'\n\nexport { RequestOrSubscribe } from '../components/app/RequestOrSubscribe.js'\n\nexport type { SubscribeResponse } from '../components/app/Subscribe.js'\nexport { Subscribe } from '../components/app/Subscribe.js'\n\nexport { SubscriberMenu } from '../components/app/SubscriberMenu.js'\n\nexport type { UnsubscribeResponse } from '../components/app/Unsubscribe.js'\nexport { Unsubscribe } from '../components/app/Unsubscribe.js'\n\nexport type { VerifyMagicLinkResponse } from '../components/app/VerifyMagicLink.js'\nexport { VerifyMagicLink } from '../components/app/VerifyMagicLink.js'\n\nexport type { SubscriberContextType } from '../contexts/SubscriberProvider.js'\nexport { SubscriberProvider, useSubscriber } from '../contexts/SubscriberProvider.js'\n\nexport { getServerUrl } from '../server-functions/serverUrl.js'\n"],"names":["RequestMagicLink","RequestOrSubscribe","Subscribe","SubscriberMenu","Unsubscribe","VerifyMagicLink","SubscriberProvider","useSubscriber","getServerUrl"],"mappings":"AACA,SAASA,gBAAgB,QAAQ,wCAAuC;AAExE,SAASC,kBAAkB,QAAQ,0CAAyC;AAG5E,SAASC,SAAS,QAAQ,iCAAgC;AAE1D,SAASC,cAAc,QAAQ,sCAAqC;AAGpE,SAASC,WAAW,QAAQ,mCAAkC;AAG9D,SAASC,eAAe,QAAQ,uCAAsC;AAGtE,SAASC,kBAAkB,EAAEC,aAAa,QAAQ,oCAAmC;AAErF,SAASC,YAAY,QAAQ,mCAAkC"}
@@ -7,3 +7,6 @@ export declare const getHash: (token: string) => {
7
7
  token: string;
8
8
  tokenHash: string;
9
9
  };
10
+ export declare const getHmacHash: (content: string) => {
11
+ hashToken: string;
12
+ };
@@ -1,7 +1,8 @@
1
1
  import crypto from 'crypto';
2
+ const SECRET_KEY = process.env.SUBSCRIBERS_SECRET || 'your-very-secure-secret';
2
3
  export const getTokenAndHash = (milliseconds)=>{
3
4
  const token = crypto.randomBytes(32).toString('hex');
4
- const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
5
+ const tokenHash = crypto.createHash('sha256').update(SECRET_KEY + token).digest('hex');
5
6
  const expiresAt = milliseconds ? new Date(Date.now() + milliseconds) : undefined;
6
7
  return {
7
8
  expiresAt,
@@ -10,11 +11,20 @@ export const getTokenAndHash = (milliseconds)=>{
10
11
  };
11
12
  };
12
13
  export const getHash = (token)=>{
13
- const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
14
+ const tokenHash = crypto.createHash('sha256').update(SECRET_KEY + token).digest('hex');
14
15
  return {
15
16
  token,
16
17
  tokenHash
17
18
  };
18
19
  };
20
+ export const getHmacHash = (content)=>{
21
+ // Create HMAC-SHA256 hash
22
+ const hmac = crypto.createHmac('sha256', SECRET_KEY);
23
+ hmac.update(`${content}`);
24
+ const hashToken = hmac.digest('hex');
25
+ return {
26
+ hashToken
27
+ };
28
+ };
19
29
 
20
30
  //# sourceMappingURL=token.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/helpers/token.ts"],"sourcesContent":["import crypto from 'crypto'\n\nexport const getTokenAndHash = (milliseconds?: number) => {\n const token = crypto.randomBytes(32).toString('hex')\n const tokenHash = crypto.createHash('sha256').update(token).digest('hex')\n const expiresAt = milliseconds ? new Date(Date.now() + milliseconds) : undefined\n\n return { expiresAt, token, tokenHash }\n}\n\nexport const getHash = (token: string) => {\n const tokenHash = crypto.createHash('sha256').update(token).digest('hex')\n return { token, tokenHash }\n}\n"],"names":["crypto","getTokenAndHash","milliseconds","token","randomBytes","toString","tokenHash","createHash","update","digest","expiresAt","Date","now","undefined","getHash"],"mappings":"AAAA,OAAOA,YAAY,SAAQ;AAE3B,OAAO,MAAMC,kBAAkB,CAACC;IAC9B,MAAMC,QAAQH,OAAOI,WAAW,CAAC,IAAIC,QAAQ,CAAC;IAC9C,MAAMC,YAAYN,OAAOO,UAAU,CAAC,UAAUC,MAAM,CAACL,OAAOM,MAAM,CAAC;IACnE,MAAMC,YAAYR,eAAe,IAAIS,KAAKA,KAAKC,GAAG,KAAKV,gBAAgBW;IAEvE,OAAO;QAAEH;QAAWP;QAAOG;IAAU;AACvC,EAAC;AAED,OAAO,MAAMQ,UAAU,CAACX;IACtB,MAAMG,YAAYN,OAAOO,UAAU,CAAC,UAAUC,MAAM,CAACL,OAAOM,MAAM,CAAC;IACnE,OAAO;QAAEN;QAAOG;IAAU;AAC5B,EAAC"}
1
+ {"version":3,"sources":["../../src/helpers/token.ts"],"sourcesContent":["import crypto from 'crypto'\n\nconst SECRET_KEY = process.env.SUBSCRIBERS_SECRET || 'your-very-secure-secret'\n\nexport const getTokenAndHash = (milliseconds?: number) => {\n const token = crypto.randomBytes(32).toString('hex')\n const tokenHash = crypto\n .createHash('sha256')\n .update(SECRET_KEY + token)\n .digest('hex')\n const expiresAt = milliseconds ? new Date(Date.now() + milliseconds) : undefined\n\n return { expiresAt, token, tokenHash }\n}\n\nexport const getHash = (token: string) => {\n const tokenHash = crypto\n .createHash('sha256')\n .update(SECRET_KEY + token)\n .digest('hex')\n return { token, tokenHash }\n}\n\nexport const getHmacHash = (content: string): { hashToken: string } => {\n // Create HMAC-SHA256 hash\n const hmac = crypto.createHmac('sha256', SECRET_KEY)\n hmac.update(`${content}`)\n const hashToken = hmac.digest('hex')\n\n return { hashToken }\n}\n"],"names":["crypto","SECRET_KEY","process","env","SUBSCRIBERS_SECRET","getTokenAndHash","milliseconds","token","randomBytes","toString","tokenHash","createHash","update","digest","expiresAt","Date","now","undefined","getHash","getHmacHash","content","hmac","createHmac","hashToken"],"mappings":"AAAA,OAAOA,YAAY,SAAQ;AAE3B,MAAMC,aAAaC,QAAQC,GAAG,CAACC,kBAAkB,IAAI;AAErD,OAAO,MAAMC,kBAAkB,CAACC;IAC9B,MAAMC,QAAQP,OAAOQ,WAAW,CAAC,IAAIC,QAAQ,CAAC;IAC9C,MAAMC,YAAYV,OACfW,UAAU,CAAC,UACXC,MAAM,CAACX,aAAaM,OACpBM,MAAM,CAAC;IACV,MAAMC,YAAYR,eAAe,IAAIS,KAAKA,KAAKC,GAAG,KAAKV,gBAAgBW;IAEvE,OAAO;QAAEH;QAAWP;QAAOG;IAAU;AACvC,EAAC;AAED,OAAO,MAAMQ,UAAU,CAACX;IACtB,MAAMG,YAAYV,OACfW,UAAU,CAAC,UACXC,MAAM,CAACX,aAAaM,OACpBM,MAAM,CAAC;IACV,OAAO;QAAEN;QAAOG;IAAU;AAC5B,EAAC;AAED,OAAO,MAAMS,cAAc,CAACC;IAC1B,0BAA0B;IAC1B,MAAMC,OAAOrB,OAAOsB,UAAU,CAAC,UAAUrB;IACzCoB,KAAKT,MAAM,CAAC,GAAGQ,SAAS;IACxB,MAAMG,YAAYF,KAAKR,MAAM,CAAC;IAE9B,OAAO;QAAEU;IAAU;AACrB,EAAC"}