apostrophe 3.11.0 → 3.14.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 +56 -0
- package/index.js +37 -1
- package/lib/moog.js +2 -2
- package/modules/@apostrophecms/asset/index.js +24 -9
- package/modules/@apostrophecms/asset/lib/globalIcons.js +2 -0
- package/modules/@apostrophecms/attachment/index.js +1 -1
- package/modules/@apostrophecms/doc/index.js +9 -3
- package/modules/@apostrophecms/doc-type/index.js +2 -2
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +0 -7
- package/modules/@apostrophecms/express/index.js +50 -38
- package/modules/@apostrophecms/http/index.js +0 -20
- package/modules/@apostrophecms/i18n/i18n/en.json +1 -0
- package/modules/@apostrophecms/i18n/i18n/sk.json +23 -1
- package/modules/@apostrophecms/i18n/index.js +62 -13
- package/modules/@apostrophecms/login/index.js +282 -42
- package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +242 -77
- package/modules/@apostrophecms/login/ui/apos/components/TheAposLoginHeader.vue +108 -0
- package/modules/@apostrophecms/module/index.js +12 -2
- package/modules/@apostrophecms/page/index.js +98 -82
- package/modules/@apostrophecms/permission/index.js +1 -1
- package/modules/@apostrophecms/piece-page-type/index.js +1 -1
- package/modules/@apostrophecms/piece-type/index.js +86 -73
- package/modules/@apostrophecms/schema/index.js +18 -2
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +12 -5
- package/modules/@apostrophecms/schema/ui/apos/components/AposInputCheckboxes.vue +4 -0
- package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +18 -0
- package/modules/@apostrophecms/util/index.js +3 -9
- package/modules/@apostrophecms/util/ui/src/http.js +1 -7
- package/package.json +2 -1
- package/test/express.js +2 -26
- package/test/http.js +0 -24
- package/test/login-requirements.js +328 -0
- package/test/modules/base-type/i18n/custom/en.json +4 -0
- package/test/modules/base-type/i18n/en.json +3 -0
- package/test/modules/nested-module-subdirs/example1/index.js +5 -0
- package/test/modules/nested-module-subdirs/modules.js +7 -0
- package/test/modules/subtype/i18n/custom/en.json +4 -0
- package/test/modules/subtype/index.js +7 -0
- package/test/pages-rest.js +39 -0
- package/test/pieces-page-type.js +63 -0
- package/test/static-i18n.js +28 -0
- package/test/with-nested-module-subdirs.js +32 -0
- package/test/without-nested-module-subdirs.js +31 -0
|
@@ -10,6 +10,7 @@ const fs = require('fs');
|
|
|
10
10
|
const _ = require('lodash');
|
|
11
11
|
const { stripIndent } = require('common-tags');
|
|
12
12
|
const ExpressSessionCookie = require('express-session/session/cookie');
|
|
13
|
+
const path = require('path');
|
|
13
14
|
|
|
14
15
|
const apostropheI18nDebugPlugin = {
|
|
15
16
|
type: 'postProcessor',
|
|
@@ -46,6 +47,7 @@ module.exports = {
|
|
|
46
47
|
}
|
|
47
48
|
},
|
|
48
49
|
async init(self) {
|
|
50
|
+
self.defaultNamespace = 'default';
|
|
49
51
|
self.namespaces = {};
|
|
50
52
|
self.debug = process.env.APOS_DEBUG_I18N ? true : self.options.debug;
|
|
51
53
|
self.show = process.env.APOS_SHOW_I18N ? true : self.options.show;
|
|
@@ -83,7 +85,7 @@ module.exports = {
|
|
|
83
85
|
// Nunjucks and Vue will already do this
|
|
84
86
|
escapeValue: false
|
|
85
87
|
},
|
|
86
|
-
defaultNS:
|
|
88
|
+
defaultNS: self.defaultNamespace,
|
|
87
89
|
debug: self.debug
|
|
88
90
|
});
|
|
89
91
|
if (self.show) {
|
|
@@ -380,21 +382,65 @@ module.exports = {
|
|
|
380
382
|
// Add the i18next resources provided by the specified module,
|
|
381
383
|
// merging with any existing phrases for the same locales and namespaces
|
|
382
384
|
addResourcesForModule(module) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
385
|
+
self.addDefaultResourcesForModule(module);
|
|
386
|
+
self.addNamespacedResourcesForModule(module);
|
|
387
|
+
},
|
|
388
|
+
// Automatically adds any localizations found in .json files in the main `i18n` subdirectory
|
|
389
|
+
// of a module.
|
|
390
|
+
//
|
|
391
|
+
// These are added to the `default` namespace, unless the legacy `i18n.ns` option is set
|
|
392
|
+
// for the module (not the preferred way, use namespace subdirectories in new projects).
|
|
393
|
+
addDefaultResourcesForModule(module) {
|
|
394
|
+
const ns = (module.options.i18n && module.options.i18n.ns) || 'default';
|
|
387
395
|
self.namespaces[ns] = self.namespaces[ns] || {};
|
|
388
|
-
self.namespaces[ns].browser = self.namespaces[ns].browser ||
|
|
396
|
+
self.namespaces[ns].browser = self.namespaces[ns].browser || (module.options.i18n && module.options.i18n.browser);
|
|
389
397
|
for (const entry of module.__meta.chain) {
|
|
390
|
-
const localizationsDir =
|
|
391
|
-
if (!
|
|
392
|
-
|
|
398
|
+
const localizationsDir = path.join(entry.dirname, 'i18n');
|
|
399
|
+
if (!self.defaultLocalizationsDirsAdded.has(localizationsDir)) {
|
|
400
|
+
self.defaultLocalizationsDirsAdded.add(localizationsDir);
|
|
401
|
+
if (!fs.existsSync(localizationsDir)) {
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
for (const localizationFile of fs.readdirSync(localizationsDir)) {
|
|
405
|
+
if (!localizationFile.endsWith('.json')) {
|
|
406
|
+
// Likely a namespace subdirectory
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const data = JSON.parse(fs.readFileSync(path.join(localizationsDir, localizationFile)));
|
|
410
|
+
const locale = localizationFile.replace('.json', '');
|
|
411
|
+
self.i18next.addResourceBundle(locale, ns, data, true, true);
|
|
412
|
+
}
|
|
393
413
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
// Automatically adds any localizations found in subdirectories of the main `i18n`
|
|
417
|
+
// subdirectory of a module. The subdirectory's name is treated as an i18n namespace
|
|
418
|
+
// name.
|
|
419
|
+
addNamespacedResourcesForModule(module) {
|
|
420
|
+
for (const entry of module.__meta.chain) {
|
|
421
|
+
const metadata = module.__meta.i18n[entry.name] || {};
|
|
422
|
+
const localizationsDir = `${entry.dirname}/i18n`;
|
|
423
|
+
if (!self.namespacedLocalizationsDirsAdded.has(localizationsDir)) {
|
|
424
|
+
self.namespacedLocalizationsDirsAdded.add(localizationsDir);
|
|
425
|
+
if (!fs.existsSync(localizationsDir)) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
for (const ns of fs.readdirSync(localizationsDir)) {
|
|
429
|
+
if (ns.endsWith('.json')) {
|
|
430
|
+
// A JSON file for the default namespace, already handled
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
self.namespaces[ns] = self.namespaces[ns] || {};
|
|
434
|
+
self.namespaces[ns].browser = self.namespaces[ns].browser ||
|
|
435
|
+
(metadata[ns] && metadata[ns].browser);
|
|
436
|
+
const namespaceDir = path.join(localizationsDir, ns);
|
|
437
|
+
for (const localizationFile of fs.readdirSync(namespaceDir)) {
|
|
438
|
+
const fullLocalizationFile = path.join(namespaceDir, localizationFile);
|
|
439
|
+
const data = JSON.parse(fs.readFileSync(fullLocalizationFile));
|
|
440
|
+
const locale = localizationFile.replace('.json', '');
|
|
441
|
+
self.i18next.addResourceBundle(locale, ns, data, true, true);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
398
444
|
}
|
|
399
445
|
}
|
|
400
446
|
},
|
|
@@ -402,6 +448,8 @@ module.exports = {
|
|
|
402
448
|
// itself, called by init. Later modules call addResourcesForModule(self),
|
|
403
449
|
// making phrases available gradually as Apostrophe starts up
|
|
404
450
|
addInitialResources() {
|
|
451
|
+
self.defaultLocalizationsDirsAdded = new Set();
|
|
452
|
+
self.namespacedLocalizationsDirsAdded = new Set();
|
|
405
453
|
for (const module of Object.values(self.apos.modules)) {
|
|
406
454
|
self.addResourcesForModule(module);
|
|
407
455
|
}
|
|
@@ -493,6 +541,7 @@ module.exports = {
|
|
|
493
541
|
i18n,
|
|
494
542
|
locale: req.locale,
|
|
495
543
|
defaultLocale: self.defaultLocale,
|
|
544
|
+
defaultNamespace: self.defaultNamespace,
|
|
496
545
|
locales: self.locales,
|
|
497
546
|
debug: self.debug,
|
|
498
547
|
show: self.show,
|
|
@@ -42,8 +42,10 @@ const Passport = require('passport').Passport;
|
|
|
42
42
|
const LocalStrategy = require('passport-local');
|
|
43
43
|
const Promise = require('bluebird');
|
|
44
44
|
const cuid = require('cuid');
|
|
45
|
+
const expressSession = require('express-session');
|
|
45
46
|
|
|
46
47
|
module.exports = {
|
|
48
|
+
cascades: [ 'requirements' ],
|
|
47
49
|
options: {
|
|
48
50
|
alias: 'login',
|
|
49
51
|
localLogin: true,
|
|
@@ -106,34 +108,13 @@ module.exports = {
|
|
|
106
108
|
return {
|
|
107
109
|
post: {
|
|
108
110
|
async login(req) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
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);
|
|
111
|
+
// Don't make verify functions worry about whether this object
|
|
112
|
+
// is present, just the value of their own sub-property
|
|
113
|
+
req.body.requirements = req.body.requirements || {};
|
|
114
|
+
if (req.body.incompleteToken) {
|
|
115
|
+
return self.finalizeIncompleteLogin(req);
|
|
127
116
|
} else {
|
|
128
|
-
|
|
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
|
-
};
|
|
117
|
+
return self.initialLogin(req);
|
|
137
118
|
}
|
|
138
119
|
},
|
|
139
120
|
async logout(req) {
|
|
@@ -141,7 +122,7 @@ module.exports = {
|
|
|
141
122
|
throw self.apos.error('forbidden', req.t('apostrophe:logOutNotLoggedIn'));
|
|
142
123
|
}
|
|
143
124
|
if (req.token) {
|
|
144
|
-
await self.bearerTokens.
|
|
125
|
+
await self.bearerTokens.removeOne({
|
|
145
126
|
userId: req.user._id,
|
|
146
127
|
_id: req.token
|
|
147
128
|
});
|
|
@@ -153,8 +134,79 @@ module.exports = {
|
|
|
153
134
|
return req.session.destroy(callback);
|
|
154
135
|
})();
|
|
155
136
|
};
|
|
137
|
+
const cookie = req.session.cookie;
|
|
156
138
|
await destroySession();
|
|
139
|
+
// Session cookie expiration isn't automatic with `req.session.destroy`.
|
|
140
|
+
// Fix that to reduce challenges for those attempting to implement custom
|
|
141
|
+
// caching strategies at the edge
|
|
142
|
+
// https://github.com/expressjs/session/issues/241
|
|
143
|
+
const expireCookie = new expressSession.Cookie(cookie);
|
|
144
|
+
expireCookie.expires = new Date(0);
|
|
145
|
+
const name = self.apos.modules['@apostrophecms/express'].sessionOptions.name;
|
|
146
|
+
req.res.header('set-cookie', expireCookie.serialize(name, 'deleted'));
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
// invokes the `props(req, user)` function for the requirement specified by
|
|
150
|
+
// `body.name`. Invoked before displaying each `afterPasswordVerified`
|
|
151
|
+
// requirement. The return value of the function, which should
|
|
152
|
+
// be an object, is delivered as the API response
|
|
153
|
+
async requirementProps(req) {
|
|
154
|
+
const { user } = await self.findIncompleteTokenAndUser(req, req.body.incompleteToken);
|
|
155
|
+
|
|
156
|
+
const name = self.apos.launder.string(req.body.name);
|
|
157
|
+
|
|
158
|
+
const requirement = self.requirements[name];
|
|
159
|
+
if (!requirement) {
|
|
160
|
+
throw self.apos.error('notfound');
|
|
161
|
+
}
|
|
162
|
+
if (!requirement.props) {
|
|
163
|
+
return {};
|
|
157
164
|
}
|
|
165
|
+
return requirement.props(req, user);
|
|
166
|
+
},
|
|
167
|
+
async requirementVerify(req) {
|
|
168
|
+
const name = self.apos.launder.string(req.body.name);
|
|
169
|
+
|
|
170
|
+
const { user } = await self.findIncompleteTokenAndUser(req, req.body.incompleteToken);
|
|
171
|
+
|
|
172
|
+
const requirement = self.requirements[name];
|
|
173
|
+
|
|
174
|
+
if (!requirement) {
|
|
175
|
+
throw self.apos.error('notfound');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!requirement.verify) {
|
|
179
|
+
throw self.apos.error('invalid', 'You must provide a verify method in your requirement');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await requirement.verify(req, req.body.value, user);
|
|
184
|
+
|
|
185
|
+
const token = await self.bearerTokens.findOne({
|
|
186
|
+
_id: self.apos.launder.string(req.body.incompleteToken),
|
|
187
|
+
requirementsToVerify: { $exists: true },
|
|
188
|
+
expires: {
|
|
189
|
+
$gte: new Date()
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (!token) {
|
|
194
|
+
throw self.apos.error('notfound');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await self.bearerTokens.updateOne(token, {
|
|
198
|
+
$pull: { requirementsToVerify: name }
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return {};
|
|
202
|
+
} catch (err) {
|
|
203
|
+
err.data = err.data || {};
|
|
204
|
+
err.data.requirement = name;
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
async context(req) {
|
|
209
|
+
return self.getContext(req);
|
|
158
210
|
},
|
|
159
211
|
...(self.options.passwordReset ? {
|
|
160
212
|
async resetRequest(req) {
|
|
@@ -217,19 +269,12 @@ module.exports = {
|
|
|
217
269
|
} : {})
|
|
218
270
|
},
|
|
219
271
|
get: {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
};
|
|
272
|
+
// For bc this route is still available via GET, however
|
|
273
|
+
// it should be accessed via POST because the result
|
|
274
|
+
// may differ by individual user session and should not
|
|
275
|
+
// be cached
|
|
276
|
+
async context(req) {
|
|
277
|
+
return self.getContext(req);
|
|
233
278
|
}
|
|
234
279
|
}
|
|
235
280
|
};
|
|
@@ -237,6 +282,33 @@ module.exports = {
|
|
|
237
282
|
methods(self) {
|
|
238
283
|
return {
|
|
239
284
|
|
|
285
|
+
// Implements the context route, which provides basic
|
|
286
|
+
// information about the site being logged into and also
|
|
287
|
+
// props for beforeSubmit requirements
|
|
288
|
+
async getContext(req) {
|
|
289
|
+
const aposPackage = require('../../../package.json');
|
|
290
|
+
// For performance beforeSubmit requirement props all happen together here
|
|
291
|
+
const requirementProps = {};
|
|
292
|
+
for (const [ name, requirement ] of Object.entries(self.requirements)) {
|
|
293
|
+
if ((requirement.phase !== 'afterPasswordVerified') && requirement.props) {
|
|
294
|
+
try {
|
|
295
|
+
requirementProps[name] = await requirement.props(req);
|
|
296
|
+
} catch (e) {
|
|
297
|
+
if (e.body && e.body.data) {
|
|
298
|
+
e.body.data.requirement = name;
|
|
299
|
+
}
|
|
300
|
+
throw e;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
env: process.env.NODE_ENV || 'development',
|
|
306
|
+
name: (process.env.npm_package_name && process.env.npm_package_name.replace(/-/g, ' ')) || 'Apostrophe',
|
|
307
|
+
version: aposPackage.version || '3',
|
|
308
|
+
requirementProps
|
|
309
|
+
};
|
|
310
|
+
},
|
|
311
|
+
|
|
240
312
|
// return the loginUrl option
|
|
241
313
|
login(url) {
|
|
242
314
|
return self.options.loginUrl ? self.options.loginUrl : '/login';
|
|
@@ -368,7 +440,17 @@ module.exports = {
|
|
|
368
440
|
username: req.user.username,
|
|
369
441
|
email: req.user.email
|
|
370
442
|
}
|
|
371
|
-
} : {})
|
|
443
|
+
} : {}),
|
|
444
|
+
requirements: Object.fromEntries(
|
|
445
|
+
Object.entries(self.requirements).map(([ name, requirement ]) => {
|
|
446
|
+
const browserRequirement = {
|
|
447
|
+
phase: requirement.phase,
|
|
448
|
+
propsRequired: !!requirement.props,
|
|
449
|
+
askForConfirmation: requirement.askForConfirmation || false
|
|
450
|
+
};
|
|
451
|
+
return [ name, browserRequirement ];
|
|
452
|
+
})
|
|
453
|
+
)
|
|
372
454
|
};
|
|
373
455
|
},
|
|
374
456
|
|
|
@@ -384,6 +466,164 @@ module.exports = {
|
|
|
384
466
|
async enableBearerTokens() {
|
|
385
467
|
self.bearerTokens = self.apos.db.collection('aposBearerTokens');
|
|
386
468
|
await self.bearerTokens.createIndex({ expires: 1 }, { expireAfterSeconds: 0 });
|
|
469
|
+
},
|
|
470
|
+
|
|
471
|
+
// Finalize an incomplete login based on the provided incompleteToken
|
|
472
|
+
// and various `requirements` subproperties. Implementation detail of the login route
|
|
473
|
+
async finalizeIncompleteLogin(req) {
|
|
474
|
+
const session = self.apos.launder.boolean(req.body.session);
|
|
475
|
+
// Completing a previous incomplete login
|
|
476
|
+
// (password was verified but post-password-verification
|
|
477
|
+
// requirements were not supplied)
|
|
478
|
+
const token = await self.bearerTokens.findOne({
|
|
479
|
+
_id: self.apos.launder.string(req.body.incompleteToken),
|
|
480
|
+
requirementsToVerify: {
|
|
481
|
+
$exists: true
|
|
482
|
+
},
|
|
483
|
+
expires: {
|
|
484
|
+
$gte: new Date()
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
if (!token) {
|
|
489
|
+
throw self.apos.error('notfound');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (token.requirementsToVerify.length) {
|
|
493
|
+
throw self.apos.error('forbidden', 'All requirements must be verified');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const user = await self.deserializeUser(token.userId);
|
|
497
|
+
if (!user) {
|
|
498
|
+
await self.bearerTokens.removeOne({
|
|
499
|
+
_id: token.userId
|
|
500
|
+
});
|
|
501
|
+
throw self.apos.error('notfound');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (session) {
|
|
505
|
+
await self.bearerTokens.removeOne({
|
|
506
|
+
_id: token.userId
|
|
507
|
+
});
|
|
508
|
+
await self.passportLogin(req, user);
|
|
509
|
+
} else {
|
|
510
|
+
delete token.requirementsToVerify;
|
|
511
|
+
self.bearerTokens.updateOne(token, {
|
|
512
|
+
$unset: {
|
|
513
|
+
requirementsToVerify: 1
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
return {
|
|
517
|
+
token
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
// Implementation detail of the login route and the requirementProps mechanism for
|
|
523
|
+
// custom login requirements. Given the string `token`, returns
|
|
524
|
+
// `{ token, user }`. Throws an exception if the token is not found.
|
|
525
|
+
// `token` is sanitized before passing to mongodb.
|
|
526
|
+
async findIncompleteTokenAndUser(req, token) {
|
|
527
|
+
token = await self.bearerTokens.findOne({
|
|
528
|
+
_id: self.apos.launder.string(token),
|
|
529
|
+
requirementsToVerify: {
|
|
530
|
+
$exists: true,
|
|
531
|
+
$ne: []
|
|
532
|
+
},
|
|
533
|
+
expires: {
|
|
534
|
+
$gte: new Date()
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
if (!token) {
|
|
538
|
+
throw self.apos.error('notfound');
|
|
539
|
+
}
|
|
540
|
+
const user = await self.deserializeUser(token.userId);
|
|
541
|
+
if (!user) {
|
|
542
|
+
await self.bearerTokens.removeOne({
|
|
543
|
+
_id: token._id
|
|
544
|
+
});
|
|
545
|
+
throw self.apos.error('notfound');
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
token,
|
|
549
|
+
user
|
|
550
|
+
};
|
|
551
|
+
},
|
|
552
|
+
|
|
553
|
+
// Implementation detail of the login route. Log in the user, or if there are
|
|
554
|
+
// `requirements` that require password verification occur first, return an incomplete token.
|
|
555
|
+
async initialLogin(req) {
|
|
556
|
+
// Initial login step
|
|
557
|
+
const username = self.apos.launder.string(req.body.username);
|
|
558
|
+
const password = self.apos.launder.string(req.body.password);
|
|
559
|
+
if (!(username && password)) {
|
|
560
|
+
throw self.apos.error('invalid', req.t('apostrophe:loginPageBothRequired'));
|
|
561
|
+
}
|
|
562
|
+
const { earlyRequirements, lateRequirements } = self.filterRequirements();
|
|
563
|
+
for (const [ name, requirement ] of Object.entries(earlyRequirements)) {
|
|
564
|
+
try {
|
|
565
|
+
await requirement.verify(req, req.body.requirements && req.body.requirements[name]);
|
|
566
|
+
} catch (e) {
|
|
567
|
+
e.data = e.data || {};
|
|
568
|
+
e.data.requirement = name;
|
|
569
|
+
throw e;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const user = await self.apos.login.verifyLogin(username, password);
|
|
573
|
+
if (!user) {
|
|
574
|
+
// For security reasons we may not tell the user which case applies
|
|
575
|
+
throw self.apos.error('invalid', req.t('apostrophe:loginPageBadCredentials'));
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const requirementsToVerify = Object.keys(lateRequirements);
|
|
579
|
+
|
|
580
|
+
if (requirementsToVerify.length) {
|
|
581
|
+
const token = cuid();
|
|
582
|
+
|
|
583
|
+
await self.bearerTokens.insert({
|
|
584
|
+
_id: token,
|
|
585
|
+
userId: user._id,
|
|
586
|
+
requirementsToVerify,
|
|
587
|
+
// Default lifetime of 1 hour is generous to permit situations like
|
|
588
|
+
// installing a TOTP app for the first time
|
|
589
|
+
expires: new Date(new Date().getTime() + (self.options.incompleteLifetime || 60 * 60 * 1000))
|
|
590
|
+
});
|
|
591
|
+
return {
|
|
592
|
+
incompleteToken: token
|
|
593
|
+
};
|
|
594
|
+
} else {
|
|
595
|
+
const session = self.apos.launder.boolean(req.body.session);
|
|
596
|
+
if (session) {
|
|
597
|
+
await self.passportLogin(req, user);
|
|
598
|
+
} else {
|
|
599
|
+
const token = cuid();
|
|
600
|
+
await self.bearerTokens.insert({
|
|
601
|
+
_id: token,
|
|
602
|
+
userId: user._id,
|
|
603
|
+
expires: new Date(new Date().getTime() + (self.options.bearerTokens.lifetime || (86400 * 7 * 2)) * 1000)
|
|
604
|
+
});
|
|
605
|
+
return {
|
|
606
|
+
token
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
|
|
612
|
+
filterRequirements() {
|
|
613
|
+
return {
|
|
614
|
+
earlyRequirements: Object.fromEntries(Object.entries(self.requirements).filter(([ name, requirement ]) => requirement.phase === 'beforeSubmit')),
|
|
615
|
+
lateRequirements: Object.fromEntries(Object.entries(self.requirements).filter(([ name, requirement ]) => requirement.phase === 'afterPasswordVerified'))
|
|
616
|
+
};
|
|
617
|
+
},
|
|
618
|
+
|
|
619
|
+
// Awaitable wrapper for req.login. An implementation detail of the login route
|
|
620
|
+
async passportLogin(req, user) {
|
|
621
|
+
const passportLogin = (user) => {
|
|
622
|
+
return require('util').promisify(function(user, callback) {
|
|
623
|
+
return req.login(user, callback);
|
|
624
|
+
})(user);
|
|
625
|
+
};
|
|
626
|
+
await passportLogin(user);
|
|
387
627
|
}
|
|
388
628
|
};
|
|
389
629
|
},
|