apostrophe 3.12.0 → 3.13.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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.13.0 - 2022-02-04
4
+
5
+ ### Adds
6
+
7
+ * Additional requirements and related UI may be imposed on native ApostropheCMS logins using the new `requirements` feature, which can be extended in modules that `improve` the `@apostrophecms/login` module. These requirements are not imposed for single sign-on logins via `@apostrophecms/passport-bridge`. See the documentation for more information.
8
+ * Adds latest Slovak translation strings to SK.json in `i18n/` folder. Thanks to [Michael Huna](https://github.com/Miselrkba) for the contribution.
9
+ * Verifies `afterPasswordVerified` requirements one by one when emitting done event, allows to manage errors ans success before to go to the next requirement. Stores and validate each requirement in the token. Checks the new `askForConfirmation` requirement option to go to the next step when emitting done event or waiting for the confirm event (in order to manage success messages). Removes support for `afterSubmit` for now.
10
+
11
+ ### Fixes
12
+
13
+ * Decodes the testReq `param` property in `serveNotFound`. This fixes a problem where page titles using diacritics triggered false 404 errors.
14
+ * Registers the default namespace in the Vue instance of i18n, fixing a lack of support for un-namespaced l10n keys in the UI.
15
+
3
16
  ## 3.12.0 - 2022-01-21
4
17
 
5
18
  ### Adds
@@ -12,6 +12,8 @@ module.exports = {
12
12
  'checkbox-blank-icon': 'CheckboxBlankOutline',
13
13
  'check-all-icon': 'CheckAll',
14
14
  'check-bold-icon': 'CheckBold',
15
+ 'check-circle-icon': 'CheckCircle',
16
+ 'check-decagram-icon': 'CheckDecagram',
15
17
  'checkbox-marked-icon': 'CheckboxMarked',
16
18
  'chevron-down-icon': 'ChevronDown',
17
19
  'chevron-left-icon': 'ChevronLeft',
@@ -1114,7 +1114,7 @@ module.exports = {
1114
1114
  await copyOut(uploadfsPath, tempFile);
1115
1115
  await self.sanitizeSvg(tempFile);
1116
1116
  await copyIn(tempFile, uploadfsPath);
1117
- await self.db.update({
1117
+ await self.db.updateOne({
1118
1118
  _id: attachment._id
1119
1119
  }, {
1120
1120
  $set: {
@@ -194,11 +194,11 @@ module.exports = {
194
194
  _id: doc._id.replace(':draft', ':previous')
195
195
  });
196
196
  if (published) {
197
- await self.apos.doc.db.remove({ _id: published._id });
197
+ await self.apos.doc.db.removeOne({ _id: published._id });
198
198
  await self.emit('afterDelete', req, published, { checkForChildren: false });
199
199
  }
200
200
  if (previous) {
201
- await self.apos.doc.db.remove({ _id: previous._id });
201
+ await self.apos.doc.db.removeOne({ _id: previous._id });
202
202
  await self.emit('afterDelete', req, previous, { checkForChildren: false });
203
203
  }
204
204
  },
@@ -307,7 +307,13 @@ module.exports = {
307
307
  // "expires" ourselves too
308
308
  const bearer = await self.apos.login.bearerTokens.findOne({
309
309
  _id: req.token,
310
- expires: { $gte: new Date() }
310
+ expires: { $gte: new Date() },
311
+ // requirementsToVerify array should be empty or inexistant
312
+ // for the token to be usable to log in.
313
+ $or: [
314
+ { requirementsToVerify: { $exists: false } },
315
+ { requirementsToVerify: { $ne: [] } }
316
+ ]
311
317
  });
312
318
  return bearer && bearer.userId;
313
319
  }
@@ -167,6 +167,7 @@
167
167
  "localizeNewRelated": "Localize new related documents",
168
168
  "localizingBusy": "Localizing Content",
169
169
  "login": "Login",
170
+ "loginErrorGeneric": "An error occurred. Please try again.",
170
171
  "loginDisabled": "Login Disabled",
171
172
  "loginPageBothRequired": "Both the username and the password are required.",
172
173
  "loginPageBadCredentials": "Your credentials are incorrect, or there is no such user",
@@ -3,6 +3,7 @@
3
3
  "addItem": "Pridať položku",
4
4
  "addWidgetType": "Pridať {{ label }}",
5
5
  "admin": "Admin",
6
+ "affirmativeLabel": "Áno, pokračovať.",
6
7
  "altText": "Alternatívny text",
7
8
  "altTextHelp": "Alternatívny popis obrázkov pre zlepšenú prístupnosť",
8
9
  "any": "ľubovoľný",
@@ -10,6 +11,8 @@
10
11
  "applyToSubpages": "Aplikovať na podstánky",
11
12
  "arrayCancelDescription": "Chcete zahodiť zmeny v tomto zozname?",
12
13
  "archive": "Archív",
14
+ "archivingBatchConfirmation": "Naozaj chcete archivovať {{ count }} {{ type }}?",
15
+ "archivingBatchConfirmationButton": "Áno, archivovať obsah.",
13
16
  "archiveImage": "Archivovať obrázok",
14
17
  "archiveOnlyThisPage": "Archivovať len túto stránku",
15
18
  "archivePageAndSubpages": "Archivovať túto stránku a všetky podradené stránky",
@@ -121,6 +124,7 @@
121
124
  "errorPageMessage": "Došlo k chybe",
122
125
  "errorPageStatusCode": "500",
123
126
  "errorPageTitle": "Došlo k chybe v nadpise stánky",
127
+ "errorBatchOperationNoti": "Skupinová operácia {{ operation }} zlyhala.",
124
128
  "everythingElse": "Všetko ostatné",
125
129
  "exit": "Odísť",
126
130
  "fetchPublishedVersionFailed": "Pri načítaní zverejnenej verzie dokumentu sa vyskytla chyba.",
@@ -173,6 +177,8 @@
173
177
  "manageDocType": "Spravovať {{ type }}",
174
178
  "manageDraftSubmissions": "Spravujte návrhy príspevkov",
175
179
  "managePages": "Spravovať stránky",
180
+ "maxLabel": "Max:",
181
+ "maxUi": "Max: {{ number }}",
176
182
  "mediaCreatedDate": "Nahraté: {{ createdDate }}",
177
183
  "mediaDimensions": "Rozmery: {{ width }} 𝗑 {{ height }}",
178
184
  "mediaFileSize": "Veľkosť súboru: {{ fileSize }}",
@@ -180,6 +186,10 @@
180
186
  "mediaMB": "{{ size }}MB",
181
187
  "mediaUploadViaDrop": "Presuňte ich sem, keď budete pripravení",
182
188
  "mediaUploadViaExplorer": "Alebo kliknutím otvorte adresárovú štruktúru",
189
+ "minLabel": "Min:",
190
+ "minUi": "Min: {{ number }}",
191
+ "modify": "Zmeniť",
192
+ "modifyOrDelete": "Zmeniť / Vymazať",
183
193
  "moreOptions": "Viac možností",
184
194
  "moreOperations": "Viac úprav",
185
195
  "multipleEditors": "Viaceré editory",
@@ -202,17 +212,19 @@
202
212
  "notFoundPageStatusCode": "404",
203
213
  "notFoundPageTitle": "404 - Stránka nenájdená",
204
214
  "notInLocale": "Aktuálna stránka v {{ label }} neexistuje. Preložte verziu z {{ currentLocale }}?",
215
+ "notificationClearEventError": "Pri vymazávaní zaregistrovanej notifikačnej udalosti sa vyskytla chyba.",
205
216
  "noTypeFound": "Nenašiel sa žiadny {{ type }}",
206
217
  "parentNotLocalized": "Najprv preložte nadradenú stránku",
207
218
  "notYetPublished": "Tento dokument ešte nebol zverejnený.",
208
219
  "nudgeDown": "Posunúť nadol",
209
220
  "nudgeUp": "Posunúť nahor",
221
+ "numberAdded": "{{ count }} Pridané",
210
222
  "office": "Kancelária",
211
223
  "openGlobal": "Otvorte globálne nastavenia webu",
212
224
  "page": "Stránka",
213
225
  "pageDoesNotExistYet": "Stránka zatiaľ neexistuje",
214
226
  "pageDoesNotExistYetDescription": "Stránka, ktorá poskytuje záznam pre tento diel, ešte nie je k dispozícii ako {{ mode }} v jazykovej mutácii {{ locale }}.",
215
- "pageIsParked": "Táto stránka je zaparkovaná a nemožno ju presunúť",
227
+ "pageIsParked": "Táto stránka je uzamknutá nemožno ju presunúť",
216
228
  "pageNumber": "Stránka {{ number }}",
217
229
  "pageTitle": "Názov stránky",
218
230
  "pages": "Stránky",
@@ -242,6 +254,8 @@
242
254
  "richTextUndo": "Vrátiť späť",
243
255
  "richTextStyleConfigWarning": "Nesprávne nakonfigurovaný štýl: popiska: {{ label }}, {{ tag }}",
244
256
  "password": "Heslo",
257
+ "passwordErrorMin": "Minimum {{ min }} znakov",
258
+ "passwordErrorMax": "Maximum {{ max }} znakov",
245
259
  "passwordResetRequest": "Vaša žiadosť o obnovenie hesla zo stránky {{ site }}",
246
260
  "pasteWidget": "Prilepiť {{ widget }}",
247
261
  "pending": "Čaká na spracovanie",
@@ -277,6 +291,8 @@
277
291
  "relatedDocsOnly": "Len súvisiace dokumenty",
278
292
  "relatedDocsDefinition": "Súvisiace dokumenty sú dokumenty, na ktoré sa odkazuje v tomto dokumente. Obvykle to zahŕňa obrázky, obsah definovaný vzťahmi atď.",
279
293
  "restore": "Obnoviť",
294
+ "restoreBatchConfirmation": "Naozaj chcete obnoviť {{ count }} {{ type }}?",
295
+ "restoreBatchConfirmationButton": "Áno, obnoviť obsah.",
280
296
  "resolveErrorsBeforeSaving": "Pred uložením vyriešte chyby.",
281
297
  "resolveErrorsFirst": "Najprv vyriešte chyby.",
282
298
  "restoreOnlyThisPage": "Obnovte iba túto stránku",
@@ -305,6 +321,12 @@
305
321
  "select": "Vybrať",
306
322
  "selectedMenuItem": "✓ {{ label }}",
307
323
  "selectAll": "Vybrať všetko",
324
+ "selectBoxMessage": "{{ num }} {{ label }} vybraté.",
325
+ "selectBoxMessagePage": "{{ num }} {{ label }} na tejto stránke vybraté.",
326
+ "selectBoxMessageAllButton": "Vybrať všetko {{ num }} {{ label }}.",
327
+ "selectBoxMessageButton": "Vybrať {{ num }} {{ label }}.",
328
+ "selectBoxMessageSelected": "{{ num }} {{ label }} vybraté.",
329
+ "selectBoxMessageAllSelected": "Všetky {{ num }} {{ label }} vybraté.",
308
330
  "deselectAll": "Odznačiť všetko",
309
331
  "selectContent": "Vyberte obsah",
310
332
  "selectContentToLocalize": "Ktorý obsah chcete lokalizovať?",
@@ -47,6 +47,7 @@ module.exports = {
47
47
  }
48
48
  },
49
49
  async init(self) {
50
+ self.defaultNamespace = 'default';
50
51
  self.namespaces = {};
51
52
  self.debug = process.env.APOS_DEBUG_I18N ? true : self.options.debug;
52
53
  self.show = process.env.APOS_SHOW_I18N ? true : self.options.show;
@@ -84,7 +85,7 @@ module.exports = {
84
85
  // Nunjucks and Vue will already do this
85
86
  escapeValue: false
86
87
  },
87
- defaultNS: 'default',
88
+ defaultNS: self.defaultNamespace,
88
89
  debug: self.debug
89
90
  });
90
91
  if (self.show) {
@@ -540,6 +541,7 @@ module.exports = {
540
541
  i18n,
541
542
  locale: req.locale,
542
543
  defaultLocale: self.defaultLocale,
544
+ defaultNamespace: self.defaultNamespace,
543
545
  locales: self.locales,
544
546
  debug: self.debug,
545
547
  show: self.show,
@@ -44,6 +44,7 @@ const Promise = require('bluebird');
44
44
  const cuid = require('cuid');
45
45
 
46
46
  module.exports = {
47
+ cascades: [ 'requirements' ],
47
48
  options: {
48
49
  alias: 'login',
49
50
  localLogin: true,
@@ -106,34 +107,13 @@ module.exports = {
106
107
  return {
107
108
  post: {
108
109
  async login(req) {
109
- const username = self.apos.launder.string(req.body.username);
110
- const password = self.apos.launder.string(req.body.password);
111
- const session = self.apos.launder.boolean(req.body.session);
112
- if (!(username && password)) {
113
- throw self.apos.error('invalid', req.t('apostrophe:loginPageBothRequired'));
114
- }
115
- const user = await self.apos.login.verifyLogin(username, password);
116
- if (!user) {
117
- // For security reasons we may not tell the user which case applies
118
- throw self.apos.error('invalid', req.t('apostrophe:loginPageBadCredentials'));
119
- }
120
- if (session) {
121
- const passportLogin = (user) => {
122
- return require('util').promisify(function(user, callback) {
123
- return req.login(user, callback);
124
- })(user);
125
- };
126
- await passportLogin(user);
110
+ // Don't make verify functions worry about whether this object
111
+ // is present, just the value of their own sub-property
112
+ req.body.requirements = req.body.requirements || {};
113
+ if (req.body.incompleteToken) {
114
+ return self.finalizeIncompleteLogin(req);
127
115
  } else {
128
- const token = cuid();
129
- await self.bearerTokens.insert({
130
- _id: token,
131
- userId: user._id,
132
- expires: new Date(new Date().getTime() + (self.options.bearerTokens.lifetime || (86400 * 7 * 2)) * 1000)
133
- });
134
- return {
135
- token
136
- };
116
+ return self.initialLogin(req);
137
117
  }
138
118
  },
139
119
  async logout(req) {
@@ -141,7 +121,7 @@ module.exports = {
141
121
  throw self.apos.error('forbidden', req.t('apostrophe:logOutNotLoggedIn'));
142
122
  }
143
123
  if (req.token) {
144
- await self.bearerTokens.remove({
124
+ await self.bearerTokens.removeOne({
145
125
  userId: req.user._id,
146
126
  _id: req.token
147
127
  });
@@ -156,6 +136,68 @@ module.exports = {
156
136
  await destroySession();
157
137
  }
158
138
  },
139
+ // invokes the `props(req, user)` function for the requirement specified by
140
+ // `body.name`. Invoked before displaying each `afterPasswordVerified`
141
+ // requirement. The return value of the function, which should
142
+ // be an object, is delivered as the API response
143
+ async requirementProps(req) {
144
+ const { user } = await self.findIncompleteTokenAndUser(req, req.body.incompleteToken);
145
+
146
+ const name = self.apos.launder.string(req.body.name);
147
+
148
+ const requirement = self.requirements[name];
149
+ if (!requirement) {
150
+ throw self.apos.error('notfound');
151
+ }
152
+ if (!requirement.props) {
153
+ return {};
154
+ }
155
+ return requirement.props(req, user);
156
+ },
157
+ async requirementVerify(req) {
158
+ const name = self.apos.launder.string(req.body.name);
159
+
160
+ const { user } = await self.findIncompleteTokenAndUser(req, req.body.incompleteToken);
161
+
162
+ const requirement = self.requirements[name];
163
+
164
+ if (!requirement) {
165
+ throw self.apos.error('notfound');
166
+ }
167
+
168
+ if (!requirement.verify) {
169
+ throw self.apos.error('invalid', 'You must provide a verify method in your requirement');
170
+ }
171
+
172
+ try {
173
+ await requirement.verify(req, req.body.value, user);
174
+
175
+ const token = await self.bearerTokens.findOne({
176
+ _id: self.apos.launder.string(req.body.incompleteToken),
177
+ requirementsToVerify: { $exists: true },
178
+ expires: {
179
+ $gte: new Date()
180
+ }
181
+ });
182
+
183
+ if (!token) {
184
+ throw self.apos.error('notfound');
185
+ }
186
+
187
+ await self.bearerTokens.updateOne(token, {
188
+ $pull: { requirementsToVerify: name }
189
+ });
190
+
191
+ return {};
192
+ } catch (err) {
193
+ err.data = err.data || {};
194
+ err.data.requirement = name;
195
+ throw err;
196
+ }
197
+ },
198
+ async context(req) {
199
+ return self.getContext(req);
200
+ },
159
201
  ...(self.options.passwordReset ? {
160
202
  async resetRequest(req) {
161
203
  const site = (req.headers.host || '').replace(/:\d+$/, '');
@@ -217,19 +259,12 @@ module.exports = {
217
259
  } : {})
218
260
  },
219
261
  get: {
220
- context () {
221
- let aposPackage = {};
222
- try {
223
- aposPackage = require('../../../package.json');
224
- } catch (err) {
225
- self.apos.util.error(err);
226
- }
227
-
228
- return {
229
- env: process.env.NODE_ENV || 'development',
230
- name: (process.env.npm_package_name && process.env.npm_package_name.replace(/-/g, ' ')) || 'Apostrophe',
231
- version: aposPackage.version || '3'
232
- };
262
+ // For bc this route is still available via GET, however
263
+ // it should be accessed via POST because the result
264
+ // may differ by individual user session and should not
265
+ // be cached
266
+ async context(req) {
267
+ return self.getContext(req);
233
268
  }
234
269
  }
235
270
  };
@@ -237,6 +272,33 @@ module.exports = {
237
272
  methods(self) {
238
273
  return {
239
274
 
275
+ // Implements the context route, which provides basic
276
+ // information about the site being logged into and also
277
+ // props for beforeSubmit requirements
278
+ async getContext(req) {
279
+ const aposPackage = require('../../../package.json');
280
+ // For performance beforeSubmit requirement props all happen together here
281
+ const requirementProps = {};
282
+ for (const [ name, requirement ] of Object.entries(self.requirements)) {
283
+ if ((requirement.phase !== 'afterPasswordVerified') && requirement.props) {
284
+ try {
285
+ requirementProps[name] = await requirement.props(req);
286
+ } catch (e) {
287
+ if (e.body && e.body.data) {
288
+ e.body.data.requirement = name;
289
+ }
290
+ throw e;
291
+ }
292
+ }
293
+ }
294
+ return {
295
+ env: process.env.NODE_ENV || 'development',
296
+ name: (process.env.npm_package_name && process.env.npm_package_name.replace(/-/g, ' ')) || 'Apostrophe',
297
+ version: aposPackage.version || '3',
298
+ requirementProps
299
+ };
300
+ },
301
+
240
302
  // return the loginUrl option
241
303
  login(url) {
242
304
  return self.options.loginUrl ? self.options.loginUrl : '/login';
@@ -368,7 +430,17 @@ module.exports = {
368
430
  username: req.user.username,
369
431
  email: req.user.email
370
432
  }
371
- } : {})
433
+ } : {}),
434
+ requirements: Object.fromEntries(
435
+ Object.entries(self.requirements).map(([ name, requirement ]) => {
436
+ const browserRequirement = {
437
+ phase: requirement.phase,
438
+ propsRequired: !!requirement.props,
439
+ askForConfirmation: requirement.askForConfirmation || false
440
+ };
441
+ return [ name, browserRequirement ];
442
+ })
443
+ )
372
444
  };
373
445
  },
374
446
 
@@ -384,6 +456,164 @@ module.exports = {
384
456
  async enableBearerTokens() {
385
457
  self.bearerTokens = self.apos.db.collection('aposBearerTokens');
386
458
  await self.bearerTokens.createIndex({ expires: 1 }, { expireAfterSeconds: 0 });
459
+ },
460
+
461
+ // Finalize an incomplete login based on the provided incompleteToken
462
+ // and various `requirements` subproperties. Implementation detail of the login route
463
+ async finalizeIncompleteLogin(req) {
464
+ const session = self.apos.launder.boolean(req.body.session);
465
+ // Completing a previous incomplete login
466
+ // (password was verified but post-password-verification
467
+ // requirements were not supplied)
468
+ const token = await self.bearerTokens.findOne({
469
+ _id: self.apos.launder.string(req.body.incompleteToken),
470
+ requirementsToVerify: {
471
+ $exists: true
472
+ },
473
+ expires: {
474
+ $gte: new Date()
475
+ }
476
+ });
477
+
478
+ if (!token) {
479
+ throw self.apos.error('notfound');
480
+ }
481
+
482
+ if (token.requirementsToVerify.length) {
483
+ throw self.apos.error('forbidden', 'All requirements must be verified');
484
+ }
485
+
486
+ const user = await self.deserializeUser(token.userId);
487
+ if (!user) {
488
+ await self.bearerTokens.removeOne({
489
+ _id: token.userId
490
+ });
491
+ throw self.apos.error('notfound');
492
+ }
493
+
494
+ if (session) {
495
+ await self.bearerTokens.removeOne({
496
+ _id: token.userId
497
+ });
498
+ await self.passportLogin(req, user);
499
+ } else {
500
+ delete token.requirementsToVerify;
501
+ self.bearerTokens.updateOne(token, {
502
+ $unset: {
503
+ requirementsToVerify: 1
504
+ }
505
+ });
506
+ return {
507
+ token
508
+ };
509
+ }
510
+ },
511
+
512
+ // Implementation detail of the login route and the requirementProps mechanism for
513
+ // custom login requirements. Given the string `token`, returns
514
+ // `{ token, user }`. Throws an exception if the token is not found.
515
+ // `token` is sanitized before passing to mongodb.
516
+ async findIncompleteTokenAndUser(req, token) {
517
+ token = await self.bearerTokens.findOne({
518
+ _id: self.apos.launder.string(token),
519
+ requirementsToVerify: {
520
+ $exists: true,
521
+ $ne: []
522
+ },
523
+ expires: {
524
+ $gte: new Date()
525
+ }
526
+ });
527
+ if (!token) {
528
+ throw self.apos.error('notfound');
529
+ }
530
+ const user = await self.deserializeUser(token.userId);
531
+ if (!user) {
532
+ await self.bearerTokens.removeOne({
533
+ _id: token._id
534
+ });
535
+ throw self.apos.error('notfound');
536
+ }
537
+ return {
538
+ token,
539
+ user
540
+ };
541
+ },
542
+
543
+ // Implementation detail of the login route. Log in the user, or if there are
544
+ // `requirements` that require password verification occur first, return an incomplete token.
545
+ async initialLogin(req) {
546
+ // Initial login step
547
+ const username = self.apos.launder.string(req.body.username);
548
+ const password = self.apos.launder.string(req.body.password);
549
+ if (!(username && password)) {
550
+ throw self.apos.error('invalid', req.t('apostrophe:loginPageBothRequired'));
551
+ }
552
+ const { earlyRequirements, lateRequirements } = self.filterRequirements();
553
+ for (const [ name, requirement ] of Object.entries(earlyRequirements)) {
554
+ try {
555
+ await requirement.verify(req, req.body.requirements && req.body.requirements[name]);
556
+ } catch (e) {
557
+ e.data = e.data || {};
558
+ e.data.requirement = name;
559
+ throw e;
560
+ }
561
+ }
562
+ const user = await self.apos.login.verifyLogin(username, password);
563
+ if (!user) {
564
+ // For security reasons we may not tell the user which case applies
565
+ throw self.apos.error('invalid', req.t('apostrophe:loginPageBadCredentials'));
566
+ }
567
+
568
+ const requirementsToVerify = Object.keys(lateRequirements);
569
+
570
+ if (requirementsToVerify.length) {
571
+ const token = cuid();
572
+
573
+ await self.bearerTokens.insert({
574
+ _id: token,
575
+ userId: user._id,
576
+ requirementsToVerify,
577
+ // Default lifetime of 1 hour is generous to permit situations like
578
+ // installing a TOTP app for the first time
579
+ expires: new Date(new Date().getTime() + (self.options.incompleteLifetime || 60 * 60 * 1000))
580
+ });
581
+ return {
582
+ incompleteToken: token
583
+ };
584
+ } else {
585
+ const session = self.apos.launder.boolean(req.body.session);
586
+ if (session) {
587
+ await self.passportLogin(req, user);
588
+ } else {
589
+ const token = cuid();
590
+ await self.bearerTokens.insert({
591
+ _id: token,
592
+ userId: user._id,
593
+ expires: new Date(new Date().getTime() + (self.options.bearerTokens.lifetime || (86400 * 7 * 2)) * 1000)
594
+ });
595
+ return {
596
+ token
597
+ };
598
+ }
599
+ }
600
+ },
601
+
602
+ filterRequirements() {
603
+ return {
604
+ earlyRequirements: Object.fromEntries(Object.entries(self.requirements).filter(([ name, requirement ]) => requirement.phase === 'beforeSubmit')),
605
+ lateRequirements: Object.fromEntries(Object.entries(self.requirements).filter(([ name, requirement ]) => requirement.phase === 'afterPasswordVerified'))
606
+ };
607
+ },
608
+
609
+ // Awaitable wrapper for req.login. An implementation detail of the login route
610
+ async passportLogin(req, user) {
611
+ const passportLogin = (user) => {
612
+ return require('util').promisify(function(user, callback) {
613
+ return req.login(user, callback);
614
+ })(user);
615
+ };
616
+ await passportLogin(user);
387
617
  }
388
618
  };
389
619
  },