@dev-crew-berlin/enter-js-utils 0.98.11 → 0.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/email-handlebars.d.ts +31 -0
- package/dist/lib/email-handlebars.js +436 -0
- package/dist/lib/email-handlebars.test.js +339 -0
- package/dist/lib/email-links.d.ts +46 -0
- package/dist/lib/email-links.js +68 -0
- package/dist/lib/email-links.test.js +348 -0
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +3 -1
- package/dist/lib/tokens.d.ts +20 -0
- package/dist/lib/tokens.js +48 -0
- package/dist/lib/tokens.test.js +508 -0
- package/dist/ui/companion-info.d.ts +3 -0
- package/dist/ui/companion-info.js +24 -8
- package/dist/ui/companion-info.stories.js +11 -0
- package/dist/ui/form-elements/input.d.ts +1 -1
- package/dist/ui/form-elements/select.d.ts +1 -1
- package/dist/ui/form-elements/select.js +4 -4
- package/dist/ui/tag.js +3 -3
- package/package.json +2 -1
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import jws from 'jws';
|
|
3
|
+
import { makeGoogleWalletLink, makeQrCodeLink, makeRegistrationLink, makeTrackingLink, makeUnsubscribeLink, makeWalletLink } from './email-links';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Mirrors FrontendConfig's link builders
|
|
7
|
+
* (enter-core/common/models/frontend_config.py) -- each describe block
|
|
8
|
+
* below corresponds to one make_*_link method there. Split into
|
|
9
|
+
* single-assertion cases (enter-core/tests/unittests/common/models/
|
|
10
|
+
* frontend_config_test.py follows the same style) so a failure names
|
|
11
|
+
* exactly which behaviour broke.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const options = {
|
|
15
|
+
jwtSecret: 'test-secret',
|
|
16
|
+
instanceName: 'my-instance',
|
|
17
|
+
domain: 'example.com'
|
|
18
|
+
};
|
|
19
|
+
function decode(link) {
|
|
20
|
+
const token = link.split('/').pop();
|
|
21
|
+
return jws.decode(token)?.payload;
|
|
22
|
+
}
|
|
23
|
+
function decodeQueryToken(link) {
|
|
24
|
+
const token = link.split('token=')[1];
|
|
25
|
+
return jws.decode(token)?.payload;
|
|
26
|
+
}
|
|
27
|
+
describe('makeTrackingLink', () => {
|
|
28
|
+
describe('tracking disabled', () => {
|
|
29
|
+
it('returns the raw target url unsigned', () => {
|
|
30
|
+
const link = makeTrackingLink(options, {
|
|
31
|
+
attendeeId: 'att-1',
|
|
32
|
+
emailName: 'welcome',
|
|
33
|
+
urlLink: '/home',
|
|
34
|
+
trackingEnabled: false
|
|
35
|
+
});
|
|
36
|
+
expect(link).toBe('https://example.com/home');
|
|
37
|
+
});
|
|
38
|
+
it('does not prefix an external url', () => {
|
|
39
|
+
const link = makeTrackingLink(options, {
|
|
40
|
+
attendeeId: 'att-1',
|
|
41
|
+
emailName: 'welcome',
|
|
42
|
+
urlLink: 'https://google.com',
|
|
43
|
+
trackingEnabled: false
|
|
44
|
+
});
|
|
45
|
+
expect(link).toBe('https://google.com/');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('tracking enabled', () => {
|
|
49
|
+
it('returns a /link/{token} url', () => {
|
|
50
|
+
const link = makeTrackingLink(options, {
|
|
51
|
+
attendeeId: 'att-1',
|
|
52
|
+
emailName: 'welcome',
|
|
53
|
+
urlLink: '/home',
|
|
54
|
+
trackingEnabled: true
|
|
55
|
+
});
|
|
56
|
+
expect(link.startsWith('https://example.com/link/')).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
it('includes the instance name in the token', () => {
|
|
59
|
+
const link = makeTrackingLink(options, {
|
|
60
|
+
attendeeId: 'att-1',
|
|
61
|
+
emailName: 'welcome',
|
|
62
|
+
urlLink: '/home',
|
|
63
|
+
trackingEnabled: true
|
|
64
|
+
});
|
|
65
|
+
expect(decode(link).instance_name).toBe('my-instance');
|
|
66
|
+
});
|
|
67
|
+
it('includes the attendee id in the token', () => {
|
|
68
|
+
const link = makeTrackingLink(options, {
|
|
69
|
+
attendeeId: 'att-1',
|
|
70
|
+
emailName: 'welcome',
|
|
71
|
+
urlLink: '/home',
|
|
72
|
+
trackingEnabled: true
|
|
73
|
+
});
|
|
74
|
+
expect(decode(link).attendee_id).toBe('att-1');
|
|
75
|
+
});
|
|
76
|
+
it('includes the email name in the token', () => {
|
|
77
|
+
const link = makeTrackingLink(options, {
|
|
78
|
+
attendeeId: 'att-1',
|
|
79
|
+
emailName: 'welcome',
|
|
80
|
+
urlLink: '/home',
|
|
81
|
+
trackingEnabled: true
|
|
82
|
+
});
|
|
83
|
+
expect(decode(link).email_name).toBe('welcome');
|
|
84
|
+
});
|
|
85
|
+
it('resolves a relative url into an absolute target in the token', () => {
|
|
86
|
+
const link = makeTrackingLink(options, {
|
|
87
|
+
attendeeId: 'att-1',
|
|
88
|
+
emailName: 'welcome',
|
|
89
|
+
urlLink: '/home',
|
|
90
|
+
trackingEnabled: true
|
|
91
|
+
});
|
|
92
|
+
expect(decode(link).target).toBe('https://example.com/home');
|
|
93
|
+
});
|
|
94
|
+
it('does not prefix an external url in the token target either', () => {
|
|
95
|
+
const link = makeTrackingLink(options, {
|
|
96
|
+
attendeeId: 'att-1',
|
|
97
|
+
emailName: 'welcome',
|
|
98
|
+
urlLink: 'https://google.com',
|
|
99
|
+
trackingEnabled: true
|
|
100
|
+
});
|
|
101
|
+
expect(decode(link).target).toBe('https://google.com/');
|
|
102
|
+
});
|
|
103
|
+
it('defaults the mailing id to null when not provided', () => {
|
|
104
|
+
const link = makeTrackingLink(options, {
|
|
105
|
+
attendeeId: 'att-1',
|
|
106
|
+
emailName: 'welcome',
|
|
107
|
+
urlLink: '/home',
|
|
108
|
+
trackingEnabled: true
|
|
109
|
+
});
|
|
110
|
+
expect(decode(link).mailing_id).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
it('includes the mailing id in the token when provided', () => {
|
|
113
|
+
const link = makeTrackingLink(options, {
|
|
114
|
+
attendeeId: 'att-1',
|
|
115
|
+
emailName: 'welcome',
|
|
116
|
+
urlLink: '/home',
|
|
117
|
+
mailingId: 'mailing-1',
|
|
118
|
+
trackingEnabled: true
|
|
119
|
+
});
|
|
120
|
+
expect(decode(link).mailing_id).toBe('mailing-1');
|
|
121
|
+
});
|
|
122
|
+
it('has no exp claim (permanent link)', () => {
|
|
123
|
+
const link = makeTrackingLink(options, {
|
|
124
|
+
attendeeId: 'att-1',
|
|
125
|
+
emailName: 'welcome',
|
|
126
|
+
urlLink: '/home',
|
|
127
|
+
trackingEnabled: true
|
|
128
|
+
});
|
|
129
|
+
expect(decode(link).exp).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe('makeUnsubscribeLink', () => {
|
|
134
|
+
it('builds a link under /unsubscribe with a query-string token', () => {
|
|
135
|
+
const link = makeUnsubscribeLink(options, {
|
|
136
|
+
attendeeId: 'att-1',
|
|
137
|
+
emailName: 'welcome',
|
|
138
|
+
emailAddress: 'a@b.com'
|
|
139
|
+
});
|
|
140
|
+
expect(link.startsWith('https://example.com/unsubscribe?token=')).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
it('includes the instance name in the token', () => {
|
|
143
|
+
const link = makeUnsubscribeLink(options, {
|
|
144
|
+
attendeeId: 'att-1',
|
|
145
|
+
emailName: 'welcome',
|
|
146
|
+
emailAddress: 'a@b.com'
|
|
147
|
+
});
|
|
148
|
+
expect(decodeQueryToken(link).instance_name).toBe('my-instance');
|
|
149
|
+
});
|
|
150
|
+
it('includes the attendee id in the token', () => {
|
|
151
|
+
const link = makeUnsubscribeLink(options, {
|
|
152
|
+
attendeeId: 'att-1',
|
|
153
|
+
emailName: 'welcome',
|
|
154
|
+
emailAddress: 'a@b.com'
|
|
155
|
+
});
|
|
156
|
+
expect(decodeQueryToken(link).attendee_id).toBe('att-1');
|
|
157
|
+
});
|
|
158
|
+
it('includes the email address in the token', () => {
|
|
159
|
+
const link = makeUnsubscribeLink(options, {
|
|
160
|
+
attendeeId: 'att-1',
|
|
161
|
+
emailName: 'welcome',
|
|
162
|
+
emailAddress: 'a@b.com'
|
|
163
|
+
});
|
|
164
|
+
expect(decodeQueryToken(link).email_address).toBe('a@b.com');
|
|
165
|
+
});
|
|
166
|
+
it('includes the email name in the token', () => {
|
|
167
|
+
const link = makeUnsubscribeLink(options, {
|
|
168
|
+
attendeeId: 'att-1',
|
|
169
|
+
emailName: 'welcome',
|
|
170
|
+
emailAddress: 'a@b.com'
|
|
171
|
+
});
|
|
172
|
+
expect(decodeQueryToken(link).email_name).toBe('welcome');
|
|
173
|
+
});
|
|
174
|
+
it('has no exp claim -- an unsubscribe link never expires', () => {
|
|
175
|
+
const link = makeUnsubscribeLink(options, {
|
|
176
|
+
attendeeId: 'att-1',
|
|
177
|
+
emailName: 'welcome',
|
|
178
|
+
emailAddress: 'a@b.com'
|
|
179
|
+
});
|
|
180
|
+
expect(decodeQueryToken(link).exp).toBeUndefined();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
describe('makeRegistrationLink', () => {
|
|
184
|
+
it('builds a link under /register/{token}', () => {
|
|
185
|
+
const link = makeRegistrationLink(options, {
|
|
186
|
+
attendeeId: 'att-1'
|
|
187
|
+
});
|
|
188
|
+
expect(link.startsWith('https://example.com/register/')).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
it('includes the attendee id in the token', () => {
|
|
191
|
+
const link = makeRegistrationLink(options, {
|
|
192
|
+
attendeeId: 'att-1'
|
|
193
|
+
});
|
|
194
|
+
expect(decode(link).attendee_id).toBe('att-1');
|
|
195
|
+
});
|
|
196
|
+
it('includes the instance name in the token', () => {
|
|
197
|
+
const link = makeRegistrationLink(options, {
|
|
198
|
+
attendeeId: 'att-1'
|
|
199
|
+
});
|
|
200
|
+
expect(decode(link).instance_name).toBe('my-instance');
|
|
201
|
+
});
|
|
202
|
+
it('defaults session_type to "register"', () => {
|
|
203
|
+
const link = makeRegistrationLink(options, {
|
|
204
|
+
attendeeId: 'att-1'
|
|
205
|
+
});
|
|
206
|
+
expect(decode(link).session_type).toBe('register');
|
|
207
|
+
});
|
|
208
|
+
it('uses a custom session_type when provided', () => {
|
|
209
|
+
const link = makeRegistrationLink(options, {
|
|
210
|
+
attendeeId: 'att-1',
|
|
211
|
+
sessionType: 'edit'
|
|
212
|
+
});
|
|
213
|
+
expect(decode(link).session_type).toBe('edit');
|
|
214
|
+
});
|
|
215
|
+
it('defaults flags to an empty object', () => {
|
|
216
|
+
const link = makeRegistrationLink(options, {
|
|
217
|
+
attendeeId: 'att-1'
|
|
218
|
+
});
|
|
219
|
+
expect(decode(link).flags).toEqual({});
|
|
220
|
+
});
|
|
221
|
+
it('passes custom flags through', () => {
|
|
222
|
+
const link = makeRegistrationLink(options, {
|
|
223
|
+
attendeeId: 'att-1',
|
|
224
|
+
flags: {
|
|
225
|
+
skipPayment: true
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
expect(decode(link).flags).toEqual({
|
|
229
|
+
skipPayment: true
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
it('omits the email claim entirely when not provided', () => {
|
|
233
|
+
const link = makeRegistrationLink(options, {
|
|
234
|
+
attendeeId: 'att-1'
|
|
235
|
+
});
|
|
236
|
+
expect(decode(link).email).toBeUndefined();
|
|
237
|
+
});
|
|
238
|
+
it('includes the email name/mailing id when provided', () => {
|
|
239
|
+
const link = makeRegistrationLink(options, {
|
|
240
|
+
attendeeId: 'att-1',
|
|
241
|
+
email: {
|
|
242
|
+
name: 'welcome',
|
|
243
|
+
mailingId: 'mailing-1'
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
expect(decode(link).email).toEqual({
|
|
247
|
+
name: 'welcome',
|
|
248
|
+
mailing_id: 'mailing-1'
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
it('defaults to a 15 day expiry', () => {
|
|
252
|
+
const before = Math.floor(Date.now() / 1000);
|
|
253
|
+
const link = makeRegistrationLink(options, {
|
|
254
|
+
attendeeId: 'att-1'
|
|
255
|
+
});
|
|
256
|
+
const fifteenDays = 15 * 24 * 60 * 60;
|
|
257
|
+
expect(decode(link).exp).toBeGreaterThanOrEqual(before + fifteenDays);
|
|
258
|
+
expect(decode(link).exp).toBeLessThanOrEqual(before + fifteenDays + 1);
|
|
259
|
+
});
|
|
260
|
+
it('uses a custom expiry when provided', () => {
|
|
261
|
+
const before = Math.floor(Date.now() / 1000);
|
|
262
|
+
const link = makeRegistrationLink(options, {
|
|
263
|
+
attendeeId: 'att-1',
|
|
264
|
+
expiresInSeconds: 3600
|
|
265
|
+
});
|
|
266
|
+
expect(decode(link).exp).toBeGreaterThanOrEqual(before + 3600);
|
|
267
|
+
expect(decode(link).exp).toBeLessThanOrEqual(before + 3601);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
describe('makeWalletLink', () => {
|
|
271
|
+
it('builds an unsigned /wallet/{attendeeId} link', () => {
|
|
272
|
+
expect(makeWalletLink(options, 'att-1')).toBe('https://example.com/wallet/att-1');
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
describe('makeGoogleWalletLink', () => {
|
|
276
|
+
it('builds an unsigned /google-wallet/{attendeeId} link', () => {
|
|
277
|
+
expect(makeGoogleWalletLink(options, 'att-1')).toBe('https://example.com/google-wallet/att-1');
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
describe('makeQrCodeLink', () => {
|
|
281
|
+
it('builds a link under /qr/', () => {
|
|
282
|
+
const link = makeQrCodeLink(options, {
|
|
283
|
+
content: 'WAR-140'
|
|
284
|
+
});
|
|
285
|
+
expect(link.startsWith('https://example.com/qr/?')).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
it('encodes the given content', () => {
|
|
288
|
+
const url = new URL(makeQrCodeLink(options, {
|
|
289
|
+
content: 'WAR-140'
|
|
290
|
+
}));
|
|
291
|
+
expect(url.searchParams.get('content')).toBe('WAR-140');
|
|
292
|
+
});
|
|
293
|
+
it('defaults size to 600', () => {
|
|
294
|
+
const url = new URL(makeQrCodeLink(options, {
|
|
295
|
+
content: 'WAR-140'
|
|
296
|
+
}));
|
|
297
|
+
expect(url.searchParams.get('size')).toBe('600');
|
|
298
|
+
});
|
|
299
|
+
it('defaults background to white', () => {
|
|
300
|
+
const url = new URL(makeQrCodeLink(options, {
|
|
301
|
+
content: 'WAR-140'
|
|
302
|
+
}));
|
|
303
|
+
expect(url.searchParams.get('background')).toBe('white');
|
|
304
|
+
});
|
|
305
|
+
it('defaults body to black', () => {
|
|
306
|
+
const url = new URL(makeQrCodeLink(options, {
|
|
307
|
+
content: 'WAR-140'
|
|
308
|
+
}));
|
|
309
|
+
expect(url.searchParams.get('body')).toBe('black');
|
|
310
|
+
});
|
|
311
|
+
it('defaults corners to black', () => {
|
|
312
|
+
const url = new URL(makeQrCodeLink(options, {
|
|
313
|
+
content: 'WAR-140'
|
|
314
|
+
}));
|
|
315
|
+
expect(url.searchParams.get('corners')).toBe('black');
|
|
316
|
+
});
|
|
317
|
+
it('defaults fancy to 0', () => {
|
|
318
|
+
const url = new URL(makeQrCodeLink(options, {
|
|
319
|
+
content: 'WAR-140'
|
|
320
|
+
}));
|
|
321
|
+
expect(url.searchParams.get('fancy')).toBe('0');
|
|
322
|
+
});
|
|
323
|
+
it('applies a custom size', () => {
|
|
324
|
+
const url = new URL(makeQrCodeLink(options, {
|
|
325
|
+
content: 'WAR-140',
|
|
326
|
+
size: 300
|
|
327
|
+
}));
|
|
328
|
+
expect(url.searchParams.get('size')).toBe('300');
|
|
329
|
+
});
|
|
330
|
+
it('applies custom background/body/corners colors', () => {
|
|
331
|
+
const url = new URL(makeQrCodeLink(options, {
|
|
332
|
+
content: 'WAR-140',
|
|
333
|
+
background: 'red',
|
|
334
|
+
body: 'blue',
|
|
335
|
+
corners: 'green'
|
|
336
|
+
}));
|
|
337
|
+
expect(url.searchParams.get('background')).toBe('red');
|
|
338
|
+
expect(url.searchParams.get('body')).toBe('blue');
|
|
339
|
+
expect(url.searchParams.get('corners')).toBe('green');
|
|
340
|
+
});
|
|
341
|
+
it('applies a custom fancy flag', () => {
|
|
342
|
+
const url = new URL(makeQrCodeLink(options, {
|
|
343
|
+
content: 'WAR-140',
|
|
344
|
+
fancy: 1
|
|
345
|
+
}));
|
|
346
|
+
expect(url.searchParams.get('fancy')).toBe('1');
|
|
347
|
+
});
|
|
348
|
+
});
|
package/dist/lib/index.d.ts
CHANGED
package/dist/lib/index.js
CHANGED
package/dist/lib/tokens.d.ts
CHANGED
|
@@ -27,6 +27,12 @@ export type TrackingToken = {
|
|
|
27
27
|
target: string;
|
|
28
28
|
mailingId: string | null;
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Sign a payload as an HS256 JWS, mirroring
|
|
32
|
+
* FrontendConfig.encode_jws on the backend -- same shared secret, same
|
|
33
|
+
* compact-JWS format, so tokens minted here verify there and vice versa.
|
|
34
|
+
*/
|
|
35
|
+
export declare function encodeJWS(payload: Record<string, unknown>, secret: string, expiresInSeconds?: number): string;
|
|
30
36
|
export declare function parseRegistrationToken(tokenString: string | undefined, options: TokenOptions): Result<TokenValidationError, RegistrationToken>;
|
|
31
37
|
export declare function parseTrackingToken(tokenString: string | undefined, options: TokenOptions): Result<TokenValidationError, TrackingToken>;
|
|
32
38
|
export declare function parseUnsubscribeToken(tokenString: string | undefined, options: TokenOptions): Result<TokenValidationError, {
|
|
@@ -39,6 +45,20 @@ export declare function parseEmailHtmlToken(tokenString: string | undefined, opt
|
|
|
39
45
|
emailName: string;
|
|
40
46
|
mailingId?: string;
|
|
41
47
|
}>;
|
|
48
|
+
export type EmailViewToken = {
|
|
49
|
+
attendeeId: string;
|
|
50
|
+
emailName: string;
|
|
51
|
+
mailingId?: string;
|
|
52
|
+
purpose: 'view' | 'render';
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Parses a /email/{mail_name}/{token} token. `purpose` -- not the
|
|
56
|
+
* request's Accept header -- is what a route should trust to decide
|
|
57
|
+
* whether to return a human-facing HTML page ("view") or the full
|
|
58
|
+
* structured render payload ("render", minted short-lived and used
|
|
59
|
+
* server-to-server only). See FrontendConfig.make_email_view_link.
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseEmailViewToken(tokenString: string | undefined, options: TokenOptions): Result<TokenValidationError, EmailViewToken>;
|
|
42
62
|
export declare function readRegistrationToken(getCookie: CookieGetter, options: TokenOptions & RegistrationTokenOptions): Result<TokenValidationError, RegistrationToken>;
|
|
43
63
|
export declare function storeRegistrationToken(token: string, setCookie: CookieSetter, options?: RegistrationTokenOptions): void;
|
|
44
64
|
export declare function deleteRegistrationToken(deleteCookie: CookieRemover, options?: RegistrationTokenOptions): void;
|
package/dist/lib/tokens.js
CHANGED
|
@@ -10,6 +10,29 @@ function parseJWS(tokenString, options) {
|
|
|
10
10
|
if (!signatureIsValid) return failure('token_invalid');
|
|
11
11
|
return success(jws.decode(tokenString));
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Sign a payload as an HS256 JWS, mirroring
|
|
16
|
+
* FrontendConfig.encode_jws on the backend -- same shared secret, same
|
|
17
|
+
* compact-JWS format, so tokens minted here verify there and vice versa.
|
|
18
|
+
*/
|
|
19
|
+
export function encodeJWS(payload, secret, expiresInSeconds) {
|
|
20
|
+
const finalPayload = expiresInSeconds ? {
|
|
21
|
+
...payload,
|
|
22
|
+
exp: Math.floor(Date.now() / 1000) + expiresInSeconds
|
|
23
|
+
} : payload;
|
|
24
|
+
// typ: 'JWT' matters beyond convention here -- jws.decode() only parses
|
|
25
|
+
// the payload as JSON (rather than leaving it a raw string) when this is
|
|
26
|
+
// set, and the backend's authlib-signed tokens always set it too.
|
|
27
|
+
return jws.sign({
|
|
28
|
+
header: {
|
|
29
|
+
alg: 'HS256',
|
|
30
|
+
typ: 'JWT'
|
|
31
|
+
},
|
|
32
|
+
payload: finalPayload,
|
|
33
|
+
secret
|
|
34
|
+
});
|
|
35
|
+
}
|
|
13
36
|
export function parseRegistrationToken(tokenString, options) {
|
|
14
37
|
const tokenResult = parseJWS(tokenString, options);
|
|
15
38
|
if (!tokenResult.success) return tokenResult;
|
|
@@ -75,6 +98,31 @@ export function parseEmailHtmlToken(tokenString, options) {
|
|
|
75
98
|
mailingId: payload.mailing_id ?? undefined
|
|
76
99
|
});
|
|
77
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Parses a /email/{mail_name}/{token} token. `purpose` -- not the
|
|
103
|
+
* request's Accept header -- is what a route should trust to decide
|
|
104
|
+
* whether to return a human-facing HTML page ("view") or the full
|
|
105
|
+
* structured render payload ("render", minted short-lived and used
|
|
106
|
+
* server-to-server only). See FrontendConfig.make_email_view_link.
|
|
107
|
+
*/
|
|
108
|
+
export function parseEmailViewToken(tokenString, options) {
|
|
109
|
+
const tokenResult = parseJWS(tokenString, options);
|
|
110
|
+
if (!tokenResult.success) return tokenResult;
|
|
111
|
+
const payload = tokenResult.data.payload;
|
|
112
|
+
if (payload.instance_name !== options.instanceName || typeof payload.attendee_id !== 'string' || typeof payload.email_name !== 'string' || payload.mailing_id != undefined && typeof payload.mailing_id !== 'string' || payload.purpose !== 'view' && payload.purpose !== 'render' || typeof payload.exp !== 'number') {
|
|
113
|
+
return failure('token_invalid');
|
|
114
|
+
}
|
|
115
|
+
const now = new Date();
|
|
116
|
+
const expires = new Date(payload.exp * 1000);
|
|
117
|
+
const isExpired = now >= expires;
|
|
118
|
+
if (isExpired) return failure('token_expired');
|
|
119
|
+
return success({
|
|
120
|
+
attendeeId: payload.attendee_id,
|
|
121
|
+
emailName: payload.email_name,
|
|
122
|
+
mailingId: payload.mailing_id ?? undefined,
|
|
123
|
+
purpose: payload.purpose
|
|
124
|
+
});
|
|
125
|
+
}
|
|
78
126
|
export function readRegistrationToken(getCookie, options) {
|
|
79
127
|
const {
|
|
80
128
|
registrationTokenCookieName = REGISTRATION_TOKEN_COOKIE_NAME
|