apostrophe 3.4.0 → 3.7.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 (87) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +1 -1
  3. package/deploy-test-count +1 -1
  4. package/index.js +125 -5
  5. package/lib/moog-require.js +41 -3
  6. package/lib/moog.js +20 -8
  7. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +42 -23
  8. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +25 -13
  9. package/modules/@apostrophecms/area/index.js +9 -0
  10. package/modules/@apostrophecms/area/lib/custom-tags/area.js +1 -1
  11. package/modules/@apostrophecms/area/lib/custom-tags/widget.js +1 -1
  12. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +3 -0
  13. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +6 -6
  14. package/modules/@apostrophecms/asset/index.js +8 -8
  15. package/modules/@apostrophecms/asset/lib/globalIcons.js +2 -0
  16. package/modules/@apostrophecms/asset/lib/webpack/src/webpack.scss.js +5 -2
  17. package/modules/@apostrophecms/doc/index.js +13 -3
  18. package/modules/@apostrophecms/doc-type/index.js +1 -1
  19. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +3 -0
  20. package/modules/@apostrophecms/i18n/i18n/en.json +11 -2
  21. package/modules/@apostrophecms/i18n/i18n/es.json +383 -0
  22. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +380 -0
  23. package/modules/@apostrophecms/i18n/i18n/sk.json +381 -0
  24. package/modules/@apostrophecms/i18n/index.js +10 -1
  25. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +153 -121
  26. package/modules/@apostrophecms/image/index.js +2 -1
  27. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +24 -13
  28. package/modules/@apostrophecms/login/index.js +36 -17
  29. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +8 -0
  30. package/modules/@apostrophecms/migration/index.js +1 -1
  31. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +6 -2
  32. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +1 -1
  33. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +6 -0
  34. package/modules/@apostrophecms/module/index.js +1 -1
  35. package/modules/@apostrophecms/permission/index.js +1 -1
  36. package/modules/@apostrophecms/permission/ui/apos/components/AposInputRole.vue +4 -2
  37. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +4 -1
  38. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +1 -1
  39. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +42 -10
  40. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapStyles.vue +3 -0
  41. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Classes.js +6 -10
  42. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Default.js +64 -0
  43. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Document.js +15 -0
  44. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Heading.js +23 -0
  45. package/modules/@apostrophecms/schema/index.js +97 -20
  46. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +1 -0
  47. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +4 -1
  48. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRadio.vue +8 -5
  49. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +24 -2
  50. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +24 -6
  51. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +0 -4
  52. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +0 -7
  53. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +25 -3
  54. package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputMixin.js +10 -2
  55. package/modules/@apostrophecms/template/index.js +61 -36
  56. package/modules/@apostrophecms/template/lib/custom-tags/component.js +1 -1
  57. package/modules/@apostrophecms/template/lib/custom-tags/render.js +6 -2
  58. package/modules/@apostrophecms/ui/index.js +6 -2
  59. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +16 -3
  60. package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +1 -1
  61. package/modules/@apostrophecms/ui/ui/apos/components/AposIndicator.vue +5 -0
  62. package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +16 -2
  63. package/modules/@apostrophecms/ui/ui/apos/scss/global/_tables.scss +4 -3
  64. package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +3 -0
  65. package/modules/@apostrophecms/ui/ui/apos/scss/global/_widgets.scss +3 -0
  66. package/modules/@apostrophecms/ui/ui/apos/scss/global/import-all.scss +2 -1
  67. package/modules/@apostrophecms/user/index.js +21 -0
  68. package/modules/@apostrophecms/util/index.js +2 -2
  69. package/modules/@apostrophecms/util/ui/src/http.js +12 -8
  70. package/modules/@apostrophecms/widget-type/index.js +1 -1
  71. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +1 -0
  72. package/package.json +3 -3
  73. package/test/extra_node_modules/improve-global/index.js +7 -0
  74. package/test/extra_node_modules/improve-piece-type/index.js +7 -0
  75. package/test/improve-overrides.js +30 -0
  76. package/test/login.js +183 -0
  77. package/test/modules/@apostrophecms/global/index.js +8 -0
  78. package/test/modules/fragment-all/views/aux-test.html +7 -0
  79. package/test/modules/fragment-all/views/fragment.html +5 -0
  80. package/test/moog.js +47 -0
  81. package/test/package.json +5 -4
  82. package/test/reverse-relationship.js +170 -0
  83. package/test/subdir-project/app.js +3 -0
  84. package/test/subdir-project.js +26 -0
  85. package/test/templates.js +7 -1
  86. package/test-lib/test.js +23 -12
  87. package/test-lib/util.js +1 -0
@@ -98,9 +98,8 @@ module.exports = (self) => {
98
98
  const source = info.body();
99
99
  const input = createRenderInput(info);
100
100
 
101
- const req = context.env.opts.req;
101
+ const req = context.ctx.__req;
102
102
  const env = self.getEnv(req, context.env.opts.module);
103
- input.apos = self.templateApos;
104
103
 
105
104
  // attach the render caller as a function
106
105
  // it's just a string, but we keep
@@ -108,6 +107,11 @@ module.exports = (self) => {
108
107
  input.rendercaller = rendercaller;
109
108
 
110
109
  const result = await require('util').promisify((s, args, callback) => {
110
+ args = {
111
+ ...self.getRenderArgs(req, {}, context.env.opts.module),
112
+ // Parameters to fragments are top level, they are not in `data`
113
+ ...args
114
+ };
111
115
  return env.renderString(s, args, callback);
112
116
  })(source, input);
113
117
  return result;
@@ -1,6 +1,7 @@
1
1
  module.exports = {
2
2
  options: {
3
- alias: 'ui'
3
+ alias: 'ui',
4
+ widgetMargin: '20px 0'
4
5
  },
5
6
  icons: {
6
7
  'earth-icon': 'Earth',
@@ -21,7 +22,10 @@ module.exports = {
21
22
  if (req.data.user && req.data.user.aposThemePrimary) {
22
23
  theme.primary = req.data.user.aposThemePrimary;
23
24
  }
24
- return { theme };
25
+ return {
26
+ theme,
27
+ widgetMargin: self.options.widgetMargin
28
+ };
25
29
  }
26
30
  };
27
31
  }
@@ -1,5 +1,9 @@
1
1
  <template>
2
- <span v-apos-tooltip="tooltip" class="apos-button__wrapper">
2
+ <span
3
+ v-apos-tooltip="tooltip"
4
+ class="apos-button__wrapper"
5
+ :class="{ 'apos-button__wrapper--block': modifiers.includes('block') }"
6
+ >
3
7
  <component
4
8
  :is="href ? 'a' : 'button'"
5
9
  v-on="href ? {} : {click: click}"
@@ -332,6 +336,9 @@ export default {
332
336
  color: var(--a-base-5);
333
337
  }
334
338
  }
339
+ .apos-button__label {
340
+ line-height: var(--a-line-tall);
341
+ }
335
342
  }
336
343
 
337
344
  .apos-button--subtle {
@@ -395,8 +402,6 @@ export default {
395
402
  box-sizing: border-box;
396
403
  display: block;
397
404
  width: 100%;
398
- height: 47px;
399
- max-width: 400px;
400
405
  }
401
406
 
402
407
  .apos-button--icon-right {
@@ -574,6 +579,10 @@ export default {
574
579
  padding: 3px;
575
580
  }
576
581
 
582
+ .apos-button--uppercase .apos-button__label {
583
+ text-transform: uppercase;
584
+ }
585
+
577
586
  .apos-button--inline {
578
587
  padding: 0;
579
588
  &, &[disabled], &:hover, &:active, &:focus {
@@ -605,6 +614,10 @@ export default {
605
614
  display: inline-block;
606
615
  }
607
616
 
617
+ .apos-button__wrapper--block {
618
+ display: block;
619
+ }
620
+
608
621
  @keyframes animateGradient {
609
622
  0% {
610
623
  background-position: 0% 50%;
@@ -64,7 +64,7 @@ export default {
64
64
  .apos-table__cell-field--context-menu__content {
65
65
  @include apos-transition();
66
66
  display: inline-block;
67
- opacity: 0;
67
+ opacity: 0.3;
68
68
  &.apos-is-visible {
69
69
  opacity: 1;
70
70
  }
@@ -7,6 +7,7 @@
7
7
  <component
8
8
  :is="icon"
9
9
  :size="iconSize"
10
+ :title="title ? title : ''"
10
11
  class="apos-indicator__icon"
11
12
  :fill-color="iconColor"
12
13
  />
@@ -30,6 +31,10 @@ export default {
30
31
  type: [ String, Object, Boolean ],
31
32
  default: false
32
33
  },
34
+ title: {
35
+ type: [ String, Boolean ],
36
+ default: false
37
+ },
33
38
  iconColor: {
34
39
  type: String,
35
40
  default: 'currentColor'
@@ -10,11 +10,20 @@ export default {
10
10
  install(Vue, options) {
11
11
  const i18n = options.i18n;
12
12
 
13
+ const fallbackLng = [ i18n.defaultLocale ];
14
+ // In case the default locale also has inadequate admin UI phrases
15
+ if (fallbackLng[0] !== 'en') {
16
+ fallbackLng.push('en');
17
+ }
18
+
13
19
  i18next.init({
14
20
  lng: i18n.locale,
15
- fallbackLng: i18n.defaultLocale,
21
+ fallbackLng,
16
22
  resources: {},
17
- debug: i18n.debug
23
+ debug: i18n.debug,
24
+ interpolation: {
25
+ escapeValue: false
26
+ }
18
27
  });
19
28
 
20
29
  for (const [ ns, phrases ] of Object.entries(i18n.i18n[i18n.locale])) {
@@ -25,6 +34,11 @@ export default {
25
34
  i18next.addResourceBundle(i18n.defaultLocale, ns, phrases, true, true);
26
35
  }
27
36
  }
37
+ if ((i18n.locale !== 'en') && (i18n.defaultLocale !== 'en')) {
38
+ for (const [ ns, phrases ] of Object.entries(i18n.i18n.en)) {
39
+ i18next.addResourceBundle('en', ns, phrases, true, true);
40
+ }
41
+ }
28
42
 
29
43
  // Like standard i18next $t, but also with support
30
44
  // for just one object argument with at least a `key`
@@ -5,7 +5,7 @@
5
5
 
6
6
  .apos-table__header {
7
7
  margin-bottom: $spacing-base;
8
- padding: 12.5px 4.5px;
8
+ padding: 12.5px 15px;
9
9
  border-bottom: 1px solid var(--a-base-8);
10
10
  color: var(--a-base-3);
11
11
  text-align: left;
@@ -52,12 +52,13 @@ span.apos-table__header-label:hover {
52
52
  @include apos-transition(all, 0.05s);
53
53
  }
54
54
  .apos-table__cell {
55
- padding: 5px;
55
+ padding: 5px 15px;
56
56
  border-bottom: 1px solid var(--a-base-10);
57
57
  }
58
58
 
59
59
  .apos-table__cell--context-menu {
60
- width: 40px;
60
+ padding-right: 0;
61
+ padding-left: 0;
61
62
  }
62
63
 
63
64
  .apos-table__cell-field--context-menu {
@@ -78,6 +78,9 @@
78
78
  --a-line-base: 1;
79
79
  --a-line-tall: 1.3;
80
80
  --a-line-tallest: 1.6;
81
+
82
+ // Admin styles
83
+ --a-widget-margin: 0;
81
84
  }
82
85
 
83
86
  .apos-theme-dark {
@@ -0,0 +1,3 @@
1
+ [data-apos-area] [data-apos-area] {
2
+ margin: var(--a-widget-margin);
3
+ }
@@ -9,4 +9,5 @@
9
9
  @import './_scrollbars';
10
10
  @import './_utilities';
11
11
  @import './_tables';
12
- @import './_tooltips';
12
+ @import './_tooltips';
13
+ @import './_widgets';
@@ -185,6 +185,19 @@ module.exports = {
185
185
  if (![ 'guest', 'editor', 'contributor', 'admin' ].includes(doc.role)) {
186
186
  throw self.apos.error('invalid', 'The role property of a user must be guest, editor, contributor or admin');
187
187
  }
188
+ },
189
+ async invalidatePriorLogins(req, doc, options) {
190
+ const effectiveUserId = req.user && req.user._id;
191
+ // Invalidate prior login sessions if the password field changes or
192
+ // the user is newly marked as disabled.
193
+ if (doc._id && doc._passwordUpdated && (effectiveUserId !== doc._id)) {
194
+ // Invalidate old sessions
195
+ doc.loginInvalidBefore = Date.now();
196
+ // Just delete old bearer tokens
197
+ return self.apos.login.bearerTokens.removeMany({
198
+ userId: doc._id
199
+ });
200
+ }
188
201
  }
189
202
  },
190
203
  // Reflect email and username changes in the safe after deduplicating in the piece
@@ -347,6 +360,13 @@ module.exports = {
347
360
  // alone and `safeUser` is not updated.
348
361
  //
349
362
  // Called automatically by `hashSecrets`, above.
363
+ //
364
+ // The secret property itself is immediately deleted from doc
365
+ // to avoid any risk of accidentally storing it in cleartext.
366
+ // However there is a way to detect that it was updated:
367
+ // if `secret` is `password`, then the `_passwordUpdated` temporary
368
+ // property is set to true. This provides a way to take additional
369
+ // actions stemming from this change in a `beforeSave` handler, etc.
350
370
 
351
371
  async hashSecret(doc, safeUser, secret) {
352
372
  if (!doc[secret]) {
@@ -355,6 +375,7 @@ module.exports = {
355
375
  const hash = await require('util').promisify(self.pw.hash)(doc[secret]);
356
376
  delete doc[secret];
357
377
  safeUser[secret + 'Hash'] = hash;
378
+ doc[`_${secret}Updated`] = true;
358
379
  },
359
380
 
360
381
  // Verify the given password by checking it against the
@@ -414,7 +414,7 @@ module.exports = {
414
414
  return self.insensitiveSortCompare(a[property], b[property]);
415
415
  });
416
416
  },
417
- // Copmpare two strings in a case-insensitive way, returning -1, 0 or 1, suitable for use with sort().
417
+ // Compare two strings in a case-insensitive way, returning -1, 0 or 1, suitable for use with sort().
418
418
  // If the two strings represent numbers, compare them as numbers for a natural sort order
419
419
  // when comparing strings like '4' and '10'.
420
420
  insensitiveSortCompare(a, b) {
@@ -858,7 +858,7 @@ module.exports = {
858
858
  return _.startCase(o);
859
859
  },
860
860
 
861
- // check if something is a function (as opposd to property)
861
+ // check if something is a function (as opposed to property)
862
862
  isFunction: function(o) {
863
863
  return (typeof o === 'function');
864
864
  },
@@ -150,10 +150,12 @@ export default () => {
150
150
  if (options.busy) {
151
151
  if (!busyActive[busyName]) {
152
152
  busyActive[busyName] = 0;
153
- apos.bus.$emit('busy', {
154
- active: true,
155
- name: busyName
156
- });
153
+ if (apos.bus) {
154
+ apos.bus.$emit('busy', {
155
+ active: true,
156
+ name: busyName
157
+ });
158
+ }
157
159
  }
158
160
  // keep track of nested calls
159
161
  busyActive[busyName]++;
@@ -240,10 +242,12 @@ export default () => {
240
242
  busyActive[busyName]--;
241
243
  if (!busyActive[busyName]) {
242
244
  // if no nested calls, disable the "busy" state
243
- apos.bus.$emit('busy', {
244
- active: false,
245
- name: busyName
246
- });
245
+ if (apos.bus) {
246
+ apos.bus.$emit('busy', {
247
+ active: false,
248
+ name: busyName
249
+ });
250
+ }
247
251
  }
248
252
  }
249
253
  });
@@ -156,7 +156,7 @@ module.exports = {
156
156
  self.schema = self.apos.schema.compose({
157
157
  addFields: self.apos.schema.fieldsToArray(`Module ${self.__meta.name}`, self.fields),
158
158
  arrangeFields: self.apos.schema.groupsToArray(self.fieldsGroups)
159
- });
159
+ }, self);
160
160
  const forbiddenFields = [
161
161
  '_id',
162
162
  'type'
@@ -19,6 +19,7 @@
19
19
  :schema="schema"
20
20
  :value="docFields"
21
21
  @input="updateDocFields"
22
+ @validate="triggerValidate"
22
23
  :following-values="followingValues()"
23
24
  :conditional-fields="conditionalFields()"
24
25
  ref="schema"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apostrophe",
3
- "version": "3.4.0",
3
+ "version": "3.7.0",
4
4
  "description": "The Apostrophe Content Management System.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -112,7 +112,7 @@
112
112
  "vue": "^2.6.14",
113
113
  "vue-click-outside-element": "^1.0.13",
114
114
  "vue-loader": "^15.9.6",
115
- "vue-material-design-icons": "^4.9.0",
115
+ "vue-material-design-icons": "~4.12.1",
116
116
  "vue-style-loader": "^4.1.2",
117
117
  "vue-template-compiler": "^2.6.14",
118
118
  "vuedraggable": "^2.24.3",
@@ -127,7 +127,7 @@
127
127
  "eslint-loader": "^4.0.2",
128
128
  "eslint-plugin-node": "^11.1.0",
129
129
  "eslint-plugin-vue": "^7.9.0",
130
- "mocha": "^7.1.2",
130
+ "mocha": "^9.1.2",
131
131
  "nyc": "^15.1.0",
132
132
  "replace-in-file": "^6.1.0",
133
133
  "vue-eslint-parser": "^7.1.1",
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ improve: '@apostrophecms/global',
3
+ options: {
4
+ testGlobalLevelLoaded: true,
5
+ testGlobalLevel: true
6
+ }
7
+ };
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ improve: '@apostrophecms/piece-type',
3
+ options: {
4
+ testPieceTypeLevelLoaded: true,
5
+ testPieceTypeLevel: true
6
+ }
7
+ };
@@ -0,0 +1,30 @@
1
+ const t = require('../test-lib/test.js');
2
+ const assert = require('assert');
3
+
4
+ describe('Improve Overrides', function() {
5
+
6
+ this.timeout(t.timeout);
7
+
8
+ it('"improve" should work, but project level should override it', async function() {
9
+ let apos;
10
+ try {
11
+ apos = await t.create({
12
+ root: module,
13
+ modules: {
14
+ 'improve-piece-type': {},
15
+ 'improve-global': {}
16
+ }
17
+ });
18
+ assert(apos.global.options.verifyProjectLevelLoaded);
19
+ assert.strictEqual(apos.user.options.testPieceTypeLevelLoaded, true);
20
+ assert.strictEqual(apos.user.options.testPieceTypeLevel, true);
21
+ assert.strictEqual(apos.global.options.testPieceTypeLevelLoaded, true);
22
+ assert.strictEqual(apos.global.options.testPieceTypeLevel, false);
23
+ assert.strictEqual(apos.global.options.testGlobalLevelLoaded, true);
24
+ assert.strictEqual(apos.global.options.testGlobalLevel, false);
25
+ } finally {
26
+ t.destroy(apos);
27
+ }
28
+ });
29
+
30
+ });
package/test/login.js CHANGED
@@ -161,4 +161,187 @@ describe('Login', function() {
161
161
  assert(page.match(/logged out/));
162
162
  });
163
163
 
164
+ it('Changing a user\'s password should invalidate sessions for that user', async function() {
165
+
166
+ const jar = apos.http.jar();
167
+
168
+ // establish session
169
+ let page = await apos.http.get(
170
+ '/',
171
+ {
172
+ jar
173
+ }
174
+ );
175
+
176
+ assert(page.match(/logged out/));
177
+
178
+ await apos.http.post(
179
+ '/api/v1/@apostrophecms/login/login',
180
+ {
181
+ method: 'POST',
182
+ body: {
183
+ username: 'HarryPutter',
184
+ password: 'crookshanks',
185
+ session: true
186
+ },
187
+ jar
188
+ }
189
+ );
190
+
191
+ page = await apos.http.get(
192
+ '/',
193
+ {
194
+ jar
195
+ }
196
+ );
197
+
198
+ assert(page.match(/logged in/));
199
+
200
+ const req = apos.task.getReq();
201
+ let user = await apos.user.find(req, {
202
+ username: 'HarryPutter'
203
+ }).toObject();
204
+ assert(user);
205
+ user.password = 'VeryPasswordManySecure🐶';
206
+ await apos.user.update(req, user);
207
+
208
+ page = await apos.http.get(
209
+ '/',
210
+ {
211
+ jar
212
+ }
213
+ );
214
+
215
+ assert(!page.match(/logged in/));
216
+ assert(page.match(/logged out/));
217
+
218
+ // Make sure we can come back from that
219
+ await apos.http.post(
220
+ '/api/v1/@apostrophecms/login/login',
221
+ {
222
+ method: 'POST',
223
+ body: {
224
+ username: 'HarryPutter',
225
+ password: 'VeryPasswordManySecure🐶',
226
+ session: true
227
+ },
228
+ jar
229
+ }
230
+ );
231
+
232
+ page = await apos.http.get(
233
+ '/',
234
+ {
235
+ jar
236
+ }
237
+ );
238
+
239
+ assert(page.match(/logged in/));
240
+
241
+ // So we do not have a stale _passwordUpdated flag
242
+ user = await apos.user.find(req, {
243
+ _id: user._id
244
+ }).toObject();
245
+
246
+ // Unrelated writes to user should not invalidate sessions
247
+ user.title = 'Extra Cool Putter';
248
+ await apos.user.update(req, user);
249
+
250
+ page = await apos.http.get(
251
+ '/',
252
+ {
253
+ jar
254
+ }
255
+ );
256
+
257
+ assert(page.match(/logged in/));
258
+
259
+ // Marking a user account as disabled should invalidate sessions
260
+ user.disabled = true;
261
+ await apos.user.update(req, user);
262
+
263
+ page = await apos.http.get(
264
+ '/',
265
+ {
266
+ jar
267
+ }
268
+ );
269
+
270
+ assert(page.match(/logged out/));
271
+
272
+ // Restore access for next test
273
+ user.disabled = false;
274
+ await apos.user.update(req, user);
275
+
276
+ });
277
+
278
+ it('Changing a user\'s password should invalidate bearer tokens for that user', async function() {
279
+
280
+ // Log in
281
+ let response = await apos.http.post('/api/v1/@apostrophecms/login/login', {
282
+ body: {
283
+ username: 'HarryPutter',
284
+ password: 'VeryPasswordManySecure🐶'
285
+ }
286
+ });
287
+ assert(response.token);
288
+ let token = response.token;
289
+
290
+ // For verification: can't do this without an admin bearer token
291
+ await apos.http.get(
292
+ '/api/v1/@apostrophecms/user',
293
+ {
294
+ headers: {
295
+ Authorization: `Bearer ${token}`
296
+ }
297
+ }
298
+ );
299
+
300
+ const req = apos.task.getReq();
301
+ const user = await apos.user.find(req, {
302
+ username: 'HarryPutter'
303
+ }).toObject();
304
+ assert(user);
305
+ user.password = 'AnotherLovelyPassword';
306
+ await apos.user.update(req, user);
307
+
308
+ let failed = false;
309
+ try {
310
+ await apos.http.get(
311
+ '/api/v1/@apostrophecms/user',
312
+ {
313
+ headers: {
314
+ Authorization: `Bearer ${token}`
315
+ }
316
+ }
317
+ );
318
+ // Should NOT work
319
+ assert(false);
320
+ } catch (e) {
321
+ failed = true;
322
+ assert.strictEqual(e.status, 401);
323
+ }
324
+ assert(failed);
325
+
326
+ // Make sure we can come back from that
327
+ response = await apos.http.post('/api/v1/@apostrophecms/login/login', {
328
+ body: {
329
+ username: 'HarryPutter',
330
+ password: 'AnotherLovelyPassword'
331
+ }
332
+ });
333
+ assert(response.token);
334
+ token = response.token;
335
+
336
+ await apos.http.get(
337
+ '/api/v1/@apostrophecms/user',
338
+ {
339
+ headers: {
340
+ Authorization: `Bearer ${token}`
341
+ }
342
+ }
343
+ );
344
+
345
+ });
346
+
164
347
  });
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ options: {
3
+ verifyProjectLevelLoaded: true,
4
+ // Verify we can override these
5
+ testPieceTypeLevel: false,
6
+ testGlobalLevel: false
7
+ }
8
+ };
@@ -0,0 +1,7 @@
1
+ {% extends data.outerLayout %}
2
+
3
+ {% import "fragment.html" as fragment %}
4
+
5
+ {% block main %}
6
+ {% render fragment.auxTest('Gee Whiz') %}
7
+ {% endblock %}
@@ -28,3 +28,8 @@
28
28
  {% fragment _print(text) %}
29
29
  {{ text }}
30
30
  {% endfragment %}
31
+
32
+ {% fragment auxTest(s) %}
33
+ {{ apos.util.slugify(s) }}
34
+ {{ __t('apostrophe:modifyOrDelete') }}
35
+ {% endfragment %}
package/test/moog.js CHANGED
@@ -237,6 +237,53 @@ describe('moog', function() {
237
237
  assert(myObject.extended(5) === 20);
238
238
  });
239
239
 
240
+ it('should support inheriting field group fields rather than requiring all fields to be restated', async function() {
241
+ const moog = require('../lib/moog.js')({});
242
+
243
+ moog.define('myObject', {
244
+ cascades: [ 'fields' ],
245
+ fields: {
246
+ add: {
247
+ one: { type: 'string' },
248
+ two: { type: 'string' },
249
+ three: { type: 'string' }
250
+ },
251
+ group: {
252
+ basics: {
253
+ fields: [ 'one', 'two', 'three' ]
254
+ }
255
+ }
256
+ }
257
+ });
258
+
259
+ moog.define('myObject', {
260
+ fields: {
261
+ add: {
262
+ four: { type: 'string' },
263
+ five: { type: 'string' }
264
+ },
265
+ group: {
266
+ basics: {
267
+ fields: [ 'four', 'five' ]
268
+ },
269
+ other: {
270
+ fields: [ 'one' ]
271
+ }
272
+ }
273
+ }
274
+ });
275
+
276
+ const myObject = await moog.create('myObject', {});
277
+ assert(myObject);
278
+ assert(myObject.fieldsGroups);
279
+ assert(!myObject.fieldsGroups.basics.fields.includes('one'));
280
+ assert(myObject.fieldsGroups.other.fields.includes('one'));
281
+ assert(myObject.fieldsGroups.basics.fields.includes('two'));
282
+ assert(myObject.fieldsGroups.basics.fields.includes('three'));
283
+ assert(myObject.fieldsGroups.basics.fields.includes('four'));
284
+ assert(myObject.fieldsGroups.basics.fields.includes('five'));
285
+ });
286
+
240
287
  // ==================================================
241
288
  // `redefine` AND `isDefined`
242
289
  // ==================================================