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.
- package/README.md +4 -1
- package/dist/components/app/RequestMagicLink.js +45 -32
- package/dist/components/app/RequestMagicLink.js.map +1 -1
- package/dist/components/app/Subscribe.js +23 -8
- package/dist/components/app/Subscribe.js.map +1 -1
- package/dist/components/app/Unsubscribe.d.ts +31 -0
- package/dist/components/app/Unsubscribe.js +155 -0
- package/dist/components/app/Unsubscribe.js.map +1 -0
- package/dist/components/app/VerifyMagicLink.js +2 -6
- package/dist/components/app/VerifyMagicLink.js.map +1 -1
- package/dist/endpoints/requestMagicLink.d.ts +3 -1
- package/dist/endpoints/requestMagicLink.js +13 -10
- package/dist/endpoints/requestMagicLink.js.map +1 -1
- package/dist/endpoints/subscribe.js +22 -8
- package/dist/endpoints/subscribe.js.map +1 -1
- package/dist/endpoints/unsubscribe.d.ts +21 -0
- package/dist/endpoints/unsubscribe.js +118 -0
- package/dist/endpoints/unsubscribe.js.map +1 -0
- package/dist/endpoints/verifyMagicLink.js +3 -2
- package/dist/endpoints/verifyMagicLink.js.map +1 -1
- package/dist/exports/ui.d.ts +2 -0
- package/dist/exports/ui.js +1 -0
- package/dist/exports/ui.js.map +1 -1
- package/dist/helpers/token.d.ts +3 -0
- package/dist/helpers/token.js +12 -2
- package/dist/helpers/token.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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>
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"AAGA,SAASA,qBAAqB,QAAQ,gCAA+B;AACrE,SAASC,eAAe,QAAQ,sBAAqB;AACrD,SAASC,YAAY,QAAQ,6BAA4B;AAoBzD;;;;;;;;CAQC,GACD,SAASC,wBAAwB,EAC/BC,4BAA4BJ,qBAAqB,EAGlD;IACC;;;;;;GAMC,GACD,MAAMK,mBAAmC,OAAOC;QAC9C,MAAMC,OAAOD,KAAKE,OAAO,MAAMF,IAAIE,IAAI,KAAK,CAAC;QAC7C,MAAM,EAAEC,KAAK,EAAEC,MAAM,EAAEC,SAAS,EAAE,GAChCJ,KAAK,kBAAkB;;QACzB,kDAAkD;QAElD,EAAE;QACF,UAAU;QACV,gEAAgE;QAChE,gDAAgD;QAChD,EAAE;QACF,MAAMK,mBAAmB,OAAO,EAC9BF,MAAM,EACNG,QAAQ,EACRC,MAAM,EACNC,iBAAiB,EACjBC,wBAAwB,EAQzB;YACC,MAAMV,IAAIW,OAAO,CAACC,MAAM,CAAC;gBACvBC,YAAYf;gBACZG,MAAM;oBACJE;oBACAC;oBACAG;oBACAC,QAAQA,UAAU;oBAClBC;oBACAC,0BAA0BA,0BAA0BI;gBACtD;gBACAC,OAAO;YACT;QACF;QACA,MAAMC,mBAAmB,OAAO,EAC9BC,EAAE,EACFb,MAAM,EACNG,QAAQ,EACRC,MAAM,EACNC,iBAAiB,EACjBC,wBAAwB,EAQzB;YACC,MAAMQ,gBAAgB,MAAMlB,IAAIW,OAAO,CAACQ,MAAM,CAAC;gBAC7CF;gBACAJ,YAAYf;gBACZG,MAAM;oBACJG;oBACAG;oBACAC;oBACAC;oBACAC,0BAA0BA,0BAA0BI,iBAAiB;gBACvE;gBACAM,OAAO;YACT;YACA,OAAOF;QACT;QACA,MAAMG,kBAAkB,OAAO,EAC7BlB,KAAK,EACLmB,QAAQ,EACRC,OAAO,EACPC,OAAO,EACPC,KAAK,EACLpB,SAAS,EAQV;YACC,MAAMqB,YAAY,GAAGrB,YAAYA,WAAWsB,SAAS,MAAM,IAAI,MAAM,EAAEF,MAAM,OAAO,EAAEtB,OAAO;YAC7F,MAAMyB,OAAOL,UAAU,CAAC,YAAY,EAAEG,UAAU,EAAE,EAAEJ,SAAS,QAAQ,CAAC;YACtE,MAAMO,cAAc,MAAM7B,IAAIW,OAAO,CAACmB,SAAS,CAAC;gBAC9CF;gBACAJ;gBACAO,IAAI5B;YACN;YACAH,IAAIW,OAAO,CAACqB,MAAM,CAACC,IAAI,CAAC,CAAC,wBAAwB,EAAEP,WAAW;YAC9D,OAAOG;QACT;QAEA,EAAE;QACF,iBAAiB;QACjB,EAAE;QACF,gBAAgB;QAChB,IAAI,CAAC1B,OAAO;YACVH,IAAIW,OAAO,CAACqB,MAAM,CAACE,KAAK,CACtBC,KAAKC,SAAS,CACZ;gBAAEF,OAAO;gBAAYG,KAAK,IAAIC,OAAOxB,WAAW;YAAG,GACnDyB,WACA;YAGJ,OAAOC,SAAStC,IAAI,CAClB;gBAAEgC,OAAO;gBAAYG,KAAK,IAAIC,OAAOxB,WAAW;YAAG,GACnD;gBAAEN,QAAQ;YAAI;QAElB;QAEA,EAAE;QACF,yBAAyB;QACzB,MAAM,EAAEiC,kBAAkB,EAAEC,gBAAgB,EAAE,GAAG,MAAM9C,aAAaI,IAAIW,OAAO,EAAEP;QAEjF,IAAIqC,oBAAoB;YACtBzC,IAAIW,OAAO,CAACqB,MAAM,CAACE,KAAK,CACtBC,KAAKC,SAAS,CACZ;gBACEF,OAAO,oBAAoBC,KAAKC,SAAS,CAAChC;gBAC1CiC,KAAK,IAAIC,OAAOxB,WAAW;YAC7B,GACAyB,WACA;YAGJ,OAAOC,SAAStC,IAAI,CAClB;gBACEgC,OAAO,oBAAoBC,KAAKC,SAAS,CAAChC;gBAC1CiC,KAAK,IAAIC,OAAOxB,WAAW;YAC7B,GACA;gBAAEN,QAAQ;YAAI;QAElB;QAEA,EAAE;QACF,2BAA2B;QAC3B,MAAMmC,cAAc,MAAM3C,IAAIW,OAAO,CAACiC,IAAI,CAAC;YACzC/B,YAAYf;YACZ+C,OAAO;gBACL1C,OAAO;oBAAE2C,QAAQ3C;gBAAM;YACzB;QACF;QACA,MAAM4C,aAAaJ,YAAYK,IAAI,CAAC,EAAE;QAEtC,EAAE;QACF,+CAA+C;QAC/C,mBAAmB;QACnB,EAAE;QACF,2DAA2D;QAC3D,EAAE;QACF,IAAIhD,IAAIiD,IAAI,IAAIjD,IAAIiD,IAAI,CAAC9C,KAAK,IAAIA,OAAO;YACvC,EAAE;YACF,qDAAqD;YACrD,EAAE;YACFH,IAAIW,OAAO,CAACqB,MAAM,CAACE,KAAK,CACtBC,KAAKC,SAAS,CACZ;gBACEF,OAAO,mBAAmB/B;gBAC1BkC,KAAK,IAAIC,OAAOxB,WAAW;YAC7B,GACAyB,WACA;YAGJ,OAAOC,SAAStC,IAAI,CAClB;gBACEgC,OAAO,mBAAmB/B;gBAC1BkC,KAAK,IAAIC,OAAOxB,WAAW;YAC7B,GACA;gBAAEN,QAAQ;YAAI;QAElB;QAEA,EAAE;QACF,2DAA2D;QAC3D,EAAE;QACF,IAAI,CAACuC,YAAY;YACf,EAAE;YACF,2CAA2C;YAC3C,wCAAwC;YACxC,0BAA0B;YAC1B,sCAAsC;YACtC,EAAE;YACF,MAAM,EAAEG,SAAS,EAAEzB,KAAK,EAAE0B,SAAS,EAAE,GAAGxD,gBAAgB,KAAK,KAAK,MAAM,qBAAqB;;YAC7F,MAAM,EAAEwD,WAAWC,UAAU,EAAE,GAAGzD,kBAAkB,aAAa;;YACjE,MAAMW,iBAAiB;gBACrBH;gBACAC;gBACAG,UAAU6C;gBACV5C,QAAQ;gBACRC,mBAAmB0C;gBACnBzC,0BAA0BwC;YAC5B;YAEA,EAAE;YACF,aAAa;YACb,MAAMrB,cAAc,MAAMR,gBAAgB;gBACxClB;gBACAmB,UAAU;gBACVC,SAAStB,KAAKsB,OAAO,IAAI,CAAC,8CAA8C,CAAC;gBACzEC,SAASvB,KAAKuB,OAAO,IAAI;gBACzBC;gBACApB;YACF;YACA,IAAI,CAACwB,aAAa;gBAChB7B,IAAIW,OAAO,CAACqB,MAAM,CAACE,KAAK,CACtBC,KAAKC,SAAS,CACZ;oBAAEF,OAAO;oBAAwBG,KAAK,IAAIC,OAAOxB,WAAW;gBAAG,GAC/DyB,WACA;gBAGJ,OAAOC,SAAStC,IAAI,CAClB;oBAAEgC,OAAO;oBAAwBG,KAAK,IAAIC,OAAOxB,WAAW;gBAAG,GAC/D;oBAAEN,QAAQ;gBAAI;YAElB;YACA,OAAOgC,SAAStC,IAAI,CAAC;gBAAE2B;gBAAaQ,KAAK,IAAIC,OAAOxB,WAAW;YAAG;QAClE,EAAE;QACJ;QACA,EAAE;QACF,2DAA2D;QAC3D,EAAE;QACF,IAAI,CAACd,IAAIiD,IAAI,IAAIF,YAAY;YAC3B,EAAE;YACF,qCAAqC;YACrC,sCAAsC;YACtC,EAAE;YACF,MAAM,EAAEG,SAAS,EAAEzB,KAAK,EAAE0B,SAAS,EAAE,GAAGxD,gBAAgB,KAAK,KAAK,MAAM,qBAAqB;;YAC7F,iDAAiD;YACjD,MAAMuB,gBAAgB,MAAMF,iBAAiB;gBAC3CC,IAAI8B,WAAW9B,EAAE;gBACjBR,mBAAmB0C;gBACnBzC,0BAA0BwC;YAC5B;YACA,IAAI,CAAChC,eAAe;gBAClBlB,IAAIW,OAAO,CAACqB,MAAM,CAACE,KAAK,CACtBC,KAAKC,SAAS,CACZ;oBAAEF,OAAO;oBAAiBG,KAAK,IAAIC,OAAOxB,WAAW;gBAAG,GACxDyB,WACA;gBAGJ,OAAOC,SAAStC,IAAI,CAClB;oBAAEgC,OAAO;oBAAiBG,KAAK,IAAIC,OAAOxB,WAAW;gBAAG,GACxD;oBAAEN,QAAQ;gBAAI;YAElB;YAEA,EAAE;YACF,aAAa;YACb,MAAMqB,cAAc,MAAMR,gBAAgB;gBACxClB;gBACAmB,UAAU;gBACVC,SAAStB,KAAKsB,OAAO,IAAI,CAAC,gDAAgD,CAAC;gBAC3EC,SAASvB,KAAKuB,OAAO,IAAI;gBACzBC;gBACApB;YACF;YACA,IAAI,CAACwB,aAAa;gBAChB7B,IAAIW,OAAO,CAACqB,MAAM,CAACE,KAAK,CACtBC,KAAKC,SAAS,CACZ;oBAAEF,OAAO;oBAAwBG,KAAK,IAAIC,OAAOxB,WAAW;gBAAG,GAC/DyB,WACA;gBAGJ,OAAOC,SAAStC,IAAI,CAClB;oBAAEgC,OAAO;oBAAwBG,KAAK,IAAIC,OAAOxB,WAAW;gBAAG,GAC/D;oBAAEN,QAAQ;gBAAI;YAElB;YACA,OAAOgC,SAAStC,IAAI,CAAC;gBAAE2B;gBAAaQ,KAAK,IAAIC,OAAOxB,WAAW;YAAG;QACpE;QACA,EAAE;QACF,2DAA2D;QAC3D,EAAE;QACF,IAAId,IAAIiD,IAAI,IAAIF,cAAcA,WAAWvC,MAAM,IAAI,WAAW;YAC5D,EAAE;YACF,0DAA0D;YAC1D,sCAAsC;YACtC,EAAE;YACF,MAAM,EAAE0C,SAAS,EAAEzB,KAAK,EAAE0B,SAAS,EAAE,GAAGxD,gBAAgB,KAAK,KAAK,MAAM,qBAAqB;;YAC7F,iDAAiD;YACjD,MAAMuB,gBAAgB,MAAMF,iBAAiB;gBAC3CC,IAAI8B,WAAW9B,EAAE;gBACjBR,mBAAmB0C;gBACnBzC,0BAA0BwC;YAC5B;YACA,IAAI,CAAChC,eAAe;gBAClBlB,IAAIW,OAAO,CAACqB,MAAM,CAACE,KAAK,CACtBC,KAAKC,SAAS,CACZ;oBAAEF,OAAO;oBAAiBG,KAAK,IAAIC,OAAOxB,WAAW;gBAAG,GACxDyB,WACA;gBAGJ,OAAOC,SAAStC,IAAI,CAClB;oBAAEgC,OAAO;oBAAiBG,KAAK,IAAIC,OAAOxB,WAAW;gBAAG,GACxD;oBAAEN,QAAQ;gBAAI;YAElB;YAEA,MAAMqB,cAAc,MAAMR,gBAAgB;gBACxClB;gBACAmB,UAAU;gBACVC,SAAStB,KAAKsB,OAAO,IAAI,CAAC,yCAAyC,CAAC;gBACpEC,SAASvB,KAAKuB,OAAO,IAAI;gBACzBC;gBACApB;YACF;YACA,IAAI,CAACwB,aAAa;gBAChB7B,IAAIW,OAAO,CAACqB,MAAM,CAACE,KAAK,CACtBC,KAAKC,SAAS,CACZ;oBAAEF,OAAO;oBAAwBG,KAAK,IAAIC,OAAOxB,WAAW;gBAAG,GAC/DyB,WACA;gBAGJ,OAAOC,SAAStC,IAAI,CAClB;oBAAEgC,OAAO;oBAAwBG,KAAK,IAAIC,OAAOxB,WAAW;gBAAG,GAC/D;oBAAEN,QAAQ;gBAAI;YAElB;YACA,OAAOgC,SAAStC,IAAI,CAAC;gBAAE2B;gBAAaQ,KAAK,IAAIC,OAAOxB,WAAW;YAAG;QACpE;QAEA,EAAE;QACF,2DAA2D;QAC3D,EAAE;QACF,IAAId,IAAIiD,IAAI,IAAIF,cAAcA,WAAWvC,MAAM,IAAI,WAAW;YAC5D,EAAE;YACF,8CAA8C;YAC9C,oCAAoC;YACpC,wDAAwD;YACxD,2FAA2F;YAC3F,EAAE;YACF,MAAM,EAAE2C,SAAS,EAAE,GAAGxD,kBAAkB,qBAAqB;;YAC7D,gCAAgC;YAChC,MAAMuB,gBAAiB,MAAMF,iBAAiB;gBAC5CC,IAAI8B,WAAW9B,EAAE;gBACjBb,QAAQsC;gBACRnC,UAAU4C;gBACV3C,QAAQ;gBACRC,mBAAmB;gBACnBC,0BAA0B;YAC5B;YAEA,gDAAgD;YAChD,OAAO8B,SAAStC,IAAI,CAAC;gBACnBC,OAAOe,cAAcf,KAAK;gBAC1BkC,KAAK,IAAIC,OAAOxB,WAAW;gBAC3BV,QAAQc,cAAcd,MAAM;YAC9B;QACF;QACA,EAAE;QACF,gBAAgB;QAChB,EAAE;QACFJ,IAAIW,OAAO,CAACqB,MAAM,CAACE,KAAK,CACtBC,KAAKC,SAAS,CACZ;YAAEF,OAAO;YAAiBG,KAAK,IAAIC,OAAOxB,WAAW;QAAG,GACxDyB,WACA;QAGJ,OAAOC,SAAStC,IAAI,CAClB;YAAEgC,OAAO;YAAiBG,KAAK,IAAIC,OAAOxB,WAAW;QAAG,GACxD;YAAEN,QAAQ;QAAI;IAElB;IAEA,mFAAmF,GACnF,MAAM6C,oBAA8B;QAClCC,SAASvD;QACTwD,QAAQ;QACRC,MAAM;IACR;IAEA,OAAOH;AACT;AAEA,eAAexD,wBAAuB"}
|
|
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":"AAGA,SAASA,qBAAqB,QAAQ,gCAA+B;AACrE,SAASC,WAAW,QAAQ,sBAAqB;AAcjD;;;;;;;;CAQC,GACD,SAASC,0BAA0B,EACjCC,4BAA4BH,qBAAqB,EAGlD;IACC;;;;;;GAMC,GACD,MAAMI,qBAAqC,OAAOC;QAChD,MAAMC,OAAOD,KAAKE,OAAO,MAAMF,IAAIE,IAAI,KAAK,CAAC;QAC7C,MAAM,EAAEC,KAAK,EAAEC,gBAAgB,EAAE,GAAgD,MAAMH,KAAK,kBAAkB;;QAC9G,kDAAkD;QAElD,EAAE;QACF,iBAAiB;QACjB,EAAE;QACF,2BAA2B;QAC3B,IAAI,CAACG,kBAAkB;YACrB,MAAMC,SAAS;gBAAEC,OAAO;gBAAYC,KAAK,IAAIC,OAAOC,WAAW;YAAG;YAClET,IAAIU,OAAO,CAACC,MAAM,CAACL,KAAK,CACtB,CAAC,mCAAmC,EAAEM,KAAKC,SAAS,CAACR,QAAQS,WAAW,IAAI;YAE9E,OAAOC,SAASb,IAAI,CAACG;QACvB;QAEA,EAAE;QACF,0BAA0B;QAC1B,MAAM,EAAEW,WAAWC,sBAAsB,EAAE,GAAGrB,YAAYO;QAC1D,IAAIC,oBAAoBa,wBAAwB;YAC9C,MAAMZ,SAAS;gBAAEC,OAAO;gBAAYC,KAAK,IAAIC,OAAOC,WAAW;YAAG;YAClET,IAAIU,OAAO,CAACC,MAAM,CAACL,KAAK,CACtB,CAAC,6CAA6C,EAAEM,KAAKC,SAAS,CAACR,QAAQS,WAAW,IAAI;YAExF,OAAOC,SAASb,IAAI,CAACG;QACvB;QAEA,EAAE;QACF,4BAA4B;QAC5B,MAAMa,cAAc,MAAMlB,IAAIU,OAAO,CAACS,IAAI,CAAC;YACzCC,YAAYtB;YACZuB,OAAO;gBACLlB,OAAO;oBAAEmB,QAAQnB;gBAAM;YACzB;QACF;QACA,MAAMoB,aAAaL,YAAYM,IAAI,CAAC,EAAE;QAEtC,IAAI,CAACD,YAAY;YACf,MAAMlB,SAAS;gBAAEC,OAAO;gBAAYC,KAAK,IAAIC,OAAOC,WAAW;YAAG;YAClET,IAAIU,OAAO,CAACC,MAAM,CAACL,KAAK,CACtB,CAAC,6BAA6B,EAAEM,KAAKC,SAAS,CAACR,QAAQS,WAAW,IAAI;YAExE,OAAOC,SAASb,IAAI,CAACG;QACvB;QAEA,EAAE;QACF,8CAA8C;QAC9C,IAAIL,IAAIyB,IAAI,IAAIzB,IAAIyB,IAAI,CAACtB,KAAK,IAAIoB,WAAWpB,KAAK,EAAE;YAClD,EAAE;YACF,qDAAqD;YACrD,EAAE;YACF,MAAME,SAAS;gBACbC,OAAO,mBAAmBiB,WAAWpB,KAAK;gBAC1CI,KAAK,IAAIC,OAAOC,WAAW;YAC7B;YACAT,IAAIU,OAAO,CAACC,MAAM,CAACL,KAAK,CACtB,CAAC,4BAA4B,EAAEM,KAAKC,SAAS,CAACR,QAAQS,WAAW,IAAI;YAEvE,OAAOC,SAASb,IAAI,CAACG;QACvB;QAEA,EAAE;QACF,0DAA0D;QAC1D,uBAAuB;QAEvB,MAAMqB,gBAAgB,MAAM1B,IAAIU,OAAO,CAACiB,MAAM,CAAC;YAC7CC,IAAIL,WAAWK,EAAE;YACjBR,YAAYtB;YACZG,MAAM;gBACJ4B,QAAQ;YACV;YACAC,OAAO;QACT;QACA,IAAI,CAACJ,eAAe;YAClB,MAAMrB,SAAS;gBACbC,OAAO;gBACPC,KAAK,IAAIC,OAAOC,WAAW;YAC7B;YACAT,IAAIU,OAAO,CAACC,MAAM,CAACL,KAAK,CACtB,CAAC,qCAAqC,EAAEM,KAAKC,SAAS,CAACR,QAAQS,WAAW,IAAI;YAEhF,OAAOC,SAASb,IAAI,CAACG,QAAQ;gBAAEwB,QAAQ;YAAI;QAC7C;QAEA,EAAE;QACF,UAAU;QACV,EAAE;QACF,MAAMxB,SAAS;YAAE0B,SAAS;YAAgBxB,KAAK,IAAIC,OAAOC,WAAW;QAAG;QACxET,IAAIU,OAAO,CAACC,MAAM,CAACL,KAAK,CACtB,CAAC,kCAAkC,EAAEM,KAAKC,SAAS,CAACR,QAAQS,WAAW,IAAI;QAE7E,OAAOC,SAASb,IAAI,CAACG;IACvB;IAEA,mFAAmF,GACnF,MAAM2B,sBAAgC;QACpCC,SAASlC;QACTmC,QAAQ;QACRC,MAAM;IACR;IAEA,OAAOH;AACT;AAEA,eAAenC,0BAAyB"}
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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":"AAGA,SAASA,qBAAqB,QAAQ,gCAA+B;AACrE,SAASC,OAAO,EAAEC,eAAe,QAAQ,sBAAqB;AAY9D;;;;;;;CAOC,GACD,SAASC,8BAA8B,EACrCC,4BAA4BJ,qBAAqB,EAGlD;IACC;;;;;;GAMC,GACD,MAAMK,yBAAyC,OAAOC;QACpD,MAAMC,UAAUD,KAAKE,OAAO,MAAMF,IAAIE,IAAI,KAAK,CAAC;QAChD,MAAM,EAAEC,KAAK,EAAEC,KAAK,EAAE,GAAqCH,QAAQ,qBAAqB;;QACxF,yDAAyD;QAEzD,IAAI,CAACE,SAAS,CAACC,OAAO;YACpB,OAAOC,SAASH,IAAI,CAClB;gBAAEI,OAAO;gBAAYC,KAAK,IAAIC,OAAOC,WAAW;YAAG,GACnD;gBAAEC,QAAQ;YAAI;QAElB;QAEA,MAAMC,cAAc,MAAMX,IAAIY,OAAO,CAACC,IAAI,CAAC;YACzCC,YAAYhB;YACZiB,OAAO;gBACLZ,OAAO;oBAAEa,QAAQb;gBAAM;YACzB;QACF;QAOA,MAAMc,OAAON,YAAYO,IAAI,CAAC,EAAE;QAEhC,IAAI,CAACD,MAAM;YACT,OAAOZ,SAASH,IAAI,CAClB;gBAAEI,OAAO;gBAAYC,KAAK,IAAIC,OAAOC,WAAW;YAAG,GACnD;gBAAEC,QAAQ;YAAI;QAElB;QAEA,MAAM,EAAES,SAAS,EAAE,GAAGxB,QAAQS;QAE9B,2BAA2B;QAC3B,wHAAwH;QACxH,IAAI;QACJ,IAAI,CAACa,KAAKG,wBAAwB,IAAID,aAAaF,KAAKI,iBAAiB,EAAE;YACzErB,IAAIY,OAAO,CAACU,MAAM,CAACC,IAAI,CAAC,CAAC,oBAAoB,EAAEJ,UAAU,IAAI,EAAEF,KAAKI,iBAAiB,EAAE;YACvF,OAAOhB,SAASH,IAAI,CAClB;gBAAEI,OAAO;gBAAsBC,KAAK,IAAIC,OAAOC,WAAW;YAAG,GAC7D;gBAAEC,QAAQ;YAAI;QAElB;QAEA,IAAI,IAAIF,KAAKA,KAAKD,GAAG,MAAM,IAAIC,KAAKS,KAAKG,wBAAwB,GAAG;YAClE,OAAOf,SAASH,IAAI,CAClB;gBAAEI,OAAO;gBAAiBC,KAAK,IAAIC,OAAOC,WAAW;YAAG,GACxD;gBAAEC,QAAQ;YAAI;QAElB;QAEA,cAAc;QACd,MAAMV,IAAIY,OAAO,CAACY,MAAM,CAAC;YACvBV,YAAYhB;YACZ2B,MAAM;gBACJC,UAAUP;YACZ;YACAJ,OAAO;gBACLZ,OAAO;oBAAEa,QAAQC,KAAKd,KAAK;gBAAC;YAC9B;QACF;QAEA,sCAAsC;QACtC,IAAIwB;QACJ,IAAI;YACF,MAAMC,WAAW,MAAMC,MACrB,GAAG7B,IAAIY,OAAO,CAACkB,MAAM,CAACC,SAAS,CAAC,KAAK,EAAEjC,0BAA0B,MAAM,CAAC,EACxE;gBACEkC,MAAMC,KAAKC,SAAS,CAAC;oBACnB/B;oBACAuB,UAAUP;gBACZ;gBACAgB,aAAa;gBACbR,SAAS;oBACP,gBAAgB;gBAClB;gBACAS,QAAQ;YACV;YAEF,IAAIR,YAAYA,SAASS,EAAE,EAAE;gBAC3BV,UAAUC,SAASD,OAAO;YAC5B;QACF,EAAE,OAAOrB,OAAO;YACd,qBAAqB;YACrB,OAAOD,SAASH,IAAI,CAAC;gBAAEI;YAAM,GAA8B;gBAAEI,QAAQ;YAAI;QAC3E;QACA,gCAAgC;QAEhC,MAAMA,SACJO,MAAMP,UAAU,YAAY,eAAeO,MAAMP;QAEnD,MAAM,EAAES,WAAWmB,UAAU,EAAE,GAAG1C,kBAAkB,aAAa;;QACjE,MAAM6B,OAAO;YACXC,UAAUY;YACV5B;YACAW,mBAAmB;YACnBD,0BAA0B;QAC5B;QACA,cAAc;QACd,MAAMpB,IAAIY,OAAO,CAACY,MAAM,CAAC;YACvBV,YAAYhB;YACZ2B;YACAV,OAAO;gBACLZ,OAAO;oBAAEa,QAAQC,KAAKd,KAAK;gBAAC;YAC9B;QACF;QAEA,OAAOE,SAASH,IAAI,CAClB;YACEqC,SAAS;YACThC,KAAK,IAAIC,OAAOC,WAAW;QAC7B,GACA;YAAEkB;QAAQ;IAEd;IAEA,yFAAyF,GACzF,MAAMa,0BAAoC;QACxCC,SAAS1C;QACTqC,QAAQ;QACRM,MAAM;IACR;IAEA,OAAOF;AACT;AAEA,eAAe3C,8BAA6B"}
|
package/dist/exports/ui.d.ts
CHANGED
|
@@ -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';
|
package/dist/exports/ui.js
CHANGED
|
@@ -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';
|
package/dist/exports/ui.js.map
CHANGED
|
@@ -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"}
|
package/dist/helpers/token.d.ts
CHANGED
package/dist/helpers/token.js
CHANGED
|
@@ -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,
|
|
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"}
|