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.
Files changed (48) hide show
  1. package/components/tryghost-i18n-6.0.7.tgz +0 -0
  2. package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +1 -1
  3. package/core/built/admin/assets/admin-x-activitypub/{index-DBxGycCG.mjs → index-B-ckGCDl.mjs} +20083 -16775
  4. package/core/built/admin/assets/admin-x-activitypub/{index-Db9aLAi4.mjs → index-C81KQoIh.mjs} +2 -2
  5. package/core/built/admin/assets/admin-x-settings/{CodeEditorView-B4W7CQcA.mjs → CodeEditorView-BfL5FINN.mjs} +2 -2
  6. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
  7. package/core/built/admin/assets/admin-x-settings/{index-jv9DN3ZO.mjs → index-C_ZS-INP.mjs} +2 -2
  8. package/core/built/admin/assets/admin-x-settings/{index-CuwMM9FM.mjs → index-DiNZ3HQD.mjs} +12 -4
  9. package/core/built/admin/assets/admin-x-settings/{modals-CUGEPPYA.mjs → modals-BEiWgHsk.mjs} +8807 -8799
  10. package/core/built/admin/assets/{chunk.524.0e2bff9b664f925d7af7.js → chunk.524.9f989b3664418d6271a7.js} +6 -6
  11. package/core/built/admin/assets/{chunk.582.a8f6c436bbec6f9ba678.js → chunk.582.2421014e45b977b43b68.js} +8 -8
  12. package/core/built/admin/assets/{ghost-2b02de85a93ec9a5623180b373cece35.js → ghost-182ed60de3f37fff8a40cdd65d3bd2ef.js} +55 -50
  13. package/core/built/admin/assets/{ghost-2c537ee89c36199137eafc1768fd7de8.css → ghost-a7a53bf80dc45c37ae9c174a0d02a882.css} +1 -1
  14. package/core/built/admin/assets/{ghost-dark-ad23efc1d702e3643a8ee90d089df5d6.css → ghost-dark-6e0062029f988d8676e87f22d8e7f4a3.css} +1 -1
  15. package/core/built/admin/assets/posts/posts.js +78821 -77904
  16. package/core/built/admin/assets/stats/stats.js +21689 -21645
  17. package/core/built/admin/index.html +4 -4
  18. package/core/server/data/tinybird/datasources/analytics_events.datasource +3 -2
  19. package/core/server/data/tinybird/datasources/analytics_events_test.datasource +3 -2
  20. package/core/server/data/tinybird/endpoints/api_top_utm_campaigns.pipe +31 -0
  21. package/core/server/data/tinybird/endpoints/api_top_utm_contents.pipe +31 -0
  22. package/core/server/data/tinybird/endpoints/api_top_utm_mediums.pipe +31 -0
  23. package/core/server/data/tinybird/endpoints/api_top_utm_sources.pipe +31 -0
  24. package/core/server/data/tinybird/endpoints/api_top_utm_terms.pipe +31 -0
  25. package/core/server/data/tinybird/scripts/configure-ghost.sh +4 -2
  26. package/core/server/data/tinybird/tests/api_top_utm_campaigns.yaml +108 -0
  27. package/core/server/data/tinybird/tests/api_top_utm_contents.yaml +108 -0
  28. package/core/server/data/tinybird/tests/api_top_utm_mediums.yaml +108 -0
  29. package/core/server/data/tinybird/tests/api_top_utm_sources.yaml +108 -0
  30. package/core/server/data/tinybird/tests/api_top_utm_terms.yaml +108 -0
  31. package/core/server/services/lib/magic-link/MagicLink.js +98 -12
  32. package/core/server/services/members/MembersConfigProvider.js +11 -1
  33. package/core/server/services/members/SingleUseTokenProvider.js +218 -5
  34. package/core/server/services/members/api.js +18 -8
  35. package/core/server/services/members/emails/signin.js +42 -5
  36. package/core/server/services/members/members-api/controllers/RouterController.js +105 -18
  37. package/core/server/services/members/members-api/members-api.js +12 -7
  38. package/core/server/services/members/members-ssr.js +5 -3
  39. package/core/server/services/members/middleware.js +2 -2
  40. package/core/server/services/stats/PostsStatsService.js +26 -9
  41. package/core/server/services/stats/StatsService.js +1 -1
  42. package/core/server/web/members/app.js +12 -3
  43. package/core/shared/config/defaults.json +1 -1
  44. package/core/shared/labs.js +3 -1
  45. package/package.json +9 -9
  46. package/tsconfig.tsbuildinfo +1 -1
  47. package/yarn.lock +213 -323
  48. 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
- * @returns {Promise<{token: Token, info: SentMessageInfo}>}
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 {token, info};
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
- getSigninURL(token, type, referrer) {
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
  }