apostrophe 4.27.1 → 4.28.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 (55) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/index.js +3 -0
  3. package/lib/stream-proxy.js +49 -0
  4. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +2 -11
  5. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +38 -6
  6. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +12 -1
  7. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +111 -41
  8. package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +1 -0
  9. package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +22 -10
  10. package/modules/@apostrophecms/area/ui/apos/logic/AposAreaEditor.js +40 -0
  11. package/modules/@apostrophecms/asset/index.js +3 -2
  12. package/modules/@apostrophecms/attachment/index.js +270 -0
  13. package/modules/@apostrophecms/doc/index.js +8 -2
  14. package/modules/@apostrophecms/doc-type/index.js +81 -1
  15. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +18 -2
  16. package/modules/@apostrophecms/express/index.js +30 -1
  17. package/modules/@apostrophecms/file/index.js +71 -6
  18. package/modules/@apostrophecms/i18n/index.js +20 -1
  19. package/modules/@apostrophecms/image/index.js +11 -0
  20. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +31 -6
  21. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +12 -10
  22. package/modules/@apostrophecms/login/index.js +43 -11
  23. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +2 -1
  24. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +5 -0
  25. package/modules/@apostrophecms/page/index.js +9 -11
  26. package/modules/@apostrophecms/page-type/index.js +6 -1
  27. package/modules/@apostrophecms/piece-page-type/index.js +100 -13
  28. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +1 -0
  29. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +28 -12
  30. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +1 -0
  31. package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +1 -1
  32. package/modules/@apostrophecms/styles/lib/apiRoutes.js +25 -5
  33. package/modules/@apostrophecms/styles/lib/handlers.js +19 -0
  34. package/modules/@apostrophecms/styles/lib/methods.js +35 -12
  35. package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +7 -2
  36. package/modules/@apostrophecms/task/index.js +9 -1
  37. package/modules/@apostrophecms/template/views/outerLayoutBase.html +3 -0
  38. package/modules/@apostrophecms/ui/index.js +2 -0
  39. package/modules/@apostrophecms/ui/ui/apos/components/AposButtonGroup.vue +1 -1
  40. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +5 -0
  41. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +5 -0
  42. package/modules/@apostrophecms/ui/ui/apos/lib/vue.js +2 -0
  43. package/modules/@apostrophecms/ui/ui/apos/stores/widget.js +12 -7
  44. package/modules/@apostrophecms/ui/ui/apos/stores/widgetGraph.js +461 -0
  45. package/modules/@apostrophecms/ui/ui/apos/universal/graph.js +452 -0
  46. package/modules/@apostrophecms/ui/ui/apos/universal/widgetGraph.js +10 -0
  47. package/modules/@apostrophecms/uploadfs/index.js +15 -1
  48. package/modules/@apostrophecms/url/index.js +419 -1
  49. package/package.json +6 -6
  50. package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
  51. package/test/external-front.js +1 -0
  52. package/test/files.js +135 -0
  53. package/test/login-requirements.js +145 -3
  54. package/test/static-build.js +2701 -0
  55. package/test/universal-graph.js +1135 -0
package/test/files.js ADDED
@@ -0,0 +1,135 @@
1
+ const t = require('../test-lib/test.js');
2
+ const assert = require('assert/strict');
3
+ const fs = require('fs');
4
+
5
+ describe('Files', function() {
6
+
7
+ let apos;
8
+
9
+ const mockFiles = [
10
+ {
11
+ type: '@apostrophecms/file',
12
+ slug: 'file-pretty-nice',
13
+ visibility: 'public',
14
+ attachment: {
15
+ type: 'attachment',
16
+ _id: 'testid',
17
+ name: 'testname',
18
+ extension: 'pdf',
19
+ // Only for simulation purposes
20
+ data: 'I am a fake PDF'
21
+ }
22
+ }
23
+ ];
24
+
25
+ this.timeout(t.timeout);
26
+
27
+ after(async function() {
28
+ return t.destroy(apos);
29
+ });
30
+
31
+ before(async function() {
32
+ this.timeout(t.timeout);
33
+ this.slow(2000);
34
+
35
+ apos = await t.create({
36
+ root: module,
37
+ modules: {
38
+ '@apostrophecms/file': {
39
+ options: {
40
+ prettyUrls: true
41
+ }
42
+ }
43
+ }
44
+ });
45
+
46
+ assert(apos.file);
47
+ assert(apos.file.__meta.name === '@apostrophecms/file');
48
+ // Bring the right port number into the base URL. This is
49
+ // good enough for loopback only, which is why we only
50
+ // use this trick in tests
51
+ apos.baseUrl = apos.http.getBase();
52
+
53
+ // Clean up any leftovers from last time
54
+ try {
55
+ const response = await apos.doc.db.deleteMany(
56
+ { type: '@apostrophecms/file' }
57
+ );
58
+ assert(response.result.ok === 1);
59
+ try {
60
+ fs.mkdirSync(`${__dirname}/public/uploads/attachments`);
61
+ } catch (e) {
62
+ // May already exist
63
+ }
64
+ for (const file of mockFiles) {
65
+ try {
66
+ const {
67
+ _id, name, extension
68
+ } = file;
69
+ fs.unlinkSync(`${__dirname}/public/uploads/attachments/${_id}-${name}.${extension}`);
70
+ } catch (e) {
71
+ // Don't care if we got that far or not
72
+ }
73
+ }
74
+ } catch (e) {
75
+ assert(false);
76
+ }
77
+
78
+ });
79
+
80
+ it('should add files for testing', async function() {
81
+ assert(apos.file.insert);
82
+
83
+ const req = apos.task.getReq();
84
+
85
+ const insertPromises = mockFiles.map(async (file) => {
86
+ const result = await apos.file.insert(req, file);
87
+ const {
88
+ _id, name, extension, data
89
+ } = file.attachment;
90
+ fs.writeFileSync(`${__dirname}/public/uploads/attachments/${_id}-${name}.${extension}`, data);
91
+ return result;
92
+ });
93
+
94
+ const inserted = await Promise.all(insertPromises);
95
+
96
+ assert(inserted.length === mockFiles.length);
97
+ assert(inserted[0]._id);
98
+ });
99
+
100
+ it('should generate an ugly URL when prettyUrls: true is not set on the module', async function() {
101
+ apos.file.options.prettyUrls = false;
102
+ try {
103
+ const req = apos.task.getAnonReq();
104
+ const files = await apos.file.find(req).toArray();
105
+ assert.strictEqual(files.length, 1);
106
+ const file = files[0];
107
+ const attachment = apos.attachment.first(file);
108
+ const url = apos.attachment.url(attachment);
109
+ assert(url);
110
+ assert.strictEqual(url, `/uploads/attachments/${attachment._id}-${attachment.name}.${attachment.extension}`);
111
+ } finally {
112
+ // So we don't spoil the next test either way
113
+ apos.file.options.prettyUrls = true;
114
+ }
115
+ });
116
+
117
+ it('should generate a pretty URL when prettyUrls: true is set and successfully serve it', async function() {
118
+ const req = apos.task.getAnonReq();
119
+ try {
120
+ apos.file.options.prettyUrls = true;
121
+ const files = await apos.file.find(req).toArray();
122
+ assert.strictEqual(files.length, 1);
123
+ const file = files[0];
124
+ const attachment = apos.attachment.first(file);
125
+ const url = apos.attachment.url(attachment);
126
+ assert(url);
127
+ assert.strictEqual(url, `${apos.http.getBase()}/files/${file.slug.replace('file-', '')}.${attachment.extension}`);
128
+ const body = await apos.http.get(url);
129
+ assert.strictEqual(body, attachment.data);
130
+ } finally {
131
+ apos.file.options.prettyUrls = false;
132
+ }
133
+ });
134
+
135
+ });
@@ -1,7 +1,7 @@
1
1
  const t = require('../test-lib/test.js');
2
2
  const assert = require('assert');
3
3
 
4
- describe('Login', function() {
4
+ describe('Login Requirements', function() {
5
5
 
6
6
  let apos;
7
7
 
@@ -433,10 +433,13 @@ describe('Login', function() {
433
433
 
434
434
  assert(page.match(/logged out/));
435
435
 
436
- // Make sure it won't convert with an incorrect ExtraSecret
437
-
438
436
  const token = result.incompleteToken;
439
437
 
438
+ // Make sure we can't use an incomplete token as a bearer token
439
+ await assert.rejects(tryAsBearerToken);
440
+
441
+ // Make sure it won't convert with an incorrect ExtraSecret
442
+
440
443
  try {
441
444
  await apos.http.post('/api/v1/@apostrophecms/login/requirement-verify', {
442
445
  body: {
@@ -447,12 +450,17 @@ describe('Login', function() {
447
450
  },
448
451
  jar
449
452
  });
453
+ // Getting here is bad
454
+ assert(false);
450
455
  } catch ({ status, body }) {
451
456
  assert(status === 400);
452
457
  assert.strictEqual(body.message, extraSecretErr);
453
458
  assert.strictEqual(body.data.requirement, 'ExtraSecret');
454
459
  }
455
460
 
461
+ // Make sure a bad conversion attempt doesn't unlock it as a bearer token either
462
+ await assert.rejects(tryAsBearerToken);
463
+
456
464
  // If we try the final login without
457
465
  // having successfully verified all requirements we get an error
458
466
  try {
@@ -512,6 +520,10 @@ describe('Login', function() {
512
520
  jar
513
521
  });
514
522
 
523
+ // Only now should we be able to use it as a bearer token
524
+ await tryAsBearerToken();
525
+
526
+ // Complete the cookie-based session login process
515
527
  await apos.http.post(
516
528
  '/api/v1/@apostrophecms/login/login',
517
529
  {
@@ -532,6 +544,136 @@ describe('Login', function() {
532
544
  );
533
545
 
534
546
  assert(page.match(/logged in/));
547
+
548
+ async function tryAsBearerToken() {
549
+ await apos.http.get('/api/v1/@apostrophecms/page', {
550
+ headers: {
551
+ authorization: `Bearer ${token}`
552
+ }
553
+ });
554
+ }
555
+ });
556
+
557
+ });
558
+
559
+ describe('Expired Token Deletion', function() {
560
+
561
+ let apos;
562
+
563
+ const extraSecretErr = 'extra secret incorrect';
564
+
565
+ this.timeout(20000);
566
+
567
+ this.beforeEach(async function() {
568
+ if (apos && apos.modules && apos.modules['@apostrophecms/login']) {
569
+ const loginModule = apos.modules['@apostrophecms/login'];
570
+ await loginModule.clearLoginAttempts('HarryPutter');
571
+ }
572
+ });
573
+
574
+ after(function() {
575
+ return t.destroy(apos);
576
+ });
577
+
578
+ // EXISTENCE
579
+
580
+ it('should initialize', async function() {
581
+ apos = await t.create({
582
+ root: module,
583
+ modules: {
584
+ '@apostrophecms/login': {
585
+ options: {
586
+ incompleteLifetime: 5000
587
+ },
588
+ requirements(self) {
589
+ return {
590
+ add: {
591
+ // Need an extra requirement so that the token will die
592
+ // after incompleteLifetime
593
+ ExtraSecret: {
594
+ phase: 'afterPasswordVerified',
595
+ async props(req, user) {
596
+ return {
597
+ // Verify we had access to the user here
598
+ hint: user.username
599
+ };
600
+ },
601
+ async verify(req, data, user) {
602
+ if (data !== user.extraSecret) {
603
+ throw self.apos.error('invalid', extraSecretErr);
604
+ }
605
+ }
606
+ }
607
+ }
608
+ };
609
+ }
610
+ }
611
+ }
612
+ });
613
+
614
+ assert(apos.modules['@apostrophecms/login']);
615
+ });
616
+
617
+ it('should be able to insert test user', async function() {
618
+ assert(apos.user.newInstance);
619
+ const user = apos.user.newInstance();
620
+ assert(user);
621
+
622
+ user.title = 'Harry Putter';
623
+ user.username = 'HarryPutter';
624
+ user.password = 'crookshanks';
625
+ user.email = 'hputter@aol.com';
626
+ user.role = 'admin';
627
+ user.extraSecret = 'roll-on';
628
+
629
+ assert(user.type === '@apostrophecms/user');
630
+ assert(apos.user.insert);
631
+ const doc = await apos.user.insert(apos.task.getReq(), user);
632
+ assert(doc._id);
535
633
  });
536
634
 
635
+ it('initial login should produce an incompleteToken, convertible with the afterPasswordVerified requirements', async function() {
636
+
637
+ const jar = apos.http.jar();
638
+
639
+ // establish session
640
+ let page = await apos.http.get(
641
+ '/',
642
+ {
643
+ jar
644
+ }
645
+ );
646
+
647
+ const result = await apos.http.post(
648
+ '/api/v1/@apostrophecms/login/login',
649
+ {
650
+ method: 'POST',
651
+ body: {
652
+ username: 'HarryPutter',
653
+ password: 'crookshanks',
654
+ session: true,
655
+ requirements: {
656
+ WeakCaptcha: 'xyz',
657
+ UponSubmit: 'abc'
658
+ }
659
+ },
660
+ jar
661
+ }
662
+ );
663
+
664
+ const token = result.incompleteToken;
665
+ assert(token);
666
+ // Verify it initially exists
667
+ assert(await apos.login.bearerTokens.findOne({ _id: token }));
668
+ // Wait until well over 5 seconds have passed to allow the cleanup interval to run
669
+ await delay(10000);
670
+ // Verify it is gone from the db
671
+ assert(!(await apos.login.bearerTokens.findOne({ _id: token })));
672
+ });
537
673
  });
674
+
675
+ function delay(ms) {
676
+ return new Promise((resolve, reject) => {
677
+ setTimeout(() => resolve(), ms);
678
+ });
679
+ }