ghost 6.0.5 → 6.0.7
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/components/tryghost-i18n-6.0.7.tgz +0 -0
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +1 -1
- package/core/built/admin/assets/admin-x-activitypub/{index-DBxGycCG.mjs → index-B-ckGCDl.mjs} +20083 -16775
- package/core/built/admin/assets/admin-x-activitypub/{index-Db9aLAi4.mjs → index-C81KQoIh.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-B4W7CQcA.mjs → CodeEditorView-BfL5FINN.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
- package/core/built/admin/assets/admin-x-settings/{index-jv9DN3ZO.mjs → index-C_ZS-INP.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-CuwMM9FM.mjs → index-DiNZ3HQD.mjs} +12 -4
- package/core/built/admin/assets/admin-x-settings/{modals-CUGEPPYA.mjs → modals-BEiWgHsk.mjs} +8807 -8799
- package/core/built/admin/assets/{chunk.524.0e2bff9b664f925d7af7.js → chunk.524.9f989b3664418d6271a7.js} +6 -6
- package/core/built/admin/assets/{chunk.582.a8f6c436bbec6f9ba678.js → chunk.582.2421014e45b977b43b68.js} +8 -8
- package/core/built/admin/assets/{ghost-2b02de85a93ec9a5623180b373cece35.js → ghost-182ed60de3f37fff8a40cdd65d3bd2ef.js} +55 -50
- package/core/built/admin/assets/{ghost-2c537ee89c36199137eafc1768fd7de8.css → ghost-a7a53bf80dc45c37ae9c174a0d02a882.css} +1 -1
- package/core/built/admin/assets/{ghost-dark-ad23efc1d702e3643a8ee90d089df5d6.css → ghost-dark-6e0062029f988d8676e87f22d8e7f4a3.css} +1 -1
- package/core/built/admin/assets/posts/posts.js +78821 -77904
- package/core/built/admin/assets/stats/stats.js +21689 -21645
- package/core/built/admin/index.html +4 -4
- package/core/server/data/tinybird/datasources/analytics_events.datasource +3 -2
- package/core/server/data/tinybird/datasources/analytics_events_test.datasource +3 -2
- package/core/server/data/tinybird/endpoints/api_top_utm_campaigns.pipe +31 -0
- package/core/server/data/tinybird/endpoints/api_top_utm_contents.pipe +31 -0
- package/core/server/data/tinybird/endpoints/api_top_utm_mediums.pipe +31 -0
- package/core/server/data/tinybird/endpoints/api_top_utm_sources.pipe +31 -0
- package/core/server/data/tinybird/endpoints/api_top_utm_terms.pipe +31 -0
- package/core/server/data/tinybird/scripts/configure-ghost.sh +4 -2
- package/core/server/data/tinybird/tests/api_top_utm_campaigns.yaml +108 -0
- package/core/server/data/tinybird/tests/api_top_utm_contents.yaml +108 -0
- package/core/server/data/tinybird/tests/api_top_utm_mediums.yaml +108 -0
- package/core/server/data/tinybird/tests/api_top_utm_sources.yaml +108 -0
- package/core/server/data/tinybird/tests/api_top_utm_terms.yaml +108 -0
- package/core/server/services/lib/magic-link/MagicLink.js +98 -12
- package/core/server/services/members/MembersConfigProvider.js +11 -1
- package/core/server/services/members/SingleUseTokenProvider.js +218 -5
- package/core/server/services/members/api.js +18 -8
- package/core/server/services/members/emails/signin.js +42 -5
- package/core/server/services/members/members-api/controllers/RouterController.js +105 -18
- package/core/server/services/members/members-api/members-api.js +12 -7
- package/core/server/services/members/members-ssr.js +5 -3
- package/core/server/services/members/middleware.js +2 -2
- package/core/server/services/stats/PostsStatsService.js +26 -9
- package/core/server/services/stats/StatsService.js +1 -1
- package/core/server/web/members/app.js +12 -3
- package/core/shared/config/defaults.json +1 -1
- package/core/shared/labs.js +3 -1
- package/package.json +9 -9
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +213 -323
- package/components/tryghost-i18n-6.0.5.tgz +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
|
|
2
|
+
- name: Date range
|
|
3
|
+
description: All fixture data
|
|
4
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC
|
|
5
|
+
expected_result: |
|
|
6
|
+
{"utm_source":"google","visits":6}
|
|
7
|
+
{"utm_source":"linkedin","visits":3}
|
|
8
|
+
{"utm_source":"twitter","visits":3}
|
|
9
|
+
{"utm_source":"newsletter","visits":3}
|
|
10
|
+
{"utm_source":"instagram","visits":1}
|
|
11
|
+
|
|
12
|
+
- name: Filtered by browser - Chrome
|
|
13
|
+
description: Filtered by browser - Chrome
|
|
14
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&browser=chrome
|
|
15
|
+
expected_result: |
|
|
16
|
+
{"utm_source":"google","visits":4}
|
|
17
|
+
{"utm_source":"linkedin","visits":1}
|
|
18
|
+
{"utm_source":"twitter","visits":1}
|
|
19
|
+
{"utm_source":"newsletter","visits":1}
|
|
20
|
+
|
|
21
|
+
- name: Filtered by device - desktop
|
|
22
|
+
description: Filtered by device - desktop
|
|
23
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&device=desktop
|
|
24
|
+
expected_result: |
|
|
25
|
+
{"utm_source":"google","visits":6}
|
|
26
|
+
{"utm_source":"twitter","visits":3}
|
|
27
|
+
{"utm_source":"newsletter","visits":3}
|
|
28
|
+
{"utm_source":"linkedin","visits":2}
|
|
29
|
+
{"utm_source":"instagram","visits":1}
|
|
30
|
+
|
|
31
|
+
- name: Filtered by location - UK
|
|
32
|
+
description: Filtered by location - UK
|
|
33
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&location=GB
|
|
34
|
+
expected_result: |
|
|
35
|
+
{"utm_source":"google","visits":3}
|
|
36
|
+
{"utm_source":"linkedin","visits":2}
|
|
37
|
+
{"utm_source":"newsletter","visits":2}
|
|
38
|
+
{"utm_source":"twitter","visits":1}
|
|
39
|
+
|
|
40
|
+
- name: Filtered by OS - Windows
|
|
41
|
+
description: Filtered by OS - Windows
|
|
42
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&os=windows
|
|
43
|
+
expected_result: |
|
|
44
|
+
{"utm_source":"google","visits":5}
|
|
45
|
+
{"utm_source":"twitter","visits":3}
|
|
46
|
+
{"utm_source":"newsletter","visits":3}
|
|
47
|
+
{"utm_source":"linkedin","visits":2}
|
|
48
|
+
{"utm_source":"instagram","visits":1}
|
|
49
|
+
|
|
50
|
+
- name: Filtered by pathname - /about/
|
|
51
|
+
description: Filtered by pathname - /about/
|
|
52
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&pathname=%2Fabout%2F
|
|
53
|
+
expected_result: |
|
|
54
|
+
{"utm_source":"google","visits":4}
|
|
55
|
+
{"utm_source":"twitter","visits":2}
|
|
56
|
+
{"utm_source":"linkedin","visits":1}
|
|
57
|
+
{"utm_source":"newsletter","visits":1}
|
|
58
|
+
|
|
59
|
+
- name: Filtered by post_uuid - 06b1b0c9-fb53-4a15-a060-3db3fde7b1fc (/about/)
|
|
60
|
+
description: Filtered by post_uuid - 06b1b0c9-fb53-4a15-a060-3db3fde7b1fc (/about/)
|
|
61
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&post_uuid=06b1b0c9-fb53-4a15-a060-3db3fde7b1fc
|
|
62
|
+
expected_result: |
|
|
63
|
+
{"utm_source":"google","visits":4}
|
|
64
|
+
{"utm_source":"twitter","visits":2}
|
|
65
|
+
{"utm_source":"linkedin","visits":1}
|
|
66
|
+
{"utm_source":"newsletter","visits":1}
|
|
67
|
+
|
|
68
|
+
- name: Filtered by source - bing.com
|
|
69
|
+
description: Filtered by source - bing.com
|
|
70
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&source=bing.com
|
|
71
|
+
expected_result: |
|
|
72
|
+
{"utm_source":"twitter","visits":2}
|
|
73
|
+
|
|
74
|
+
- name: Filtered by member status - paid
|
|
75
|
+
description: Filtered by member status - paid
|
|
76
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&member_status=paid
|
|
77
|
+
expected_result: |
|
|
78
|
+
{"utm_source":"google","visits":2}
|
|
79
|
+
{"utm_source":"newsletter","visits":2}
|
|
80
|
+
{"utm_source":"twitter","visits":1}
|
|
81
|
+
|
|
82
|
+
- name: Filtered by member status - undefined
|
|
83
|
+
description: Filtered by member status - undefined
|
|
84
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&member_status=undefined
|
|
85
|
+
expected_result: |
|
|
86
|
+
{"utm_source":"linkedin","visits":3}
|
|
87
|
+
{"utm_source":"google","visits":1}
|
|
88
|
+
{"utm_source":"instagram","visits":1}
|
|
89
|
+
{"utm_source":"newsletter","visits":1}
|
|
90
|
+
|
|
91
|
+
- name: Filtered by timezone - America/Los_Angeles
|
|
92
|
+
description: Filtered by timezone - America/Los_Angeles
|
|
93
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=America/Los_Angeles
|
|
94
|
+
expected_result: |
|
|
95
|
+
{"utm_source":"google","visits":5}
|
|
96
|
+
{"utm_source":"newsletter","visits":3}
|
|
97
|
+
{"utm_source":"linkedin","visits":2}
|
|
98
|
+
{"utm_source":"twitter","visits":2}
|
|
99
|
+
{"utm_source":"instagram","visits":1}
|
|
100
|
+
|
|
101
|
+
- name: Test with multiple filters combined
|
|
102
|
+
description: Test with multiple filters combined
|
|
103
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&device=desktop&browser=firefox
|
|
104
|
+
expected_result: |
|
|
105
|
+
{"utm_source":"newsletter","visits":2}
|
|
106
|
+
{"utm_source":"linkedin","visits":1}
|
|
107
|
+
{"utm_source":"twitter","visits":1}
|
|
108
|
+
{"utm_source":"instagram","visits":1}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
|
|
2
|
+
- name: Date range
|
|
3
|
+
description: All fixture data
|
|
4
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC
|
|
5
|
+
expected_result: |
|
|
6
|
+
{"utm_term":"ghost cms","visits":6}
|
|
7
|
+
{"utm_term":"blog software","visits":3}
|
|
8
|
+
{"utm_term":"content management","visits":3}
|
|
9
|
+
{"utm_term":"newsletter platform","visits":3}
|
|
10
|
+
{"utm_term":"membership site","visits":1}
|
|
11
|
+
|
|
12
|
+
- name: Filtered by browser - Chrome
|
|
13
|
+
description: Filtered by browser - Chrome
|
|
14
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&browser=chrome
|
|
15
|
+
expected_result: |
|
|
16
|
+
{"utm_term":"ghost cms","visits":4}
|
|
17
|
+
{"utm_term":"blog software","visits":1}
|
|
18
|
+
{"utm_term":"content management","visits":1}
|
|
19
|
+
{"utm_term":"newsletter platform","visits":1}
|
|
20
|
+
|
|
21
|
+
- name: Filtered by device - desktop
|
|
22
|
+
description: Filtered by device - desktop
|
|
23
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&device=desktop
|
|
24
|
+
expected_result: |
|
|
25
|
+
{"utm_term":"ghost cms","visits":6}
|
|
26
|
+
{"utm_term":"blog software","visits":3}
|
|
27
|
+
{"utm_term":"content management","visits":3}
|
|
28
|
+
{"utm_term":"newsletter platform","visits":2}
|
|
29
|
+
{"utm_term":"membership site","visits":1}
|
|
30
|
+
|
|
31
|
+
- name: Filtered by location - UK
|
|
32
|
+
description: Filtered by location - UK
|
|
33
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&location=GB
|
|
34
|
+
expected_result: |
|
|
35
|
+
{"utm_term":"ghost cms","visits":3}
|
|
36
|
+
{"utm_term":"blog software","visits":2}
|
|
37
|
+
{"utm_term":"newsletter platform","visits":2}
|
|
38
|
+
{"utm_term":"content management","visits":1}
|
|
39
|
+
|
|
40
|
+
- name: Filtered by OS - Windows
|
|
41
|
+
description: Filtered by OS - Windows
|
|
42
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&os=windows
|
|
43
|
+
expected_result: |
|
|
44
|
+
{"utm_term":"ghost cms","visits":5}
|
|
45
|
+
{"utm_term":"blog software","visits":3}
|
|
46
|
+
{"utm_term":"content management","visits":3}
|
|
47
|
+
{"utm_term":"newsletter platform","visits":2}
|
|
48
|
+
{"utm_term":"membership site","visits":1}
|
|
49
|
+
|
|
50
|
+
- name: Filtered by pathname - /about/
|
|
51
|
+
description: Filtered by pathname - /about/
|
|
52
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&pathname=%2Fabout%2F
|
|
53
|
+
expected_result: |
|
|
54
|
+
{"utm_term":"ghost cms","visits":4}
|
|
55
|
+
{"utm_term":"content management","visits":2}
|
|
56
|
+
{"utm_term":"blog software","visits":1}
|
|
57
|
+
{"utm_term":"newsletter platform","visits":1}
|
|
58
|
+
|
|
59
|
+
- name: Filtered by post_uuid - 06b1b0c9-fb53-4a15-a060-3db3fde7b1fc (/about/)
|
|
60
|
+
description: Filtered by post_uuid - 06b1b0c9-fb53-4a15-a060-3db3fde7b1fc (/about/)
|
|
61
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&post_uuid=06b1b0c9-fb53-4a15-a060-3db3fde7b1fc
|
|
62
|
+
expected_result: |
|
|
63
|
+
{"utm_term":"ghost cms","visits":4}
|
|
64
|
+
{"utm_term":"content management","visits":2}
|
|
65
|
+
{"utm_term":"blog software","visits":1}
|
|
66
|
+
{"utm_term":"newsletter platform","visits":1}
|
|
67
|
+
|
|
68
|
+
- name: Filtered by source - bing.com
|
|
69
|
+
description: Filtered by source - bing.com
|
|
70
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&source=bing.com
|
|
71
|
+
expected_result: |
|
|
72
|
+
{"utm_term":"content management","visits":2}
|
|
73
|
+
|
|
74
|
+
- name: Filtered by member status - paid
|
|
75
|
+
description: Filtered by member status - paid
|
|
76
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&member_status=paid
|
|
77
|
+
expected_result: |
|
|
78
|
+
{"utm_term":"blog software","visits":2}
|
|
79
|
+
{"utm_term":"ghost cms","visits":2}
|
|
80
|
+
{"utm_term":"content management","visits":1}
|
|
81
|
+
|
|
82
|
+
- name: Filtered by member status - undefined
|
|
83
|
+
description: Filtered by member status - undefined
|
|
84
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&member_status=undefined
|
|
85
|
+
expected_result: |
|
|
86
|
+
{"utm_term":"newsletter platform","visits":3}
|
|
87
|
+
{"utm_term":"blog software","visits":1}
|
|
88
|
+
{"utm_term":"membership site","visits":1}
|
|
89
|
+
{"utm_term":"ghost cms","visits":1}
|
|
90
|
+
|
|
91
|
+
- name: Filtered by timezone - America/Los_Angeles
|
|
92
|
+
description: Filtered by timezone - America/Los_Angeles
|
|
93
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=America/Los_Angeles
|
|
94
|
+
expected_result: |
|
|
95
|
+
{"utm_term":"ghost cms","visits":5}
|
|
96
|
+
{"utm_term":"blog software","visits":3}
|
|
97
|
+
{"utm_term":"content management","visits":2}
|
|
98
|
+
{"utm_term":"newsletter platform","visits":2}
|
|
99
|
+
{"utm_term":"membership site","visits":1}
|
|
100
|
+
|
|
101
|
+
- name: Test with multiple filters combined
|
|
102
|
+
description: Test with multiple filters combined
|
|
103
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&device=desktop&browser=firefox
|
|
104
|
+
expected_result: |
|
|
105
|
+
{"utm_term":"blog software","visits":2}
|
|
106
|
+
{"utm_term":"membership site","visits":1}
|
|
107
|
+
{"utm_term":"content management","visits":1}
|
|
108
|
+
{"utm_term":"newsletter platform","visits":1}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const {IncorrectUsageError, BadRequestError} = require('@tryghost/errors');
|
|
2
2
|
const {isEmail} = require('@tryghost/validator');
|
|
3
3
|
const tpl = require('@tryghost/tpl');
|
|
4
|
+
|
|
4
5
|
const messages = {
|
|
5
6
|
invalidEmail: 'Email is not valid'
|
|
6
7
|
};
|
|
@@ -11,12 +12,19 @@ const messages = {
|
|
|
11
12
|
* @typedef { string } URL
|
|
12
13
|
*/
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} TokenValidateOptions
|
|
17
|
+
* @prop {string} [otcVerification] - "timestamp:hash" string used to verify an OTC-bound token
|
|
18
|
+
*/
|
|
19
|
+
|
|
14
20
|
/**
|
|
15
21
|
* @template T
|
|
16
22
|
* @template D
|
|
17
23
|
* @typedef {Object} TokenProvider<T, D>
|
|
18
24
|
* @prop {(data: D) => Promise<T>} create
|
|
19
|
-
* @prop {(token: T) => Promise<D>} validate
|
|
25
|
+
* @prop {(token: T, options?: TokenValidateOptions) => Promise<D>} validate
|
|
26
|
+
* @prop {(token: T) => Promise<string | null>} [getIdByToken]
|
|
27
|
+
* @prop {(otcRef: string, tokenValue: T) => string} [deriveOTC]
|
|
20
28
|
*/
|
|
21
29
|
|
|
22
30
|
/**
|
|
@@ -29,11 +37,12 @@ class MagicLink {
|
|
|
29
37
|
* @param {object} options
|
|
30
38
|
* @param {MailTransporter} options.transporter
|
|
31
39
|
* @param {TokenProvider<Token, TokenData>} options.tokenProvider
|
|
32
|
-
* @param {(token: Token, type: string, referrer?: string) => URL} options.getSigninURL
|
|
40
|
+
* @param {(token: Token, type: string, referrer?: string, otcVerification?: string) => URL} options.getSigninURL
|
|
33
41
|
* @param {typeof defaultGetText} [options.getText]
|
|
34
42
|
* @param {typeof defaultGetHTML} [options.getHTML]
|
|
35
43
|
* @param {typeof defaultGetSubject} [options.getSubject]
|
|
36
44
|
* @param {object} [options.sentry]
|
|
45
|
+
* @param {{isSet(name: string): boolean}} [options.labsService]
|
|
37
46
|
*/
|
|
38
47
|
constructor(options) {
|
|
39
48
|
if (!options || !options.transporter || !options.tokenProvider || !options.getSigninURL) {
|
|
@@ -46,6 +55,7 @@ class MagicLink {
|
|
|
46
55
|
this.getHTML = options.getHTML || defaultGetHTML;
|
|
47
56
|
this.getSubject = options.getSubject || defaultGetSubject;
|
|
48
57
|
this.sentry = options.sentry || undefined;
|
|
58
|
+
this.labsService = options.labsService || undefined;
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
/**
|
|
@@ -56,7 +66,8 @@ class MagicLink {
|
|
|
56
66
|
* @param {TokenData} options.tokenData - The data for token
|
|
57
67
|
* @param {string} [options.type='signin'] - The type to be passed to the url and content generator functions
|
|
58
68
|
* @param {string} [options.referrer=null] - The referrer of the request, if exists. The member will be redirected back to this URL after signin.
|
|
59
|
-
* @
|
|
69
|
+
* @param {boolean} [options.includeOTC=false] - Whether to send a one-time-code in the email.
|
|
70
|
+
* @returns {Promise<{token: Token, otcRef: string | null, info: SentMessageInfo}>}
|
|
60
71
|
*/
|
|
61
72
|
async sendMagicLink(options) {
|
|
62
73
|
this.sentry?.captureMessage?.(`[Magic Link] Generating magic link`, {extra: options});
|
|
@@ -73,14 +84,38 @@ class MagicLink {
|
|
|
73
84
|
|
|
74
85
|
const url = this.getSigninURL(token, type, options.referrer);
|
|
75
86
|
|
|
87
|
+
let otc = null;
|
|
88
|
+
if (this.labsService?.isSet('membersSigninOTC') && options.includeOTC) {
|
|
89
|
+
try {
|
|
90
|
+
otc = await this.getOTCFromToken(token);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
this.sentry?.captureException?.(err);
|
|
93
|
+
otc = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
76
97
|
const info = await this.transporter.sendMail({
|
|
77
98
|
to: options.email,
|
|
78
|
-
subject: this.getSubject(type),
|
|
79
|
-
text: this.getText(url, type, options.email),
|
|
80
|
-
html: this.getHTML(url, type, options.email)
|
|
99
|
+
subject: this.getSubject(type, otc),
|
|
100
|
+
text: this.getText(url, type, options.email, otc),
|
|
101
|
+
html: this.getHTML(url, type, options.email, otc)
|
|
81
102
|
});
|
|
82
103
|
|
|
83
|
-
return
|
|
104
|
+
// return otcRef so we can pass it as a reference to the client so it
|
|
105
|
+
// can pass it back as a reference when verifying the OTC. We only do
|
|
106
|
+
// this if we've successfully generated an OTC to avoid clients showing
|
|
107
|
+
// a token input field when the email doesn't contain an OTC
|
|
108
|
+
let otcRef = null;
|
|
109
|
+
if (this.labsService?.isSet('membersSigninOTC') && otc) {
|
|
110
|
+
try {
|
|
111
|
+
otcRef = await this.getIdFromToken(token);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
this.sentry?.captureException?.(err);
|
|
114
|
+
otcRef = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {token, otcRef, info};
|
|
84
119
|
}
|
|
85
120
|
|
|
86
121
|
/**
|
|
@@ -99,14 +134,47 @@ class MagicLink {
|
|
|
99
134
|
return this.getSigninURL(token, type, options.referrer);
|
|
100
135
|
}
|
|
101
136
|
|
|
137
|
+
/**
|
|
138
|
+
* getIdFromToken
|
|
139
|
+
*
|
|
140
|
+
* @param {Token} token - The token to get the id from
|
|
141
|
+
* @returns {Promise<string|null>} id - The id of the token
|
|
142
|
+
*/
|
|
143
|
+
async getIdFromToken(token) {
|
|
144
|
+
if (typeof this.tokenProvider.getIdByToken !== 'function') {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const id = await this.tokenProvider.getIdByToken(token);
|
|
149
|
+
return id;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* getOTCFromToken
|
|
154
|
+
*
|
|
155
|
+
* @param {Token} token - The token to get the otc from
|
|
156
|
+
* @returns {Promise<string|null>} otc - The otc of the token
|
|
157
|
+
*/
|
|
158
|
+
async getOTCFromToken(token) {
|
|
159
|
+
const tokenId = await this.getIdFromToken(token);
|
|
160
|
+
|
|
161
|
+
if (!tokenId || typeof this.tokenProvider.deriveOTC !== 'function') {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const otc = await this.tokenProvider.deriveOTC(tokenId, token);
|
|
166
|
+
return otc;
|
|
167
|
+
}
|
|
168
|
+
|
|
102
169
|
/**
|
|
103
170
|
* getDataFromToken
|
|
104
171
|
*
|
|
105
172
|
* @param {Token} token - The token to decode
|
|
173
|
+
* @param {string} [otcVerification] - Optional "timestamp:hash" to bind token usage to an OTC verification window
|
|
106
174
|
* @returns {Promise<TokenData>} data - The data object associated with the magic link
|
|
107
175
|
*/
|
|
108
|
-
async getDataFromToken(token) {
|
|
109
|
-
const tokenData = await this.tokenProvider.validate(token);
|
|
176
|
+
async getDataFromToken(token, otcVerification) {
|
|
177
|
+
const tokenData = await this.tokenProvider.validate(token, {otcVerification});
|
|
110
178
|
return tokenData;
|
|
111
179
|
}
|
|
112
180
|
}
|
|
@@ -117,13 +185,19 @@ class MagicLink {
|
|
|
117
185
|
* @param {URL} url - The url which will trigger sign in flow
|
|
118
186
|
* @param {string} type - The type of email to send e.g. signin, signup
|
|
119
187
|
* @param {string} email - The recipient of the email to send
|
|
188
|
+
* @param {string} otc - Optional one-time-code
|
|
120
189
|
* @returns {string} text - The text content of an email to send
|
|
121
190
|
*/
|
|
122
|
-
function defaultGetText(url, type, email) {
|
|
191
|
+
function defaultGetText(url, type, email, otc) {
|
|
123
192
|
let msg = 'sign in';
|
|
124
193
|
if (type === 'signup') {
|
|
125
194
|
msg = 'confirm your email address';
|
|
126
195
|
}
|
|
196
|
+
|
|
197
|
+
if (otc) {
|
|
198
|
+
return `Enter the code ${otc} or click here to ${msg} ${url}. This msg was sent to ${email}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
127
201
|
return `Click here to ${msg} ${url}. This msg was sent to ${email}`;
|
|
128
202
|
}
|
|
129
203
|
|
|
@@ -133,13 +207,19 @@ function defaultGetText(url, type, email) {
|
|
|
133
207
|
* @param {URL} url - The url which will trigger sign in flow
|
|
134
208
|
* @param {string} type - The type of email to send e.g. signin, signup
|
|
135
209
|
* @param {string} email - The recipient of the email to send
|
|
210
|
+
* @param {string} otc - Optional one-time-code
|
|
136
211
|
* @returns {string} HTML - The HTML content of an email to send
|
|
137
212
|
*/
|
|
138
|
-
function defaultGetHTML(url, type, email) {
|
|
213
|
+
function defaultGetHTML(url, type, email, otc) {
|
|
139
214
|
let msg = 'sign in';
|
|
140
215
|
if (type === 'signup') {
|
|
141
216
|
msg = 'confirm your email address';
|
|
142
217
|
}
|
|
218
|
+
|
|
219
|
+
if (otc) {
|
|
220
|
+
return `Enter the code ${otc} or <a href="${url}">click here to ${msg}</a> This msg was sent to ${email}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
143
223
|
return `<a href="${url}">Click here to ${msg}</a> This msg was sent to ${email}`;
|
|
144
224
|
}
|
|
145
225
|
|
|
@@ -147,12 +227,18 @@ function defaultGetHTML(url, type, email) {
|
|
|
147
227
|
* defaultGetSubject
|
|
148
228
|
*
|
|
149
229
|
* @param {string} type - The type of email to send e.g. signin, signup
|
|
230
|
+
* @param {string} otc - Optional one-time-code
|
|
150
231
|
* @returns {string} subject - The subject of an email to send
|
|
151
232
|
*/
|
|
152
|
-
function defaultGetSubject(type) {
|
|
233
|
+
function defaultGetSubject(type, otc) {
|
|
153
234
|
if (type === 'signup') {
|
|
154
235
|
return `Signup!`;
|
|
155
236
|
}
|
|
237
|
+
|
|
238
|
+
if (otc) {
|
|
239
|
+
return `Your signin verification code is ${otc}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
156
242
|
return `Signin!`;
|
|
157
243
|
}
|
|
158
244
|
|
|
@@ -82,7 +82,14 @@ class MembersConfigProvider {
|
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
/**
|
|
86
|
+
* @param {string} token
|
|
87
|
+
* @param {string} type - also known as "action", e.g. "signin" or "signup"
|
|
88
|
+
* @param {string} [referrer] - optional URL for redirecting to after signin
|
|
89
|
+
* @param {string} [otcVerification] - optional for verifying an OTC signin redirect
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
getSigninURL(token, type, referrer, otcVerification) {
|
|
86
93
|
const siteUrl = this._urlUtils.urlFor({relativeUrl: '/members/'}, true);
|
|
87
94
|
const signinURL = new URL(siteUrl);
|
|
88
95
|
signinURL.searchParams.set('token', token);
|
|
@@ -90,6 +97,9 @@ class MembersConfigProvider {
|
|
|
90
97
|
if (referrer) {
|
|
91
98
|
signinURL.searchParams.set('r', referrer);
|
|
92
99
|
}
|
|
100
|
+
if (otcVerification) {
|
|
101
|
+
signinURL.searchParams.set('otc_verification', otcVerification);
|
|
102
|
+
}
|
|
93
103
|
return signinURL.toString();
|
|
94
104
|
}
|
|
95
105
|
}
|