apostrophe 4.1.1 → 4.2.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/lib/mongodb-connect.js +1 -1
  3. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarMenu.vue +1 -0
  4. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +22 -16
  5. package/modules/@apostrophecms/area/index.js +1 -1
  6. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +6 -1
  7. package/modules/@apostrophecms/db/index.js +0 -6
  8. package/modules/@apostrophecms/doc/index.js +19 -31
  9. package/modules/@apostrophecms/doc/lib/migrations.js +59 -0
  10. package/modules/@apostrophecms/doc-type/index.js +5 -2
  11. package/modules/@apostrophecms/express/index.js +3 -2
  12. package/modules/@apostrophecms/i18n/i18n/de.json +5 -1
  13. package/modules/@apostrophecms/i18n/i18n/en.json +5 -1
  14. package/modules/@apostrophecms/i18n/i18n/es.json +5 -1
  15. package/modules/@apostrophecms/i18n/i18n/fr.json +5 -1
  16. package/modules/@apostrophecms/i18n/i18n/it.json +5 -1
  17. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +5 -1
  18. package/modules/@apostrophecms/i18n/i18n/sk.json +5 -1
  19. package/modules/@apostrophecms/i18n/index.js +1 -1
  20. package/modules/@apostrophecms/migration/index.js +5 -0
  21. package/modules/@apostrophecms/modal/ui/apos/apps/AposModals.js +2 -2
  22. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +188 -187
  23. package/modules/@apostrophecms/modal/ui/apos/composables/AposFocus.js +2 -2
  24. package/modules/@apostrophecms/notification/index.js +2 -0
  25. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +2 -2
  26. package/modules/@apostrophecms/rich-text-widget/index.js +19 -8
  27. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +44 -15
  28. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapMarks.vue +226 -0
  29. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapStyles.vue +90 -18
  30. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Classes.js +70 -6
  31. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Default.js +2 -1
  32. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Document.js +1 -1
  33. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Heading.js +1 -1
  34. package/modules/@apostrophecms/schema/ui/apos/components/AposInputColor.vue +1 -1
  35. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +1 -1
  36. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputColor.js +4 -0
  37. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputSlug.js +3 -0
  38. package/modules/@apostrophecms/ui/ui/apos/components/AposButtonSplit.vue +2 -2
  39. package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +4 -8
  40. package/modules/@apostrophecms/user/index.js +40 -14
  41. package/modules/@apostrophecms/user/lib/password-hash.js +122 -0
  42. package/modules/@apostrophecms/util/ui/src/http.js +7 -0
  43. package/package.json +3 -5
  44. package/test/docs.js +151 -0
  45. package/test/password-hash.js +56 -0
  46. package/test/users.js +19 -3
  47. package/.github/workflows/outdated-dependencies.yml +0 -43
  48. package/modules/@apostrophecms/modal/ui/apos/mixins/AposFocusMixin.js +0 -91
@@ -34,7 +34,7 @@
34
34
  // their preferred language for the admin UI. It will be added only if
35
35
  // @apostrophecms/i18n is configured with `adminLocales`.
36
36
 
37
- const credentials = require('credentials');
37
+ const passwordHash = require('./lib/password-hash.js');
38
38
  const prompts = require('prompts');
39
39
 
40
40
  module.exports = {
@@ -52,7 +52,20 @@ module.exports = {
52
52
  publishRole: 'admin',
53
53
  viewRole: 'admin',
54
54
  showPermissions: true,
55
- relationshipSuggestionIcon: 'account-box-icon'
55
+ relationshipSuggestionIcon: 'account-box-icon',
56
+ scrypt: {
57
+ // These are the defaults. If you choose to pass
58
+ // this option, you can pass one or more new values.
59
+ // "cost" must be a power of 2. See:
60
+ //
61
+ // https://nodejs.org/api/crypto.html#cryptoscryptpassword-salt-keylen-options-callback
62
+ //
63
+ // Do not pass maxmem, it is calculated automatically.
64
+ //
65
+ // cost: 131072,
66
+ // parallelization: 1,
67
+ // blockSize: 8
68
+ }
56
69
  },
57
70
  fields(self, options) {
58
71
  const fields = {};
@@ -426,11 +439,11 @@ module.exports = {
426
439
  // in `options.secrets` when configuring this module or via
427
440
  // `addSecrets` are not stored as plaintext and are not kept in the
428
441
  // aposDocs collection. Instead, they are hashed and salted using the
429
- // `credential` module and the resulting hash is stored
442
+ // the same algorithm applied to passwords and the resulting hash is stored
430
443
  // in a separate `aposUsersSafe` collection. This method
431
444
  // can be used to verify that `attempt` matches the
432
445
  // previously hashed value for the property named `secret`,
433
- // without ever storing the actual value of the secret
446
+ // without ever storing the actual value of the secret.
434
447
  //
435
448
  // If the secret does not match, an `invalid` error is thrown.
436
449
  // Otherwise the method returns normally.
@@ -440,10 +453,20 @@ module.exports = {
440
453
  if (!safeUser) {
441
454
  throw new Error('No such user in the safe.');
442
455
  }
443
-
444
- const isVerified = await self.pw.verify(migrate(safeUser[secret + 'Hash']), attempt);
456
+ const key = secret + 'Hash';
457
+ const isVerified = await self.pw.verify(migrate(safeUser[key]), attempt);
445
458
 
446
459
  if (isVerified) {
460
+ if ((typeof isVerified) === 'string') {
461
+ // "verify" updated the hash, store the new one
462
+ const $set = {};
463
+ $set[key] = isVerified;
464
+ await self.safe.updateOne({
465
+ _id: user._id
466
+ }, {
467
+ $set
468
+ });
469
+ }
447
470
  return null;
448
471
  } else {
449
472
  throw self.apos.error('invalid', `Incorrect ${secret}`);
@@ -452,8 +475,9 @@ module.exports = {
452
475
  function migrate(json) {
453
476
  const data = JSON.parse(json);
454
477
 
455
- // Do not re-encode salt generated by credentials@3
456
- if (data.credentials3) {
478
+ // * Do not re-encode legacy salt generated by credentials@3
479
+ // * Do not alter salts not generated by the credentials module
480
+ if (data.credentials3 || (data.hashMethod !== 'pbkdf2')) {
457
481
  return json;
458
482
  }
459
483
 
@@ -477,13 +501,15 @@ module.exports = {
477
501
  await self.safe.updateOne({ _id: user._id }, changes);
478
502
  },
479
503
 
480
- // Initialize the [credential](https://npmjs.org/package/credential) module.
504
+ // Initialize password hashing system. Name is for
505
+ // legacy reasons
506
+
481
507
  initializeCredential() {
482
- self.pw = credentials({
483
- // For efficient unit tests only. Reducing the work factor
484
- // for actual credentials increases the speed of brute force attacks
485
- // if the database is ever compromised
486
- work: self.options.insecurePasswords ? 0.01 : 1
508
+ self.pw = passwordHash({
509
+ error(s) {
510
+ return self.apos.error('invalid', s);
511
+ },
512
+ scrypt: self.options.scrypt
487
513
  });
488
514
  },
489
515
 
@@ -0,0 +1,122 @@
1
+ // Password hashing based on scrypt, per:
2
+ // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
3
+ //
4
+ // Also includes legacy support for pbkdf2 passwords.
5
+ //
6
+ // Adapted from the "credential" and "credentials" modules,
7
+ // which were also released under the MIT license.
8
+
9
+ const util = require('util');
10
+ const crypto = require('crypto');
11
+
12
+ const scrypt = util.promisify(crypto.scrypt);
13
+ const pbkdf2 = util.promisify(crypto.pbkdf2);
14
+ const randomBytes = util.promisify(crypto.randomBytes);
15
+ const timingSafeEqual = crypto.timingSafeEqual;
16
+
17
+ function getScryptOptions(options) {
18
+ const result = {
19
+ cost: 131072,
20
+ parallelization: 1,
21
+ blockSize: 8,
22
+ ...options
23
+ };
24
+ // Per https://github.com/nodejs/node/issues/21524
25
+ // Without this the parameters are rejected as soon as we
26
+ // exceed the default cost of 16384
27
+ result.maxmem = 128 * result.parallelization * result.blockSize + 128 * (2 + result.cost) * result.blockSize;
28
+ return result;
29
+ }
30
+
31
+ configure.hash = hash;
32
+ configure.verify = verify;
33
+
34
+ module.exports = configure;
35
+
36
+ function configure(opts) {
37
+ opts = {
38
+ keyLength: 64,
39
+ ...opts,
40
+ scrypt: getScryptOptions(opts.scrypt)
41
+ };
42
+ return {
43
+ hash: password => hash(password, opts),
44
+ verify: (stored, input) => verify(stored, input, opts)
45
+ };
46
+ }
47
+
48
+ async function hash(password, opts) {
49
+ const { keyLength } = opts;
50
+
51
+ if (typeof password !== 'string' || password.length === 0) {
52
+ throw opts.error('Password must be a non-empty string.');
53
+ }
54
+
55
+ const salt = await randomBytes(keyLength);
56
+ const hash = await scrypt(password, salt, keyLength, opts.scrypt);
57
+
58
+ return JSON.stringify({
59
+ hashMethod: 'scrypt',
60
+ salt: salt.toString('base64'),
61
+ hash: hash.toString('base64'),
62
+ keyLength,
63
+ scrypt: opts.scrypt
64
+ });
65
+ }
66
+
67
+ async function verify(stored, input, opts) {
68
+ const parsed = parse(stored);
69
+
70
+ const {
71
+ hashMethod, keyLength, salt, hash: hashA
72
+ } = parse(stored);
73
+
74
+ if (typeof input !== 'string' || input.length === 0) {
75
+ throw opts.error('Input password must be a non-empty string.');
76
+ }
77
+ if (!hashMethod) {
78
+ throw opts.error('Couldn\'t parse stored hash.');
79
+ }
80
+ let hashB;
81
+ if (hashMethod === 'scrypt') {
82
+ // Use scrypt as a more modern but also safely portable
83
+ // solution in Node.js
84
+ const { scrypt: scryptOptions } = parsed;
85
+ // Calculate maxmem to make sure we still have the resources
86
+ // if this password was hashed with a higher cost factor
87
+ // than the one we are using for new passwords
88
+ hashB = await scrypt(input, salt, keyLength, getScryptOptions(scryptOptions));
89
+ } else {
90
+ // Support existing pbkdf2 hashes from credentials module
91
+ const { iterations } = parsed;
92
+ const dfn = hashMethod.slice(0, 6);
93
+ const hfn = hashMethod.slice(7) || 'sha1';
94
+ if (dfn !== 'pbkdf2') {
95
+ throw opts.error('Unsupported key derivation function');
96
+ }
97
+ if (![ 'sha1', 'sha512' ].includes(hfn)) {
98
+ throw opts.error('Unsupported hash function');
99
+ }
100
+ hashB = await pbkdf2(input, salt, iterations, keyLength, hfn);
101
+ }
102
+ const equal = timingSafeEqual(hashA, hashB);
103
+ if (equal && (hashMethod !== 'scrypt')) {
104
+ // Modernize legacy hashes on next login
105
+ return hash(input, opts);
106
+ } else {
107
+ return equal;
108
+ }
109
+ }
110
+
111
+ function parse(stored) {
112
+ try {
113
+ const parsed = JSON.parse(stored);
114
+ return {
115
+ ...parsed,
116
+ salt: Buffer.from(parsed.salt, 'base64'),
117
+ hash: Buffer.from(parsed.hash, 'base64')
118
+ };
119
+ } catch (err) {
120
+ return {};
121
+ }
122
+ }
@@ -99,6 +99,9 @@ export default () => {
99
99
  throw new Error('If you wish to receive a promise from apos.http methods in older browsers you must have a Promise polyfill. If you do not want to provide one, pass a callback instead.');
100
100
  }
101
101
  return new window.Promise(function(resolve, reject) {
102
+ if (!url) {
103
+ return reject(new Error('url is not defined'));
104
+ }
102
105
  return apos.http.remote(method, url, options, function(err, result) {
103
106
  if (err) {
104
107
  return reject(err);
@@ -108,6 +111,10 @@ export default () => {
108
111
  });
109
112
  }
110
113
 
114
+ if (!url) {
115
+ return callback(new Error('url is not defined'));
116
+ }
117
+
111
118
  if (apos.prefix && options.prefix !== false) {
112
119
  // Prepend the prefix if the URL is absolute:
113
120
  if (url.substring(0, 1) === '/') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "4.1.1",
3
+ "version": "4.2.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -31,6 +31,7 @@
31
31
  "author": "Apostrophe Technologies, Inc.",
32
32
  "license": "MIT",
33
33
  "dependencies": {
34
+ "@apostrophecms/emulate-mongo-3-driver": "^1.0.2",
34
35
  "@apostrophecms/vue-material-design-icons": "^1.0.0",
35
36
  "@ckpack/vue-color": "^1.4.1",
36
37
  "@floating-ui/dom": "^1.5.3",
@@ -51,7 +52,6 @@
51
52
  "@tiptap/extension-underline": "^2.0.3",
52
53
  "@tiptap/starter-kit": "^2.0.3",
53
54
  "@tiptap/vue-3": "^2.0.3",
54
- "@vue/compat": "^3.3.8",
55
55
  "@vue/compiler-sfc": "^3.3.8",
56
56
  "autoprefixer": "^10.4.1",
57
57
  "bluebird": "^3.7.2",
@@ -61,11 +61,10 @@
61
61
  "cheerio": "^1.0.0-rc.10",
62
62
  "chokidar": "^3.5.2",
63
63
  "common-tags": "^1.8.0",
64
- "connect-mongo": "^3.0.0",
64
+ "connect-mongo": "^5.1.0",
65
65
  "connect-multiparty": "^2.1.1",
66
66
  "cookie-parser": "^1.4.5",
67
67
  "cors": "^2.8.5",
68
- "credentials": "^3.0.2",
69
68
  "css-loader": "^5.2.4",
70
69
  "cuid": "^2.1.8",
71
70
  "dayjs": "^1.9.8",
@@ -91,7 +90,6 @@
91
90
  "mini-css-extract-plugin": "^1.6.0",
92
91
  "minimatch": "^3.0.4",
93
92
  "mkdirp": "^0.5.5",
94
- "mongodb": "^3.6.6",
95
93
  "node-fetch": "^2.6.1",
96
94
  "nodemailer": "^6.6.1",
97
95
  "nunjucks": "^3.2.1",
package/test/docs.js CHANGED
@@ -1047,6 +1047,157 @@ describe('Docs', function() {
1047
1047
  assert(response.cacheInvalidatedAt.getTime() === response.updatedAt.getTime());
1048
1048
  assert(draft.cacheInvalidatedAt.getTime() === draft.updatedAt.getTime());
1049
1049
  });
1050
+
1051
+ describe('beforeInsert handler', function() {
1052
+ it('should rely on req.mode when inserting a doc without _id', async function() {
1053
+ const req = apos.task.getReq();
1054
+ const draftReq = apos.task.getReq({ mode: 'draft' });
1055
+ const people = apos.modules['test-people'];
1056
+ const instance = people.newInstance();
1057
+
1058
+ const piece1 = {
1059
+ ...instance,
1060
+ title: 'piece 1'
1061
+ };
1062
+
1063
+ const piece2 = {
1064
+ ...instance,
1065
+ title: 'piece 2'
1066
+ };
1067
+
1068
+ const pieceDraft = await people.insert(draftReq, piece1);
1069
+ const piecePublished = await people.insert(req, piece2);
1070
+
1071
+ const actual = {
1072
+ draft: {
1073
+ idMode: pieceDraft._id.split(':').pop(),
1074
+ aposLocale: pieceDraft.aposLocale,
1075
+ aposMode: pieceDraft.aposMode
1076
+ },
1077
+ published: {
1078
+ idMode: piecePublished._id.split(':').pop(),
1079
+ aposLocale: piecePublished.aposLocale,
1080
+ aposMode: piecePublished.aposMode
1081
+ }
1082
+ };
1083
+
1084
+ const expected = {
1085
+ draft: {
1086
+ idMode: 'draft',
1087
+ aposLocale: 'en:draft',
1088
+ aposMode: 'draft'
1089
+ },
1090
+ published: {
1091
+ idMode: 'published',
1092
+ aposLocale: 'en:published',
1093
+ aposMode: 'published'
1094
+ }
1095
+ };
1096
+
1097
+ assert.deepEqual(actual, expected);
1098
+ });
1099
+
1100
+ it('should rely on _id when present for aposMode and aposLocale even if req.mode does not match', async function() {
1101
+ const req = apos.task.getReq();
1102
+ const draftReq = apos.task.getReq({ mode: 'draft' });
1103
+ const people = apos.modules['test-people'];
1104
+ const instance = people.newInstance();
1105
+
1106
+ const piece1 = {
1107
+ _id: 'testid:en:draft',
1108
+ ...instance,
1109
+ title: 'piece 1'
1110
+ };
1111
+
1112
+ const piece2 = {
1113
+ _id: 'testid:en:published',
1114
+ ...instance,
1115
+ title: 'piece 2'
1116
+ };
1117
+
1118
+ const pieceDraft = await people.insert(req, piece1);
1119
+ const piecePublished = await people.insert(draftReq, piece2);
1120
+
1121
+ const actual = {
1122
+ draft: {
1123
+ idMode: pieceDraft._id.split(':').pop(),
1124
+ aposLocale: pieceDraft.aposLocale,
1125
+ aposMode: pieceDraft.aposMode
1126
+ },
1127
+ published: {
1128
+ idMode: piecePublished._id.split(':').pop(),
1129
+ aposLocale: piecePublished.aposLocale,
1130
+ aposMode: piecePublished.aposMode
1131
+ }
1132
+ };
1133
+
1134
+ const expected = {
1135
+ draft: {
1136
+ idMode: 'draft',
1137
+ aposLocale: 'en:draft',
1138
+ aposMode: 'draft'
1139
+ },
1140
+ published: {
1141
+ idMode: 'published',
1142
+ aposLocale: 'en:published',
1143
+ aposMode: 'published'
1144
+ }
1145
+ };
1146
+
1147
+ assert.deepEqual(actual, expected);
1148
+ });
1149
+
1150
+ it('should rely on aposMode when present for _id and aposLocale even if req.mode does not match', async function() {
1151
+ const req = apos.task.getReq();
1152
+ const draftReq = apos.task.getReq({ mode: 'draft' });
1153
+ const people = apos.modules['test-people'];
1154
+ const instance = people.newInstance();
1155
+
1156
+ const piece1 = {
1157
+ ...instance,
1158
+ title: 'piece 1',
1159
+ aposLocale: 'en:draft'
1160
+ };
1161
+
1162
+ const piece2 = {
1163
+ ...instance,
1164
+ title: 'piece 2',
1165
+ aposLocale: 'en:published'
1166
+ };
1167
+
1168
+ const pieceDraft = await people.insert(req, piece1);
1169
+ const piecePublished = await people.insert(draftReq, piece2);
1170
+
1171
+ const actual = {
1172
+ draft: {
1173
+ idMode: pieceDraft._id.split(':').pop(),
1174
+ aposLocale: pieceDraft.aposLocale,
1175
+ aposMode: pieceDraft.aposMode
1176
+ },
1177
+ published: {
1178
+ idMode: piecePublished._id.split(':').pop(),
1179
+ aposLocale: piecePublished.aposLocale,
1180
+ aposMode: piecePublished.aposMode
1181
+ }
1182
+ };
1183
+
1184
+ const expected = {
1185
+ draft: {
1186
+ idMode: 'draft',
1187
+ aposLocale: 'en:draft',
1188
+ aposMode: 'draft'
1189
+ },
1190
+ published: {
1191
+ idMode: 'published',
1192
+ aposLocale: 'en:published',
1193
+ aposMode: 'published'
1194
+ }
1195
+ };
1196
+
1197
+ assert.deepEqual(actual, expected);
1198
+ });
1199
+ });
1200
+
1050
1201
  });
1051
1202
 
1052
1203
  async function insertPeople(apos) {
@@ -0,0 +1,56 @@
1
+ const assert = require('assert');
2
+ const passwordHash = require('../modules/@apostrophecms/user/lib/password-hash.js');
3
+
4
+ const legacyHash = '{"hashMethod":"pbkdf2-sha512","salt":"JEB7TX4iOky4kWy+1xsGlN0u7GpEtEoUxmRzaf0Oi35A5j9ynYZfT1Lk4JofBz5nbAHD4HoMqQnevltTLd4Hbw==","hash":"aFM6axOnaPiwNGly7NsfYEvFHEv1ML4lNyi2nEz95tudK1/M1PUlMbtxujZ+W1Gv8Q2mHh7KnL6Ql94OOL8S0g==","keyLength":64,"iterations":4449149,"credentials3":true}';
5
+
6
+ describe('password-hash', function() {
7
+ it('can hash a password', async function() {
8
+ const instance = getInstance();
9
+ const hash = await instance.hash('test one');
10
+ assert(hash);
11
+ });
12
+ it('can verify a correct password', async function() {
13
+ const instance = getInstance();
14
+ const hash = await instance.hash('test one');
15
+ assert(await instance.verify(hash, 'test one'));
16
+ });
17
+ it('cannot verify an incorrect password', async function() {
18
+ const instance = getInstance();
19
+ const hash = await instance.hash('test one');
20
+ assert(!await instance.verify(hash, 'test two'));
21
+ });
22
+ it('hash does not contain password and uses scrypt with parameters', async function() {
23
+ const instance = getInstance();
24
+ const hash = JSON.parse(await instance.hash('test one'));
25
+ assert(!JSON.stringify(hash).includes('test one'));
26
+ assert.strictEqual(hash.hashMethod, 'scrypt');
27
+ assert(hash.scrypt);
28
+ assert.strictEqual(hash.scrypt.cost, 131072);
29
+ assert.strictEqual(hash.scrypt.blockSize, 8);
30
+ assert.strictEqual(hash.scrypt.parallelization, 1);
31
+ });
32
+ it('can verify and modernize a legacy pbkdf2 password hash', async function() {
33
+ this.timeout(10000);
34
+ const instance = getInstance();
35
+ const hash = await instance.verify(legacyHash, 'test-password');
36
+ assert(hash);
37
+ assert.strictEqual(typeof hash, 'string');
38
+ const data = JSON.parse(hash);
39
+ assert.strictEqual(data.hashMethod, 'scrypt');
40
+ assert.strictEqual(await instance.verify(hash, 'test-password'), true);
41
+ assert.strictEqual(await instance.verify(hash, 'bogus-password'), false);
42
+ });
43
+ it('can reject a bad password for a legacy pbkdf2 hash', async function() {
44
+ this.timeout(10000);
45
+ const instance = getInstance();
46
+ assert(!await instance.verify(legacyHash, 'bad-password'));
47
+ });
48
+ });
49
+
50
+ function getInstance() {
51
+ return passwordHash({
52
+ error(s) {
53
+ return new Error(s);
54
+ }
55
+ });
56
+ }
package/test/users.js CHANGED
@@ -99,7 +99,7 @@ describe('Users', function() {
99
99
  }
100
100
  });
101
101
 
102
- it('should verify a user password created with former credential package', async function() {
102
+ it('should verify a user password created with former credential package and also upgrade the hash', async function() {
103
103
  const req = apos.task.getReq();
104
104
  const user = apos.user.newInstance();
105
105
 
@@ -115,9 +115,25 @@ describe('Users', function() {
115
115
  const oldPasswordHashSimulated =
116
116
  '{"hash":"/1GntJjtkMY1iPmQY1gn9f3bOZ5tb2qFL+x4qsDerZq2JL8+12TERR4/xqh246wBb+QJwwIRsF/6E+eccshsLxT/","salt":"GJHukLNaG6xDgdIpxVOpqV7xQLQM7e5xnhDW7oaUOe7mTicr7Ca76M4uUJalN/cQ68CE9O7yXZ5WJOz4RN/udcX0","keyLength":66,"hashMethod":"pbkdf2","iterations":2853053}';
117
117
 
118
- await apos.user.safe.update({ username: 'olduser' }, { $set: { passwordHash: oldPasswordHashSimulated } });
118
+ await apos.user.safe.updateOne({ username: 'olduser' }, { $set: { passwordHash: oldPasswordHashSimulated } });
119
119
  await apos.user.verifyPassword(user, 'passwordThatWentThroughOldCredentialPackageHashing');
120
- await apos.user.safe.remove({ username: 'olduser' });
120
+
121
+ // verifyPassword now upgrades legacy hashes on next use, e.g.
122
+ // the next time it is possible because the password is known
123
+ const newHash = JSON.parse((await apos.user.safe.findOne({
124
+ username: 'olduser'
125
+ })).passwordHash);
126
+ assert.strictEqual(newHash.hashMethod, 'scrypt');
127
+ // Confirm the modernized end result is still verifiable with the old password
128
+ await apos.user.verifyPassword(user, 'passwordThatWentThroughOldCredentialPackageHashing');
129
+ try {
130
+ // ... And not with a bogus one
131
+ await apos.user.verifyPassword(user, 'bogus');
132
+ assert(false);
133
+ } catch (e) {
134
+ // Good
135
+ }
136
+ await apos.user.safe.removeOne({ username: 'olduser' });
121
137
  });
122
138
 
123
139
  it('should not be able to insert a new user if their email already exists', async function() {
@@ -1,43 +0,0 @@
1
- name: Check outdated dependencies
2
- on:
3
- schedule:
4
- # Runs every Monday at 8:00
5
- - cron: "0 8 * * MON"
6
- # Allows you to run this workflow manually from the Actions tab
7
- workflow_dispatch:
8
-
9
- jobs:
10
- check_outdated_dependencies:
11
- runs-on: ubuntu-latest
12
- steps:
13
- - name: Git checkout
14
- uses: actions/checkout@v2
15
- - name: Use Node.js 18
16
- uses: actions/setup-node@v1
17
- with:
18
- node-version: 18
19
- - name: Install dependencies
20
- run: npm install
21
- - name: Check outdated dependencies
22
- run: |
23
- echo "$(npm outdated)" > output
24
- npm outdated
25
- - name: Report Status
26
- if: failure()
27
- run: |
28
- outdated_dependencies=$(cat output)
29
-
30
- repo="${{ github.repository }}"
31
- repo_url="${{ github.server_url }}/$repo"
32
- run_url="$repo_url/actions/runs/${{ github.run_id }}"
33
-
34
- text="ℹ️ The <$repo_url|$repo> project has outdated dependencies:
35
- \`\`\`
36
- $outdated_dependencies
37
- \`\`\`
38
- <$run_url|View Run>"
39
-
40
- payload="{\"text\": \"$text\"}"
41
- curl -X POST -H 'Content-type: application/json' --data "$payload" $SLACK_WEBHOOK_URL
42
- env:
43
- SLACK_WEBHOOK_URL: ${{ secrets.ACTION_MONITORING_SLACK }}
@@ -1,91 +0,0 @@
1
- /*
2
- * Provides:
3
- *
4
- * Methods to handle focus with keyboard.
5
- */
6
- export default {
7
- data() {
8
- return {
9
- elementsToFocus: [],
10
-
11
- // specific to modals:
12
- focusedElement: null
13
- };
14
- },
15
- methods: {
16
- // Adapted from https://uxdesign.cc/how-to-trap-focus-inside-modal-to-make-it-ada-compliant-6a50f9a70700
17
- // All the elements inside modal which you want to make focusable.
18
- //
19
- // This has been adapted to Vue logic with `this.elementsToFocus` array as a data
20
- // so that any elements, not only from a modal but a menu for instance, can be focusable.
21
- // `cycleElementsToFocus` listeners relies on this dynamic list which has the advantage of
22
- // taking new or less elements to focus, after an update has happened inside a modal,
23
- // like an XHR call to get the pieces list in the AposDocsManager modal, for instance.
24
- cycleElementsToFocus(e) {
25
- if (!this.elementsToFocus.length) {
26
- return;
27
- }
28
-
29
- const isTabPressed = e.key === 'Tab' || e.code === 'Tab';
30
- if (!isTabPressed) {
31
- return;
32
- }
33
-
34
- const firstElementToFocus = this.elementsToFocus.at(0);
35
- const lastElementToFocus = this.elementsToFocus.at(-1);
36
-
37
- // If shift key pressed for shift + tab combination
38
- if (e.shiftKey) {
39
- if (document.activeElement === firstElementToFocus) {
40
- // Add focus for the last focusable element
41
- lastElementToFocus.focus();
42
- e.preventDefault();
43
- }
44
- return;
45
- }
46
-
47
- // If tab key is pressed
48
- if (document.activeElement === lastElementToFocus) {
49
- // Add focus for the first focusable element
50
- firstElementToFocus.focus();
51
- e.preventDefault();
52
- }
53
- },
54
- // Focus the last focused element from the last modal.
55
- // If it is not focusable (not visible/not in the DOM),
56
- // fallbacks to the first focusable element from the last modal.
57
- focusLastModalFocusedElement() {
58
- const lastModal = apos.modal.stack.at(-1);
59
-
60
- if (!lastModal) {
61
- return;
62
- }
63
-
64
- const { focusedElement, elementsToFocus } = lastModal;
65
-
66
- this.focusElement(focusedElement, elementsToFocus[0]);
67
- },
68
- storeFocusedElement(e) {
69
- this.focusedElement = e.target;
70
- },
71
- // Iterate through elements given in arguments and
72
- // focus the first element that exists in the DOM.
73
- focusElement(...elementsToFocus) {
74
- for (const element of elementsToFocus) {
75
- const isAlreadySelected = document.activeElement === element;
76
-
77
- if (!element || !this.isElementVisible(element)) {
78
- continue;
79
- }
80
- if (!isAlreadySelected) {
81
- element.focus();
82
- }
83
- // Element exists in the DOM and is focused, stop iterating.
84
- return;
85
- }
86
- },
87
- isElementVisible(element) {
88
- return element.offsetParent !== null;
89
- }
90
- }
91
- };