payload-subscribers-plugin 0.0.1

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.
Files changed (133) hide show
  1. package/README.md +305 -0
  2. package/dist/collections/OptInChannels.d.ts +3 -0
  3. package/dist/collections/OptInChannels.js +44 -0
  4. package/dist/collections/OptInChannels.js.map +1 -0
  5. package/dist/collections/Subscribers.d.ts +8 -0
  6. package/dist/collections/Subscribers.js +88 -0
  7. package/dist/collections/Subscribers.js.map +1 -0
  8. package/dist/collections/fields/OptedInChannels.d.ts +2 -0
  9. package/dist/collections/fields/OptedInChannels.js +12 -0
  10. package/dist/collections/fields/OptedInChannels.js.map +1 -0
  11. package/dist/components/BeforeDashboardClient.d.ts +1 -0
  12. package/dist/components/BeforeDashboardClient.js +40 -0
  13. package/dist/components/BeforeDashboardClient.js.map +1 -0
  14. package/dist/components/BeforeDashboardServer.d.ts +2 -0
  15. package/dist/components/BeforeDashboardServer.js +22 -0
  16. package/dist/components/BeforeDashboardServer.js.map +1 -0
  17. package/dist/components/BeforeDashboardServer.module.css +5 -0
  18. package/dist/components/app/RequestMagicLink.d.ts +16 -0
  19. package/dist/components/app/RequestMagicLink.js +114 -0
  20. package/dist/components/app/RequestMagicLink.js.map +1 -0
  21. package/dist/components/app/RequestMagicLink.module.css +5 -0
  22. package/dist/components/app/RequestOrSubscribe.d.ts +17 -0
  23. package/dist/components/app/RequestOrSubscribe.js +28 -0
  24. package/dist/components/app/RequestOrSubscribe.js.map +1 -0
  25. package/dist/components/app/SelectOptInChannels.d.ts +20 -0
  26. package/dist/components/app/SelectOptInChannels.js +120 -0
  27. package/dist/components/app/SelectOptInChannels.js.map +1 -0
  28. package/dist/components/app/SelectOptInChannels.module.css +5 -0
  29. package/dist/components/app/Subscribe.d.ts +18 -0
  30. package/dist/components/app/Subscribe.js +169 -0
  31. package/dist/components/app/Subscribe.js.map +1 -0
  32. package/dist/components/app/Subscribe.module.css +5 -0
  33. package/dist/components/app/SubscriberMenu.d.ts +7 -0
  34. package/dist/components/app/SubscriberMenu.js +44 -0
  35. package/dist/components/app/SubscriberMenu.js.map +1 -0
  36. package/dist/components/app/VerifyMagicLink.d.ts +23 -0
  37. package/dist/components/app/VerifyMagicLink.js +169 -0
  38. package/dist/components/app/VerifyMagicLink.js.map +1 -0
  39. package/dist/components/app/VerifyMagicLink.module.css +5 -0
  40. package/dist/components/app/helpers.d.ts +1 -0
  41. package/dist/components/app/helpers.js +5 -0
  42. package/dist/components/app/helpers.js.map +1 -0
  43. package/dist/components/app/shared.module.css +14 -0
  44. package/dist/contexts/SubscriberProvider.d.ts +15 -0
  45. package/dist/contexts/SubscriberProvider.js +105 -0
  46. package/dist/contexts/SubscriberProvider.js.map +1 -0
  47. package/dist/copied/payload-types.d.ts +395 -0
  48. package/dist/copied/payload-types.js +15 -0
  49. package/dist/copied/payload-types.js.map +1 -0
  50. package/dist/copied/payload.config.d.ts +2 -0
  51. package/dist/endpoints/customEndpointHandler.d.ts +2 -0
  52. package/dist/endpoints/customEndpointHandler.js +7 -0
  53. package/dist/endpoints/customEndpointHandler.js.map +1 -0
  54. package/dist/endpoints/getOptInChannels.d.ts +19 -0
  55. package/dist/endpoints/getOptInChannels.js +42 -0
  56. package/dist/endpoints/getOptInChannels.js.map +1 -0
  57. package/dist/endpoints/logout.d.ts +20 -0
  58. package/dist/endpoints/logout.js +60 -0
  59. package/dist/endpoints/logout.js.map +1 -0
  60. package/dist/endpoints/requestMagicLink.d.ts +20 -0
  61. package/dist/endpoints/requestMagicLink.js +122 -0
  62. package/dist/endpoints/requestMagicLink.js.map +1 -0
  63. package/dist/endpoints/subscribe.d.ts +24 -0
  64. package/dist/endpoints/subscribe.js +343 -0
  65. package/dist/endpoints/subscribe.js.map +1 -0
  66. package/dist/endpoints/subscriberAuth.d.ts +22 -0
  67. package/dist/endpoints/subscriberAuth.js +69 -0
  68. package/dist/endpoints/subscriberAuth.js.map +1 -0
  69. package/dist/endpoints/verifyMagicLink.d.ts +20 -0
  70. package/dist/endpoints/verifyMagicLink.js +142 -0
  71. package/dist/endpoints/verifyMagicLink.js.map +1 -0
  72. package/dist/exports/client.d.ts +1 -0
  73. package/dist/exports/client.js +3 -0
  74. package/dist/exports/client.js.map +1 -0
  75. package/dist/exports/index.d.ts +1 -0
  76. package/dist/exports/index.js +3 -0
  77. package/dist/exports/index.js.map +1 -0
  78. package/dist/exports/rsc.d.ts +1 -0
  79. package/dist/exports/rsc.js +3 -0
  80. package/dist/exports/rsc.js.map +1 -0
  81. package/dist/exports/ui.d.ts +11 -0
  82. package/dist/exports/ui.js +9 -0
  83. package/dist/exports/ui.js.map +1 -0
  84. package/dist/helpers/serverConfig.d.ts +4 -0
  85. package/dist/helpers/serverConfig.js +22 -0
  86. package/dist/helpers/serverConfig.js.map +1 -0
  87. package/dist/helpers/testData.d.ts +2 -0
  88. package/dist/helpers/testData.js +4 -0
  89. package/dist/helpers/testData.js.map +1 -0
  90. package/dist/helpers/token.d.ts +9 -0
  91. package/dist/helpers/token.js +20 -0
  92. package/dist/helpers/token.js.map +1 -0
  93. package/dist/helpers/verifyOptIns.d.ts +5 -0
  94. package/dist/helpers/verifyOptIns.js +33 -0
  95. package/dist/helpers/verifyOptIns.js.map +1 -0
  96. package/dist/index.d.ts +26 -0
  97. package/dist/index.js +147 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/react-hooks/useServerUrl.d.ts +3 -0
  100. package/dist/react-hooks/useServerUrl.js +19 -0
  101. package/dist/react-hooks/useServerUrl.js.map +1 -0
  102. package/dist/server-functions/serverUrl.d.ts +3 -0
  103. package/dist/server-functions/serverUrl.js +31 -0
  104. package/dist/server-functions/serverUrl.js.map +1 -0
  105. package/dist/server-functions/subscriberAuth.d.ts +11 -0
  106. package/package.json +94 -0
  107. package/src/collections/OptInChannels.ts +45 -0
  108. package/src/collections/Subscribers.ts +99 -0
  109. package/src/collections/fields/OptedInChannels.ts +12 -0
  110. package/src/components/app/RequestMagicLink.tsx +129 -0
  111. package/src/components/app/RequestOrSubscribe.tsx +58 -0
  112. package/src/components/app/SelectOptInChannels.tsx +147 -0
  113. package/src/components/app/Subscribe.tsx +190 -0
  114. package/src/components/app/SubscriberMenu.tsx +46 -0
  115. package/src/components/app/VerifyMagicLink.tsx +197 -0
  116. package/src/components/app/helpers.ts +6 -0
  117. package/src/components/app/shared.module.css +14 -0
  118. package/src/contexts/SubscriberProvider.tsx +122 -0
  119. package/src/copied/payload-types.ts +478 -0
  120. package/src/endpoints/getOptInChannels.ts +56 -0
  121. package/src/endpoints/logout.ts +104 -0
  122. package/src/endpoints/requestMagicLink.ts +139 -0
  123. package/src/endpoints/subscribe.ts +435 -0
  124. package/src/endpoints/subscriberAuth.ts +100 -0
  125. package/src/endpoints/verifyMagicLink.ts +164 -0
  126. package/src/exports/index.ts +1 -0
  127. package/src/exports/ui.ts +17 -0
  128. package/src/helpers/testData.ts +2 -0
  129. package/src/helpers/token.ts +14 -0
  130. package/src/helpers/verifyOptIns.ts +39 -0
  131. package/src/index.ts +207 -0
  132. package/src/react-hooks/useServerUrl.tsx +18 -0
  133. package/src/server-functions/serverUrl.ts +38 -0
@@ -0,0 +1,139 @@
1
+ import type { CollectionSlug, Endpoint, PayloadHandler, PayloadRequest, TypedUser } from 'payload'
2
+
3
+ import crypto from 'crypto'
4
+ import { defaultCollectionSlug } from '../collections/Subscribers.js'
5
+
6
+ import { getTokenAndHash } from '../helpers/token.js'
7
+
8
+ export type RequestMagicLinkResponse =
9
+ | {
10
+ emailResult: any
11
+ now: string
12
+ }
13
+ | {
14
+ error: string
15
+ now: string
16
+ }
17
+
18
+ /**
19
+ * createEndpointRequestMagicLink
20
+ * @param options
21
+ * @returns
22
+ *
23
+ * Factory to generate the endpoint config with handler based on input option for subscribersCollectionSlug
24
+ *
25
+ */
26
+ function createEndpointRequestMagicLink({
27
+ subscribersCollectionSlug = defaultCollectionSlug,
28
+ }: {
29
+ subscribersCollectionSlug: CollectionSlug
30
+ }): Endpoint {
31
+ /**
32
+ * requestMagicLink Endpoint Handler
33
+ * @param req
34
+ * @data { email }
35
+ * @returns { status: 200, json: {message: string, now: date} }
36
+ * @returns { status: 400, json: {error: ('Bad data' | 'Unknown email result'), now: date} }
37
+ */
38
+ const requestMagicLinkHandler: PayloadHandler = async (req: PayloadRequest) => {
39
+ const data = req?.json ? await req.json() : {}
40
+ const { email, forwardUrl } = data // if by POST data
41
+ // const { email } = req.routeParams // if by path
42
+
43
+ if (!email) {
44
+ return Response.json(
45
+ { error: 'Bad data', now: new Date().toISOString() } as RequestMagicLinkResponse,
46
+ { status: 400 },
47
+ )
48
+ }
49
+
50
+ const userResults = await req.payload.find({
51
+ collection: subscribersCollectionSlug,
52
+ where: {
53
+ email: { equals: email },
54
+ },
55
+ })
56
+ const user = userResults.docs[0] as TypedUser
57
+
58
+ if (!user) {
59
+ //
60
+ // Create subscriber with status 'pending',
61
+ // and an invisible unknowable password,
62
+ //
63
+ const { tokenHash: tokenHash2 } = getTokenAndHash() // Unknowable
64
+ const createResult = await req.payload.create({
65
+ collection: subscribersCollectionSlug,
66
+ data: {
67
+ email,
68
+ password: tokenHash2,
69
+ status: 'pending',
70
+ },
71
+ draft: false,
72
+ })
73
+ if (!createResult) {
74
+ return Response.json(
75
+ { error: 'Bad data', now: new Date().toISOString() } as RequestMagicLinkResponse,
76
+ { status: 400 },
77
+ )
78
+ }
79
+ }
80
+
81
+ // Update user with verificationToken
82
+ const token = crypto.randomBytes(32).toString('hex')
83
+ const tokenHash = crypto.createHash('sha256').update(token).digest('hex')
84
+ const expiresAt = new Date(Date.now() + 15 * 60 * 1000) // 15 mins
85
+ await req.payload.update({
86
+ collection: subscribersCollectionSlug,
87
+ data: {
88
+ verificationToken: tokenHash,
89
+ verificationTokenExpires: expiresAt.toISOString(),
90
+ },
91
+ where: {
92
+ email: { equals: user.email },
93
+ },
94
+ })
95
+
96
+ // Send email
97
+ const forwardUrlParam = forwardUrl ? `&forwardUrl=${encodeURI(forwardUrl)}` : ''
98
+ const magicLink = `${req.payload.config.serverURL}/verify?token=${token}&email=${email}${forwardUrlParam}`
99
+ const subject = data.subject || 'Your Magic Login Link'
100
+ const message = `
101
+ ${data.message || '<p>Use this link to log in:</p>'}
102
+ <p><a href="${magicLink}"><b>Login</b></a></p>
103
+ `
104
+ const emailResult = await req.payload.sendEmail({
105
+ html: message,
106
+ subject,
107
+ to: user.email,
108
+ })
109
+ // req.payload.logger.info(`email result: ${JSON.stringify(emailResult)}`)
110
+ // return data; // Return data to allow normal submission if needed
111
+ if (!emailResult) {
112
+ return Response.json(
113
+ {
114
+ error: 'Unknown email result',
115
+ now: new Date().toISOString(),
116
+ } as RequestMagicLinkResponse,
117
+ { status: 400 },
118
+ )
119
+ }
120
+ req.payload.logger.info(`requestMagicLinkHandler email sent \n ${magicLink}`)
121
+ return Response.json({
122
+ emailResult,
123
+ now: new Date().toISOString(),
124
+ } as RequestMagicLinkResponse)
125
+ }
126
+
127
+ /**
128
+ * requestMagicLink Endpoint Config
129
+ */
130
+ const requestMagicLinkEndpoint: Endpoint = {
131
+ handler: requestMagicLinkHandler,
132
+ method: 'post',
133
+ path: '/emailToken',
134
+ }
135
+
136
+ return requestMagicLinkEndpoint
137
+ }
138
+
139
+ export default createEndpointRequestMagicLink
@@ -0,0 +1,435 @@
1
+ import type { CollectionSlug, Endpoint, PayloadHandler } from 'payload'
2
+ import type { Subscriber } from 'src/copied/payload-types.js'
3
+
4
+ import { defaultCollectionSlug } from '../collections/Subscribers.js'
5
+
6
+ import { getTokenAndHash } from '../helpers/token.js'
7
+ import { verifyOptIns } from '../helpers/verifyOptIns.js'
8
+
9
+ export type SubscribeResponse =
10
+ // When subscriber optIns are updated...
11
+ | {
12
+ email: string
13
+ now: string
14
+ optIns: string[]
15
+ }
16
+ // When a verify link is emailed...
17
+ | {
18
+ emailResult: any
19
+ now: string
20
+ }
21
+ // When any error occurs...
22
+ | {
23
+ error: string
24
+ now: string
25
+ }
26
+
27
+ /**
28
+ * createEndpointLogout
29
+ * @param options
30
+ * @returns
31
+ *
32
+ * Factory to generate the endpoint config with handler based on input option for subscribersCollectionSlug
33
+ *
34
+ */
35
+ function createEndpointSubscribe({
36
+ subscribersCollectionSlug = defaultCollectionSlug,
37
+ }: {
38
+ subscribersCollectionSlug: CollectionSlug
39
+ }): Endpoint {
40
+ /**
41
+ * subscribe Endpoint Handler
42
+ * @param req
43
+ * @data { email }
44
+ * @returns { status: 200, json: {message: string, now: date} }
45
+ * @returns { status: 400, json: {error: ('Bad data' | 'Already subscribed' | 'Unknown email result'), now: date} }
46
+ */
47
+ const subscribeHandler: PayloadHandler = async (req) => {
48
+ const data = req?.json ? await req.json() : {}
49
+ const {
50
+ afterVerifyUrl,
51
+ email,
52
+ optIns,
53
+ }: { afterVerifyUrl: string; email: string; optIns: string[] } = data // if by POST data
54
+ // const { email } = req.routeParams // if by path
55
+
56
+ //
57
+ // HELPERS
58
+ // Some of these functions make use of the scope within handler,
59
+ // and would have to be refactored if moved out.
60
+ //
61
+ const createSubscriber = async ({
62
+ optIns,
63
+ password,
64
+ status,
65
+ verificationToken,
66
+ verificationTokenExpires,
67
+ }: {
68
+ email: string
69
+ optIns?: string[]
70
+ password?: string
71
+ status?: 'pending' | 'subscribed' | 'unsubscribed'
72
+ verificationToken?: string
73
+ verificationTokenExpires?: Date
74
+ }) => {
75
+ await req.payload.create({
76
+ collection: subscribersCollectionSlug,
77
+ data: {
78
+ email,
79
+ optIns,
80
+ password,
81
+ status: status || 'pending',
82
+ verificationToken,
83
+ verificationTokenExpires: verificationTokenExpires?.toISOString(),
84
+ },
85
+ draft: false,
86
+ })
87
+ }
88
+ const updateSubscriber = async ({
89
+ id,
90
+ optIns,
91
+ password,
92
+ status,
93
+ verificationToken,
94
+ verificationTokenExpires,
95
+ }: {
96
+ id: string
97
+ optIns?: string[]
98
+ password?: string
99
+ status?: 'pending' | 'subscribed' | 'unsubscribed'
100
+ verificationToken?: string
101
+ verificationTokenExpires?: Date | null
102
+ }) => {
103
+ const updateResults = await req.payload.update({
104
+ id,
105
+ collection: subscribersCollectionSlug,
106
+ data: {
107
+ optIns,
108
+ password,
109
+ status,
110
+ verificationToken,
111
+ verificationTokenExpires: verificationTokenExpires?.toISOString() || null,
112
+ },
113
+ depth: 0,
114
+ })
115
+ return updateResults
116
+ }
117
+ const sendVerifyEmail = async ({
118
+ email,
119
+ forwardUrl,
120
+ linkText,
121
+ message,
122
+ subject,
123
+ token,
124
+ }: {
125
+ email: string
126
+ forwardUrl?: string
127
+ linkText: string
128
+ message: string
129
+ subject: string
130
+ token: string
131
+ }) => {
132
+ const forwardUrlParam = forwardUrl ? `&forwardUrl=${encodeURI(forwardUrl)}` : ''
133
+ const magicLink = `${req.payload.config.serverURL}/verify?token=${token}&email=${email}${forwardUrlParam}`
134
+ const html = message + `<p><a href="${magicLink}">${linkText}</a></p>`
135
+ const emailResult = await req.payload.sendEmail({
136
+ html,
137
+ subject,
138
+ to: email,
139
+ })
140
+ req.payload.logger.info(`subscribe email sent \n ${magicLink}`)
141
+ return emailResult
142
+ }
143
+
144
+ //
145
+ // VALIDATE INPUT
146
+ //
147
+ // Require email
148
+ if (!email) {
149
+ req.payload.logger.error(
150
+ JSON.stringify(
151
+ { error: 'Bad data', now: new Date().toISOString() } as SubscribeResponse,
152
+ undefined,
153
+ 2,
154
+ ),
155
+ )
156
+ return Response.json(
157
+ { error: 'Bad data', now: new Date().toISOString() } as SubscribeResponse,
158
+ { status: 400 },
159
+ )
160
+ }
161
+
162
+ //
163
+ // Validate OptInChannels
164
+ const { invalidOptInsInput, verifiedOptInIDs } = await verifyOptIns(req.payload, optIns)
165
+
166
+ if (invalidOptInsInput) {
167
+ req.payload.logger.error(
168
+ JSON.stringify(
169
+ {
170
+ error: 'Invalid input: ' + JSON.stringify(optIns),
171
+ now: new Date().toISOString(),
172
+ } as SubscribeResponse,
173
+ undefined,
174
+ 2,
175
+ ),
176
+ )
177
+ return Response.json(
178
+ {
179
+ error: 'Invalid input: ' + JSON.stringify(optIns),
180
+ now: new Date().toISOString(),
181
+ } as SubscribeResponse,
182
+ { status: 400 },
183
+ )
184
+ }
185
+
186
+ //
187
+ // Verify subscriber exists
188
+ const userResults = await req.payload.find({
189
+ collection: subscribersCollectionSlug,
190
+ where: {
191
+ email: { equals: email },
192
+ },
193
+ })
194
+ const subscriber = userResults.docs[0] as Subscriber
195
+
196
+ //
197
+ // Now we have a subscriber and validatedOptIns
198
+ // Handle scenarios
199
+ //
200
+ // ********************************************************
201
+ //
202
+ if (req.user && req.user.email != email) {
203
+ //
204
+ // Error: Auth-ed user doesn't match subscriber email
205
+ //
206
+ req.payload.logger.error(
207
+ JSON.stringify(
208
+ {
209
+ error: 'Unauthorized: ' + email,
210
+ now: new Date().toISOString(),
211
+ } as SubscribeResponse,
212
+ undefined,
213
+ 2,
214
+ ),
215
+ )
216
+ return Response.json(
217
+ {
218
+ error: 'Unauthorized: ' + email,
219
+ now: new Date().toISOString(),
220
+ } as SubscribeResponse,
221
+ { status: 400 },
222
+ )
223
+ }
224
+
225
+ //
226
+ // ********************************************************
227
+ //
228
+ if (!subscriber) {
229
+ //
230
+ // Create subscriber with status 'pending',
231
+ // and an invisible unknowable password,
232
+ // and send a verify email
233
+ // Pass all optIns through verify link
234
+ //
235
+ const { expiresAt, token, tokenHash } = getTokenAndHash(15 * 60 * 1000) // Use for magic link
236
+ const { tokenHash: tokenHash2 } = getTokenAndHash() // Unknowable
237
+ await createSubscriber({
238
+ email,
239
+ optIns,
240
+ password: tokenHash2,
241
+ status: 'pending',
242
+ verificationToken: tokenHash,
243
+ verificationTokenExpires: expiresAt,
244
+ })
245
+
246
+ //
247
+ // Send email
248
+ const emailResult = await sendVerifyEmail({
249
+ email,
250
+ forwardUrl: afterVerifyUrl,
251
+ linkText: '<b>Verify</b>',
252
+ message: data.message || `<p>Click here to verify your subscription:</p>`,
253
+ subject: data.subject || 'Please verify your subscription',
254
+ token,
255
+ })
256
+ if (!emailResult) {
257
+ req.payload.logger.error(
258
+ JSON.stringify(
259
+ { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,
260
+ undefined,
261
+ 2,
262
+ ),
263
+ )
264
+ return Response.json(
265
+ { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,
266
+ { status: 400 },
267
+ )
268
+ }
269
+ return Response.json({ emailResult, now: new Date().toISOString() } as SubscribeResponse)
270
+ //
271
+ }
272
+ //
273
+ // ********************************************************
274
+ //
275
+ if (!req.user && subscriber) {
276
+ //
277
+ // Send magic link to log the user in
278
+ // Pass all optIns through verify link
279
+ //
280
+ const { expiresAt, token, tokenHash } = getTokenAndHash(15 * 60 * 1000) // Use for magic link
281
+ // Update subscriber with token for pending email
282
+ const updateResults = await updateSubscriber({
283
+ id: subscriber.id,
284
+ verificationToken: tokenHash,
285
+ verificationTokenExpires: expiresAt,
286
+ })
287
+ if (!updateResults) {
288
+ req.payload.logger.error(
289
+ JSON.stringify(
290
+ { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,
291
+ undefined,
292
+ 2,
293
+ ),
294
+ )
295
+ return Response.json(
296
+ { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,
297
+ { status: 400 },
298
+ )
299
+ }
300
+
301
+ //
302
+ // Send email
303
+ const emailResult = await sendVerifyEmail({
304
+ email,
305
+ forwardUrl: afterVerifyUrl,
306
+ linkText: 'Verify',
307
+ message: data.message || `<h1>Click here to verify your subscription:</h1>`,
308
+ subject: data.subject || 'Please verify your subscription',
309
+ token,
310
+ })
311
+ if (!emailResult) {
312
+ req.payload.logger.error(
313
+ JSON.stringify(
314
+ { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,
315
+ undefined,
316
+ 2,
317
+ ),
318
+ )
319
+ return Response.json(
320
+ { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,
321
+ { status: 400 },
322
+ )
323
+ }
324
+ return Response.json({ emailResult, now: new Date().toISOString() } as SubscribeResponse)
325
+ }
326
+ //
327
+ // ********************************************************
328
+ //
329
+ if (req.user && subscriber && subscriber.status == 'pending') {
330
+ //
331
+ // Send magic link to verify the email and log the user in
332
+ // Pass all optIns through verify link
333
+ //
334
+ const { expiresAt, token, tokenHash } = getTokenAndHash(15 * 60 * 1000) // Use for magic link
335
+ // Create subscriber with token for pending email
336
+ const updateResults = await updateSubscriber({
337
+ id: subscriber.id,
338
+ verificationToken: tokenHash,
339
+ verificationTokenExpires: expiresAt,
340
+ })
341
+ if (!updateResults) {
342
+ req.payload.logger.error(
343
+ JSON.stringify(
344
+ { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,
345
+ undefined,
346
+ 2,
347
+ ),
348
+ )
349
+ return Response.json(
350
+ { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,
351
+ { status: 400 },
352
+ )
353
+ }
354
+
355
+ const emailResult = await sendVerifyEmail({
356
+ email,
357
+ forwardUrl: afterVerifyUrl,
358
+ linkText: 'Verify',
359
+ message: data.message || `<h1>Click here to verify your email:</h1>`,
360
+ subject: data.subject || 'Please verify your subscription',
361
+ token,
362
+ })
363
+ if (!emailResult) {
364
+ req.payload.logger.error(
365
+ JSON.stringify(
366
+ { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,
367
+ undefined,
368
+ 2,
369
+ ),
370
+ )
371
+ return Response.json(
372
+ { error: 'Unknown email result', now: new Date().toISOString() } as SubscribeResponse,
373
+ { status: 400 },
374
+ )
375
+ }
376
+ return Response.json({ emailResult, now: new Date().toISOString() } as SubscribeResponse)
377
+ }
378
+
379
+ //
380
+ // ********************************************************
381
+ //
382
+ if (req.user && subscriber && subscriber.status != 'pending') {
383
+ //
384
+ // Update subscriber with status 'subscribed',
385
+ // an invisible unknowable password,
386
+ // and if any optIns input exists, set subscriber optIns
387
+ // to EXACTLY verifiedOptInIDs (potentially unsubscribing from any not in verifiedOptInIDs)
388
+ //
389
+ const { tokenHash } = getTokenAndHash() // Use for magic link
390
+ // Update subscriber with optIns
391
+ const updateResults = (await updateSubscriber({
392
+ id: subscriber.id,
393
+ optIns: verifiedOptInIDs,
394
+ password: tokenHash,
395
+ status: 'subscribed',
396
+ verificationToken: '',
397
+ verificationTokenExpires: null,
398
+ })) as Subscriber
399
+
400
+ // Return results, including the verified optIns
401
+ return Response.json({
402
+ email: updateResults.email,
403
+ now: new Date().toISOString(),
404
+ optIns: updateResults.optIns,
405
+ } as SubscribeResponse)
406
+ }
407
+ //
408
+ // Uncaught case
409
+ //
410
+ req.payload.logger.error(
411
+ JSON.stringify(
412
+ { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,
413
+ undefined,
414
+ 2,
415
+ ),
416
+ )
417
+ return Response.json(
418
+ { error: 'Unknown error', now: new Date().toISOString() } as SubscribeResponse,
419
+ { status: 400 },
420
+ )
421
+ }
422
+
423
+ /**
424
+ * subscribe Endpoint Config
425
+ */
426
+ const subscribeEndpoint: Endpoint = {
427
+ handler: subscribeHandler,
428
+ method: 'post',
429
+ path: '/subscribe',
430
+ }
431
+
432
+ return subscribeEndpoint
433
+ }
434
+
435
+ export default createEndpointSubscribe
@@ -0,0 +1,100 @@
1
+ import { headers as nextHeaders } from 'next/headers.js'
2
+
3
+ import type { Subscriber } from '../copied/payload-types.js'
4
+
5
+ // If you're using Next.js, you'll have to import headers from next/headers, like so:
6
+ import type { CollectionSlug, Endpoint, PayloadHandler, Permissions } from 'payload'
7
+
8
+ import { defaultCollectionSlug } from '../collections/Subscribers.js'
9
+
10
+ export type SubscriberAuthResponse =
11
+ | {
12
+ error: string
13
+ now: string
14
+ }
15
+ | {
16
+ now: string
17
+ permissions: Permissions
18
+ subscriber: null | Subscriber
19
+ }
20
+
21
+ /**
22
+ * createEndpointLogout
23
+ * @param options
24
+ * @returns
25
+ *
26
+ * Factory to generate the endpoint config with handler based on input option for subscribersCollectionSlug
27
+ *
28
+ */
29
+ function createEndpointSubscriberAuth({
30
+ subscribersCollectionSlug = defaultCollectionSlug,
31
+ }: {
32
+ subscribersCollectionSlug: CollectionSlug
33
+ }): Endpoint {
34
+ /**
35
+ * subscriberAuth Endpoint Handler
36
+ * @param req
37
+ * @returns { status: 200, json: {message: string, now: date} }
38
+ * @returns { status: 400, json: {error: ('No subscriber authed' | catchError | 'Unknown error'), now: date} }
39
+ */
40
+ const subscriberAuthHandler: PayloadHandler = async (req) => {
41
+ // req.payload.logger.info('subscriberAuthHandler')
42
+ // Log the user in via Payload headers
43
+ const headers = await nextHeaders()
44
+
45
+ try {
46
+ const { permissions, user } = await req.payload.auth({
47
+ headers,
48
+ })
49
+
50
+ // req.payload.logger.info(`user = ${JSON.stringify(user)}`)
51
+ // req.payload.logger.info(`permissions = ${JSON.stringify(permissions)}`)
52
+
53
+ if (user && user.collection == subscribersCollectionSlug) {
54
+ const subscriber: Subscriber = user as Subscriber
55
+ if (subscriber.optIns) {
56
+ subscriber.optIns = subscriber.optIns.map((channel) =>
57
+ typeof channel == 'string' ? channel : channel.id,
58
+ )
59
+ }
60
+ return Response.json({
61
+ now: new Date().toISOString(),
62
+ permissions,
63
+ subscriber,
64
+ } as SubscriberAuthResponse)
65
+ }
66
+
67
+ // req.payload.logger.info('subscriberAuthHandler: No subscriber authed')
68
+ return Response.json(
69
+ {
70
+ // error: 'No subscriber authed',
71
+ now: new Date().toISOString(),
72
+ permissions,
73
+ subscriber: null,
74
+ } as SubscriberAuthResponse,
75
+ { headers, status: 200 },
76
+ )
77
+ } catch (error: unknown) {
78
+ // req.payload.logger.info(`subscriberAuth error: ${JSON.stringify(error)}`)
79
+ return Response.json(
80
+ {
81
+ error,
82
+ now: new Date().toISOString(),
83
+ } as SubscriberAuthResponse,
84
+ { headers, status: 400 },
85
+ )
86
+ }
87
+ }
88
+
89
+ /**
90
+ * subscriberAuth Endpoint Config
91
+ */
92
+ const subscriberAuthEndpoint: Endpoint = {
93
+ handler: subscriberAuthHandler,
94
+ method: 'post',
95
+ path: '/subscriberAuth',
96
+ }
97
+ return subscriberAuthEndpoint
98
+ }
99
+
100
+ export default createEndpointSubscriberAuth