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.
Files changed (43) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/index.js +37 -1
  3. package/lib/moog.js +2 -2
  4. package/modules/@apostrophecms/asset/index.js +24 -9
  5. package/modules/@apostrophecms/asset/lib/globalIcons.js +2 -0
  6. package/modules/@apostrophecms/attachment/index.js +1 -1
  7. package/modules/@apostrophecms/doc/index.js +9 -3
  8. package/modules/@apostrophecms/doc-type/index.js +2 -2
  9. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocContextMenu.vue +0 -7
  10. package/modules/@apostrophecms/express/index.js +50 -38
  11. package/modules/@apostrophecms/http/index.js +0 -20
  12. package/modules/@apostrophecms/i18n/i18n/en.json +1 -0
  13. package/modules/@apostrophecms/i18n/i18n/sk.json +23 -1
  14. package/modules/@apostrophecms/i18n/index.js +62 -13
  15. package/modules/@apostrophecms/login/index.js +282 -42
  16. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +242 -77
  17. package/modules/@apostrophecms/login/ui/apos/components/TheAposLoginHeader.vue +108 -0
  18. package/modules/@apostrophecms/module/index.js +12 -2
  19. package/modules/@apostrophecms/page/index.js +98 -82
  20. package/modules/@apostrophecms/permission/index.js +1 -1
  21. package/modules/@apostrophecms/piece-page-type/index.js +1 -1
  22. package/modules/@apostrophecms/piece-type/index.js +86 -73
  23. package/modules/@apostrophecms/schema/index.js +18 -2
  24. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArea.vue +12 -5
  25. package/modules/@apostrophecms/schema/ui/apos/components/AposInputCheckboxes.vue +4 -0
  26. package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +18 -0
  27. package/modules/@apostrophecms/util/index.js +3 -9
  28. package/modules/@apostrophecms/util/ui/src/http.js +1 -7
  29. package/package.json +2 -1
  30. package/test/express.js +2 -26
  31. package/test/http.js +0 -24
  32. package/test/login-requirements.js +328 -0
  33. package/test/modules/base-type/i18n/custom/en.json +4 -0
  34. package/test/modules/base-type/i18n/en.json +3 -0
  35. package/test/modules/nested-module-subdirs/example1/index.js +5 -0
  36. package/test/modules/nested-module-subdirs/modules.js +7 -0
  37. package/test/modules/subtype/i18n/custom/en.json +4 -0
  38. package/test/modules/subtype/index.js +7 -0
  39. package/test/pages-rest.js +39 -0
  40. package/test/pieces-page-type.js +63 -0
  41. package/test/static-i18n.js +28 -0
  42. package/test/with-nested-module-subdirs.js +32 -0
  43. package/test/without-nested-module-subdirs.js +31 -0
package/test/express.js CHANGED
@@ -39,37 +39,13 @@ describe('Express', function() {
39
39
  assert(body.toString() === 'ok');
40
40
  });
41
41
 
42
- it('should flunk a POST request with no X-XSRF-TOKEN header', async function() {
42
+ it('should flunk a POST request without the CSRF cookie', async function() {
43
43
  try {
44
44
  await apos.http.post('/tests/body', {
45
45
  body: {
46
46
  person: {
47
47
  age: '30'
48
48
  }
49
- },
50
- jar,
51
- csrf: false
52
- });
53
- assert(false);
54
- } catch (e) {
55
- assert(e);
56
- }
57
- });
58
-
59
- it('should flunk a POST request with the wrong CSRF token', async function() {
60
- const csrfToken = 'BOGOSITY';
61
-
62
- try {
63
- await apos.http.post('/tests/body', {
64
- body: {
65
- person: {
66
- age: '30'
67
- }
68
- },
69
- jar,
70
- csrf: false,
71
- headers: {
72
- 'X-XSRF-TOKEN': csrfToken
73
49
  }
74
50
  });
75
51
  assert(false);
@@ -78,7 +54,7 @@ describe('Express', function() {
78
54
  }
79
55
  });
80
56
 
81
- it('should use the extended bodyParser for submitted forms', async function() {
57
+ it('should use the extended bodyParser for submitted forms, and pass CSRF with the cookie', async function() {
82
58
 
83
59
  const response = await apos.http.post('/tests/body', {
84
60
  send: 'form',
package/test/http.js CHANGED
@@ -40,30 +40,6 @@ describe('Http', function() {
40
40
  assert(result.match(/logged out/));
41
41
  });
42
42
 
43
- it('should not be able to make an http POST request without csrf header', async () => {
44
- try {
45
- await apos.http.post('/csrf-test', {
46
- jar,
47
- csrf: false
48
- });
49
- assert(false);
50
- } catch (e) {
51
- assert(e.status === 403);
52
- }
53
- });
54
-
55
- it('should be able to make an http POST request with manually built csrf header', async () => {
56
- const response = await apos.http.post('/csrf-test?manual=1', {
57
- jar,
58
- headers: {
59
- 'X-XSRF-TOKEN': apos.http.getCookie(jar, '/', `${apos.options.shortName}.csrf`)
60
- },
61
- body: {},
62
- csrf: false
63
- });
64
- assert(response.ok === true);
65
- });
66
-
67
43
  it('should be able to make an http POST request with csrf header via default csrf convenience of http.post', async () => {
68
44
  const response = await apos.http.post('/csrf-test', {
69
45
  jar,
@@ -0,0 +1,328 @@
1
+ const t = require('../test-lib/test.js');
2
+ const assert = require('assert');
3
+
4
+ let apos;
5
+
6
+ describe('Login', function() {
7
+
8
+ this.timeout(20000);
9
+
10
+ after(function() {
11
+ return t.destroy(apos);
12
+ });
13
+
14
+ // EXISTENCE
15
+
16
+ it('should initialize', async function() {
17
+ apos = await t.create({
18
+ root: module,
19
+ modules: {
20
+ '@apostrophecms/login': {
21
+ requirements(self) {
22
+ return {
23
+ add: {
24
+ WeakCaptcha: {
25
+ phase: 'beforeSubmit',
26
+ async props(req) {
27
+ return {
28
+ hint: 'xyz'
29
+ };
30
+ },
31
+ async verify(req, data) {
32
+ if (data !== 'xyz') {
33
+ throw self.apos.error('invalid', 'captcha code incorrect');
34
+ }
35
+ }
36
+ },
37
+
38
+ ExtraSecret: {
39
+ phase: 'afterPasswordVerified',
40
+ async props(req, user) {
41
+ return {
42
+ // Verify we had access to the user here
43
+ hint: user.username
44
+ };
45
+ },
46
+ async verify(req, data, user) {
47
+ if (data !== user.extraSecret) {
48
+ throw self.apos.error('invalid', 'extra secret incorrect');
49
+ }
50
+ }
51
+ }
52
+ }
53
+ };
54
+ }
55
+ }
56
+ }
57
+ });
58
+
59
+ assert(apos.modules['@apostrophecms/login']);
60
+ });
61
+
62
+ it('should be able to insert test user', async function() {
63
+ assert(apos.user.newInstance);
64
+ const user = apos.user.newInstance();
65
+ assert(user);
66
+
67
+ user.title = 'Harry Putter';
68
+ user.username = 'HarryPutter';
69
+ user.password = 'crookshanks';
70
+ user.email = 'hputter@aol.com';
71
+ user.role = 'admin';
72
+ user.extraSecret = 'roll-on';
73
+
74
+ assert(user.type === '@apostrophecms/user');
75
+ assert(apos.user.insert);
76
+ const doc = await apos.user.insert(apos.task.getReq(), user);
77
+ assert(doc._id);
78
+ });
79
+
80
+ it('should not be able to login a user without meeting a beforeSubmit requirement', async function() {
81
+
82
+ const jar = apos.http.jar();
83
+
84
+ // establish session
85
+ let page = await apos.http.get(
86
+ '/',
87
+ {
88
+ jar
89
+ }
90
+ );
91
+
92
+ assert(page.match(/logged out/));
93
+
94
+ const context = await apos.http.post(
95
+ '/api/v1/@apostrophecms/login/context',
96
+ {
97
+ method: 'POST',
98
+ body: {},
99
+ jar
100
+ }
101
+ );
102
+ assert(context.requirementProps);
103
+ assert(context.requirementProps.WeakCaptcha);
104
+ assert.strictEqual(context.requirementProps.WeakCaptcha.hint, 'xyz');
105
+
106
+ try {
107
+ await apos.http.post(
108
+ '/api/v1/@apostrophecms/login/login',
109
+ {
110
+ method: 'POST',
111
+ body: {
112
+ username: 'HarryPutter',
113
+ password: 'crookshanks',
114
+ session: true
115
+ },
116
+ jar
117
+ }
118
+ );
119
+ assert(false);
120
+ } catch (e) {
121
+ assert(e.status === 400);
122
+ assert.strictEqual(e.body.message, 'captcha code incorrect');
123
+ assert.strictEqual(e.body.data.requirement, 'WeakCaptcha');
124
+ }
125
+
126
+ // Make sure it really didn't work
127
+ page = await apos.http.get(
128
+ '/',
129
+ {
130
+ jar
131
+ }
132
+ );
133
+
134
+ assert(page.match(/logged out/));
135
+ });
136
+
137
+ it('should not be able to login a user with the wrong value for a beforeSubmit requirement', async function() {
138
+
139
+ const jar = apos.http.jar();
140
+
141
+ // establish session
142
+ let page = await apos.http.get(
143
+ '/',
144
+ {
145
+ jar
146
+ }
147
+ );
148
+
149
+ assert(page.match(/logged out/));
150
+
151
+ try {
152
+ await apos.http.post(
153
+ '/api/v1/@apostrophecms/login/login',
154
+ {
155
+ method: 'POST',
156
+ body: {
157
+ username: 'HarryPutter',
158
+ password: 'crookshanks',
159
+ session: true,
160
+ requirements: {
161
+ WeakCaptcha: 'abc'
162
+ }
163
+ },
164
+ jar
165
+ }
166
+ );
167
+ assert(false);
168
+ } catch (e) {
169
+ assert(e.status === 400);
170
+ assert.strictEqual(e.body.message, 'captcha code incorrect');
171
+ assert.strictEqual(e.body.data.requirement, 'WeakCaptcha');
172
+ }
173
+
174
+ // Make sure it really didn't work
175
+ page = await apos.http.get(
176
+ '/',
177
+ {
178
+ jar
179
+ }
180
+ );
181
+
182
+ assert(page.match(/logged out/));
183
+ });
184
+
185
+ it('initial login should produce an incompleteToken, convertible with the afterPasswordVerified requirements', async function() {
186
+
187
+ const jar = apos.http.jar();
188
+
189
+ // establish session
190
+ let page = await apos.http.get(
191
+ '/',
192
+ {
193
+ jar
194
+ }
195
+ );
196
+
197
+ assert(page.match(/logged out/));
198
+
199
+ const result = await apos.http.post(
200
+ '/api/v1/@apostrophecms/login/login',
201
+ {
202
+ method: 'POST',
203
+ body: {
204
+ username: 'HarryPutter',
205
+ password: 'crookshanks',
206
+ session: true,
207
+ requirements: {
208
+ WeakCaptcha: 'xyz'
209
+ }
210
+ },
211
+ jar
212
+ }
213
+ );
214
+
215
+ assert(result.incompleteToken);
216
+
217
+ // Make sure it did not create a login session prematurely
218
+ page = await apos.http.get(
219
+ '/',
220
+ {
221
+ jar
222
+ }
223
+ );
224
+
225
+ assert(page.match(/logged out/));
226
+
227
+ // Make sure it won't convert with an incorrect ExtraSecret
228
+
229
+ const token = result.incompleteToken;
230
+
231
+ try {
232
+ await apos.http.post('/api/v1/@apostrophecms/login/requirement-verify', {
233
+ body: {
234
+ incompleteToken: token,
235
+ session: true,
236
+ name: 'ExtraSecret',
237
+ value: 'roll-off'
238
+ },
239
+ jar
240
+ });
241
+ } catch ({ status, body }) {
242
+ assert(status === 400);
243
+ assert.strictEqual(body.message, 'extra secret incorrect');
244
+ assert.strictEqual(body.data.requirement, 'ExtraSecret');
245
+ }
246
+
247
+ // If we try the final login without
248
+ // having successfully verified all requirements we get an error
249
+ try {
250
+ await apos.http.post(
251
+ '/api/v1/@apostrophecms/login/login',
252
+ {
253
+ method: 'POST',
254
+ body: {
255
+ incompleteToken: token,
256
+ session: true
257
+ },
258
+ jar
259
+ }
260
+ );
261
+ } catch ({ status, body }) {
262
+ assert(status === 403);
263
+ assert.strictEqual(body.message, 'All requirements must be verified');
264
+ }
265
+
266
+ // Make sure it did not create a login session prematurely
267
+ page = await apos.http.get(
268
+ '/',
269
+ {
270
+ jar
271
+ }
272
+ );
273
+
274
+ assert(page.match(/logged out/));
275
+
276
+ // Fetch props for afterPasswordVerified component
277
+
278
+ const props = await apos.http.post(
279
+ '/api/v1/@apostrophecms/login/requirement-props',
280
+ {
281
+ method: 'POST',
282
+ body: {
283
+ incompleteToken: token,
284
+ name: 'ExtraSecret'
285
+ },
286
+ jar
287
+ }
288
+ );
289
+
290
+ assert.strictEqual(props.hint, 'HarryPutter');
291
+
292
+ // Now convert token to an actual login session
293
+ // by providing the post-password-verification requirements,
294
+ // correctly
295
+
296
+ await apos.http.post('/api/v1/@apostrophecms/login/requirement-verify', {
297
+ body: {
298
+ incompleteToken: token,
299
+ session: true,
300
+ name: 'ExtraSecret',
301
+ value: 'roll-on'
302
+ },
303
+ jar
304
+ });
305
+
306
+ await apos.http.post(
307
+ '/api/v1/@apostrophecms/login/login',
308
+ {
309
+ method: 'POST',
310
+ body: {
311
+ incompleteToken: token,
312
+ session: true
313
+ },
314
+ jar
315
+ }
316
+ );
317
+
318
+ page = await apos.http.get(
319
+ '/',
320
+ {
321
+ jar
322
+ }
323
+ );
324
+
325
+ assert(page.match(/logged in/));
326
+ });
327
+
328
+ });
@@ -0,0 +1,4 @@
1
+ {
2
+ "customTestOne": "Custom Test One From Base Type",
3
+ "customTestTwo": "Custom Test Two From Base Type"
4
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "defaultTestOne": "Default Test One"
3
+ }
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ init(self) {
3
+ self.initialized = true;
4
+ }
5
+ };
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ example1: {
3
+ options: {
4
+ folderLevelOption: true
5
+ }
6
+ }
7
+ };
@@ -0,0 +1,4 @@
1
+ {
2
+ "customTestOne": "Custom Test One From Subtype",
3
+ "customTestThree": "Custom Test Three From Subtype"
4
+ }
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ i18n: {
3
+ custom: {
4
+ browser: true
5
+ }
6
+ }
7
+ };
@@ -1486,6 +1486,45 @@ describe('Pages REST', function() {
1486
1486
  assert(doc.title === 'Advisory Test Patched Again');
1487
1487
  });
1488
1488
 
1489
+ let diacriticsId;
1490
+ it('is able to make a page including diacritics', async function() {
1491
+ const body = {
1492
+ slug: '/ḑiaçritiçs-čharaćters',
1493
+ visibility: 'public',
1494
+ type: 'test-page',
1495
+ title: 'Ḑiaçritiçs Čharaćters',
1496
+ _targetId: '_home',
1497
+ _position: '1'
1498
+ };
1499
+
1500
+ const page = await apos.http.post('/api/v1/@apostrophecms/page', {
1501
+ body,
1502
+ jar
1503
+ });
1504
+
1505
+ assert(page);
1506
+ assert(page.title === 'Ḑiaçritiçs Čharaćters');
1507
+ diacriticsId = page._id;
1508
+ // Accesses the published page.
1509
+ const published = await apos.http.get('/ḑiaçritiçs-čharaćters');
1510
+ assert(published);
1511
+ });
1512
+
1513
+ it('can archive the diacritics page then access the draft preview', async function () {
1514
+ // Now only a draft preview will be available.
1515
+ await apos.page.archive(apos.task.getReq(), diacriticsId);
1516
+
1517
+ try {
1518
+ const rendered = await apos.http.get('/ḑiaçritiçs-čharaćters', {
1519
+ jar
1520
+ });
1521
+ assert(rendered.match(/Sing to me, Oh Muse\./));
1522
+ } catch (error) {
1523
+ console.error(error);
1524
+ assert(false);
1525
+ }
1526
+ });
1527
+
1489
1528
  let jar2;
1490
1529
 
1491
1530
  it('should be able to log in as second user', async () => {
@@ -34,6 +34,24 @@ describe('Pieces Pages', function() {
34
34
  perPage: 10
35
35
  }
36
36
  },
37
+ home: {
38
+ extend: '@apostrophecms/piece-type',
39
+ options: {
40
+ name: 'home',
41
+ label: 'Home',
42
+ alias: 'home',
43
+ sort: { title: 1 }
44
+ }
45
+ },
46
+ 'home-page': {
47
+ extend: '@apostrophecms/piece-page-type',
48
+ options: {
49
+ name: 'homePiecePage',
50
+ label: 'Home Piece Page',
51
+ alias: 'homePiecePage',
52
+ perPage: 10
53
+ }
54
+ },
37
55
  '@apostrophecms/page': {
38
56
  options: {
39
57
  park: [
@@ -42,6 +60,12 @@ describe('Pieces Pages', function() {
42
60
  type: 'eventPage',
43
61
  slug: '/events',
44
62
  parkedId: 'events'
63
+ },
64
+ {
65
+ title: 'Home piece page',
66
+ type: 'homePiecePage',
67
+ slug: '/',
68
+ parkedId: 'home'
45
69
  }
46
70
  ]
47
71
  }
@@ -81,6 +105,36 @@ describe('Pieces Pages', function() {
81
105
  return apos.doc.db.insertMany(testItems);
82
106
  });
83
107
 
108
+ it('should be able to use db to insert test "home" pieces', async function() {
109
+ assert(apos.modules.home);
110
+ const testItems = [];
111
+ const total = 100;
112
+ for (let i = 1; (i <= total); i++) {
113
+ const paddedInt = apos.launder.padInteger(i, 3);
114
+
115
+ testItems.push({
116
+ _id: 'home' + paddedInt,
117
+ slug: 'home-' + paddedInt,
118
+ visibility: 'public',
119
+ type: 'home',
120
+ title: 'Home ' + paddedInt,
121
+ titleSortified: 'home ' + paddedInt,
122
+ body: {
123
+ metaType: 'area',
124
+ _id: apos.util.generateId(),
125
+ items: [
126
+ {
127
+ metaType: 'widget',
128
+ type: '@apostrophecms/rich-text',
129
+ content: '<p>This is some content.</p>'
130
+ }
131
+ ]
132
+ }
133
+ });
134
+ }
135
+
136
+ return apos.doc.db.insertMany(testItems);
137
+ });
84
138
  it('should populate the ._url property of pieces in any docs query', async function() {
85
139
  const piece = await apos.doc.find(apos.task.getAnonReq(), {
86
140
  type: 'event',
@@ -104,6 +158,15 @@ describe('Pieces Pages', function() {
104
158
  assert((!piece._url) || (piece._url.match(/undefined/)));
105
159
  });
106
160
 
161
+ it('should not create a double-slashed _url on a piece-page-type set as the homepage', async function() {
162
+ const piece = await apos.doc.find(apos.task.getAnonReq(), {
163
+ type: 'home',
164
+ title: 'Home 001'
165
+ }).toObject();
166
+ assert(piece);
167
+ assert(piece._url === '/home-001');
168
+ });
169
+
107
170
  it('should correctly populate the ._url property of pieces in a docs query if _url itself is "projected"', async function() {
108
171
  const piece = await apos.doc.find(apos.task.getAnonReq(), {
109
172
  type: 'event',
@@ -33,9 +33,19 @@ describe('static i18n', function() {
33
33
  'apos-fr': {
34
34
  options: {
35
35
  i18n: {
36
+ // Legacy technique must work
36
37
  ns: 'apostrophe'
37
38
  }
38
39
  }
40
+ },
41
+ // A base class that contributes some namespaced phrases in the new style way (subdirs)
42
+ 'base-type': {
43
+ instantiate: false
44
+ },
45
+ // Also contributes namespaced phrases in the new style way (subdirs)
46
+ // plus default locale phrases in the root i18n folder
47
+ subtype: {
48
+ extend: 'base-type'
39
49
  }
40
50
  }
41
51
  });
@@ -60,4 +70,22 @@ describe('static i18n', function() {
60
70
  assert.strictEqual(apos.task.getReq({ locale: 'fr' }).t('apostrophe:richTextAlignCenter'), 'Aligner Le Centre');
61
71
  });
62
72
 
73
+ it('should fetch default locale phrases from main i18n dir with no i18n option necessary', function() {
74
+ assert.strictEqual(apos.task.getReq().t('defaultTestOne'), 'Default Test One');
75
+ });
76
+
77
+ it('should fetch custom locale phrases from corresponding subdir', function() {
78
+ assert.strictEqual(apos.task.getReq().t('custom:customTestTwo'), 'Custom Test Two From Base Type');
79
+ assert.strictEqual(apos.task.getReq().t('custom:customTestThree'), 'Custom Test Three From Subtype');
80
+ });
81
+
82
+ it('last appearance in inheritance + configuration order wins', function() {
83
+ assert.strictEqual(apos.task.getReq().t('custom:customTestOne'), 'Custom Test One From Subtype');
84
+ });
85
+
86
+ it('should honor the browser: true flag in the i18n section of an index.js file', function() {
87
+ const browserData = apos.i18n.getBrowserData(apos.task.getReq());
88
+ assert.strictEqual(browserData.i18n.en.custom.customTestOne, 'Custom Test One From Subtype');
89
+ });
90
+
63
91
  });
@@ -0,0 +1,32 @@
1
+ const t = require('../test-lib/test.js');
2
+ const assert = require('assert');
3
+
4
+ describe('With Nested Module Subdirs', function() {
5
+ this.timeout(t.timeout);
6
+
7
+ let apos;
8
+
9
+ after(function () {
10
+ return t.destroy(apos);
11
+ });
12
+
13
+ /// ///
14
+ // EXISTENCE
15
+ /// ///
16
+
17
+ it('should initialize', async function() {
18
+ apos = await t.create({
19
+ root: module,
20
+ nestedModuleSubdirs: true,
21
+ modules: {
22
+ example1: {}
23
+ }
24
+ });
25
+ assert(apos.modules.example1);
26
+ // With nestedModuleSubdirs switched on, the index.js should be found,
27
+ // and modules.js should be loaded
28
+ assert(apos.modules.example1.options.folderLevelOption);
29
+ assert(apos.modules.example1.initialized);
30
+ });
31
+
32
+ });
@@ -0,0 +1,31 @@
1
+ const t = require('../test-lib/test.js');
2
+ const assert = require('assert');
3
+
4
+ describe('Without Nested Module Subdirs', function() {
5
+ this.timeout(t.timeout);
6
+
7
+ let apos;
8
+
9
+ after(function () {
10
+ return t.destroy(apos);
11
+ });
12
+
13
+ /// ///
14
+ // EXISTENCE
15
+ /// ///
16
+
17
+ it('should initialize', async function() {
18
+ apos = await t.create({
19
+ root: module,
20
+ modules: {
21
+ example1: {}
22
+ }
23
+ });
24
+ assert(apos.modules.example1);
25
+ // Should fail because we didn't turn on nestedModuleSubdirs,
26
+ // so the index.js was not found and modules.js was not loaded
27
+ assert(!apos.modules.example1.options.folderLevelOption);
28
+ assert(!apos.modules.example1.initialized);
29
+ });
30
+
31
+ });