apostrophe 3.31.0 → 3.32.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 (59) hide show
  1. package/.eslintrc +3 -0
  2. package/CHANGELOG.md +21 -1
  3. package/modules/@apostrophecms/asset/index.js +65 -7
  4. package/modules/@apostrophecms/asset/lib/webpack/utils.js +242 -28
  5. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +26 -16
  6. package/modules/@apostrophecms/i18n/i18n/en.json +17 -3
  7. package/modules/@apostrophecms/i18n/i18n/es.json +1 -0
  8. package/modules/@apostrophecms/i18n/i18n/fr.json +1 -0
  9. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +2 -1
  10. package/modules/@apostrophecms/i18n/i18n/sk.json +1 -0
  11. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +41 -20
  12. package/modules/@apostrophecms/login/index.js +123 -26
  13. package/modules/@apostrophecms/login/ui/apos/components/AposForgotPasswordForm.vue +124 -0
  14. package/modules/@apostrophecms/login/ui/apos/components/AposLoginForm.vue +339 -0
  15. package/modules/@apostrophecms/login/ui/apos/components/AposResetPasswordForm.vue +163 -0
  16. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +135 -293
  17. package/modules/@apostrophecms/login/ui/apos/components/TheAposLoginHeader.vue +65 -14
  18. package/modules/@apostrophecms/login/ui/apos/mixins/AposLoginFormMixin.js +45 -0
  19. package/modules/@apostrophecms/login/views/passwordResetEmail.html +9 -0
  20. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +17 -6
  21. package/modules/@apostrophecms/modal/ui/apos/components/AposModalBody.vue +4 -1
  22. package/modules/@apostrophecms/modal/ui/apos/components/AposModalTabs.vue +7 -1
  23. package/modules/@apostrophecms/piece-type/index.js +1 -1
  24. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +4 -3
  25. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Default.js +11 -5
  26. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Heading.js +6 -2
  27. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Link.js +7 -4
  28. package/modules/@apostrophecms/schema/lib/addFieldTypes.js +1 -1
  29. package/modules/@apostrophecms/schema/ui/apos/components/AposInputWrapper.vue +4 -1
  30. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +4 -0
  31. package/modules/@apostrophecms/template/index.js +11 -12
  32. package/modules/@apostrophecms/template/lib/bundlesLoader.js +20 -5
  33. package/modules/@apostrophecms/ui/ui/apos/mixins/AposAdvisoryLockMixin.js +2 -1
  34. package/modules/@apostrophecms/widget-type/index.js +17 -3
  35. package/package.json +1 -1
  36. package/test/assets.js +338 -25
  37. package/test/extra_node_modules/@company/bundle/index.js +8 -0
  38. package/test/extra_node_modules/@company/bundle/ui/src/company.js +3 -0
  39. package/test/extra_node_modules/@company/bundle/ui/src/company.scss +3 -0
  40. package/test/login.js +427 -12
  41. package/test/modules/@company/bundle/index.js +10 -0
  42. package/test/modules/@company/bundle/ui/src/company.js +3 -0
  43. package/test/modules/@company/bundle/ui/src/company.scss +3 -0
  44. package/test/modules/bundle-edge/index.js +10 -0
  45. package/test/modules/bundle-edge/ui/src/edge.js +3 -0
  46. package/test/modules/bundle-edge/ui/src/edge.scss +3 -0
  47. package/test/modules/bundle-page/index.js +2 -1
  48. package/test/modules/bundle-page/ui/src/extra.js +3 -1
  49. package/test/modules/bundle-page/ui/src/extra.scss +3 -0
  50. package/test/modules/bundle-page/ui/src/main.js +3 -0
  51. package/test/modules/bundle-page/ui/src/main.scss +3 -0
  52. package/test/modules/bundle-page-type/index.js +12 -0
  53. package/test/modules/bundle-page-type/ui/src/another.js +3 -0
  54. package/test/modules/bundle-page-type/ui/src/index.js +3 -0
  55. package/test/modules/bundle-page-type/ui/src/index.scss +3 -0
  56. package/test/modules/bundle-page-type/ui/src/main.js +3 -0
  57. package/test/modules/bundle-page-type/ui/src/main.scss +3 -0
  58. package/test/modules/bundle-widget/ui/src/extra2.js +3 -1
  59. package/test-lib/test.js +9 -1
package/test/login.js CHANGED
@@ -1,19 +1,14 @@
1
1
  const t = require('../test-lib/test.js');
2
- const assert = require('assert');
3
-
4
- let apos;
2
+ const assert = require('assert').strict;
5
3
 
6
4
  describe('Login', function() {
5
+ let apos;
6
+ let resetUserId;
7
+ let resetToken;
7
8
 
8
9
  this.timeout(20000);
9
10
 
10
- after(function() {
11
- return t.destroy(apos);
12
- });
13
-
14
- // EXISTENCE
15
-
16
- it('should initialize', async function() {
11
+ before(async function () {
17
12
  apos = await t.create({
18
13
  root: module,
19
14
  modules: {
@@ -25,9 +20,24 @@ describe('Login', function() {
25
20
  }
26
21
  }
27
22
  }
23
+ },
24
+ '@apostrophecms/login': {
25
+ options: {
26
+ passwordReset: true
27
+ }
28
28
  }
29
29
  }
30
30
  });
31
+ });
32
+
33
+ after(function() {
34
+ return t.destroy(apos);
35
+ });
36
+
37
+ // EXISTENCE
38
+
39
+ it('should initialize', async function() {
40
+ assert(apos);
31
41
 
32
42
  assert(apos.modules['@apostrophecms/login']);
33
43
  assert(apos.user.safe.remove);
@@ -227,7 +237,7 @@ describe('Login', function() {
227
237
  assert(page.match(/logged out/));
228
238
  });
229
239
 
230
- it('Changing a user\'s password should invalidate sessions for that user', async function() {
240
+ it('changing a user\'s password should invalidate sessions for that user', async function() {
231
241
 
232
242
  const jar = apos.http.jar();
233
243
 
@@ -341,7 +351,7 @@ describe('Login', function() {
341
351
 
342
352
  });
343
353
 
344
- it('Changing a user\'s password should invalidate bearer tokens for that user', async function() {
354
+ it('changing a user\'s password should invalidate bearer tokens for that user', async function() {
345
355
 
346
356
  // Log in
347
357
  let response = await apos.http.post('/api/v1/@apostrophecms/login/login', {
@@ -447,4 +457,409 @@ describe('Login', function() {
447
457
  assert(!page2.match(/Bob Smith/));
448
458
  assert(page2.match(/System Task/));
449
459
  });
460
+
461
+ it('should validate POST /login/reset-request', async function() {
462
+ const jar = apos.http.jar();
463
+ await apos.http.get(
464
+ '/',
465
+ {
466
+ jar
467
+ }
468
+ );
469
+
470
+ await assert.rejects(() => apos.http.post(
471
+ '/api/v1/@apostrophecms/login/reset-request',
472
+ {
473
+ body: {
474
+ session: true
475
+ },
476
+ jar
477
+ }
478
+ ), {
479
+ status: 400
480
+ });
481
+ });
482
+
483
+ it('should hide sensitive exceptions POST /login/reset-request', async function() {
484
+ let log;
485
+ const orig = apos.util.error;
486
+ apos.util.error = (m) => {
487
+ log = m;
488
+ };
489
+ const jar = apos.http.jar();
490
+ await apos.http.get(
491
+ '/',
492
+ {
493
+ jar
494
+ }
495
+ );
496
+ await apos.http.post(
497
+ '/api/v1/@apostrophecms/login/reset-request',
498
+ {
499
+ body: {
500
+ email: 'invalidUser',
501
+ session: true
502
+ },
503
+ jar
504
+ }
505
+ );
506
+ assert.match(log, /invalidUser/);
507
+
508
+ const user = apos.user.newInstance();
509
+ user.title = 'noEmail';
510
+ user.username = 'noEmail';
511
+ user.password = 'secret';
512
+ user.role = 'guest';
513
+ await apos.user.insert(apos.task.getReq(), user);
514
+
515
+ await apos.http.post(
516
+ '/api/v1/@apostrophecms/login/reset-request',
517
+ {
518
+ body: {
519
+ email: 'noEmail',
520
+ session: true
521
+ },
522
+ jar
523
+ }
524
+ );
525
+ assert.match(log, /noEmail/);
526
+
527
+ apos.util.error = orig;
528
+ });
529
+
530
+ it('should reset password POST /login/reset-request (request)', async function() {
531
+ let args;
532
+ const orig = apos.login.email;
533
+ apos.login.email = (req, ...a) => {
534
+ args = a;
535
+ };
536
+ const jar = apos.http.jar();
537
+ await apos.http.get(
538
+ '/',
539
+ {
540
+ jar
541
+ }
542
+ );
543
+
544
+ let user = apos.user.newInstance();
545
+ user.title = 'resetUser';
546
+ user.email = 'resetUser@example.com';
547
+ user.username = 'resetUser';
548
+ user.password = 'secret';
549
+ user.role = 'guest';
550
+ user = await apos.user.insert(apos.task.getReq(), user);
551
+ resetUserId = user._id;
552
+
553
+ await apos.http.post(
554
+ '/api/v1/@apostrophecms/login/reset-request',
555
+ {
556
+ body: {
557
+ email: 'resetUser',
558
+ session: true
559
+ },
560
+ jar
561
+ }
562
+ );
563
+
564
+ {
565
+ assert(Array.isArray(args));
566
+ const [ template, data, opts ] = args;
567
+ assert(template);
568
+ assert.deepEqual(data.user._id, user._id);
569
+ assert(data.user.passwordResetAt);
570
+ assert.match(data.url, /\/login\?reset=/);
571
+ assert.match(data.url, /&email=/);
572
+ assert(data.site);
573
+ assert.equal(opts.to, user.email);
574
+ assert(opts.subject);
575
+ }
576
+
577
+ await apos.http.post(
578
+ '/api/v1/@apostrophecms/login/reset-request',
579
+ {
580
+ body: {
581
+ email: 'resetUser@example.com',
582
+ session: true
583
+ },
584
+ jar
585
+ }
586
+ );
587
+
588
+ {
589
+ assert(Array.isArray(args));
590
+ const [ template, data, opts ] = args;
591
+ assert(template);
592
+ assert.deepEqual(data.user._id, user._id);
593
+ assert(data.user.passwordResetAt);
594
+ assert.match(data.url, /\/login\?reset=/);
595
+ assert.match(data.url, /&email=/);
596
+ assert(data.site);
597
+ assert.equal(opts.to, user.email);
598
+ assert(opts.subject);
599
+
600
+ // Safe the token for the reset tests
601
+ const url = new URL(data.url);
602
+ resetToken = url.searchParams.get('reset');
603
+ }
604
+
605
+ apos.login.email = orig;
606
+ });
607
+
608
+ it('should reset password GET /login/reset (validate)', async function() {
609
+ const user = await apos.doc.db.findOne({ _id: resetUserId });
610
+
611
+ // Fail
612
+ await assert.rejects(() => apos.http.get(
613
+ '/api/v1/@apostrophecms/login/reset',
614
+ {
615
+ qs: {}
616
+ }
617
+ ), {
618
+ status: 400
619
+ });
620
+ await assert.rejects(() => apos.http.get(
621
+ '/api/v1/@apostrophecms/login/reset',
622
+ {
623
+ qs: {
624
+ reset: 'invalid',
625
+ email: user.username
626
+ }
627
+ }
628
+ ), {
629
+ status: 400
630
+ });
631
+ await assert.rejects(() => apos.http.get(
632
+ '/api/v1/@apostrophecms/login/reset',
633
+ {
634
+ qs: {
635
+ reset: resetToken,
636
+ email: 'invalid'
637
+ }
638
+ }
639
+ ), {
640
+ status: 400
641
+ });
642
+
643
+ // Success
644
+ await apos.http.get(
645
+ '/api/v1/@apostrophecms/login/reset',
646
+ {
647
+ qs: {
648
+ reset: resetToken,
649
+ email: user.username
650
+ }
651
+ }
652
+ );
653
+
654
+ await apos.http.get(
655
+ '/api/v1/@apostrophecms/login/reset',
656
+ {
657
+ qs: {
658
+ reset: resetToken,
659
+ email: user.email
660
+ }
661
+ }
662
+ );
663
+ });
664
+
665
+ it('should reset password POST /login/reset (validate & reset)', async function() {
666
+ const jar = apos.http.jar();
667
+ const user = await apos.doc.db.findOne({ _id: resetUserId });
668
+ await apos.http.get(
669
+ '/',
670
+ {
671
+ jar
672
+ }
673
+ );
674
+
675
+ // Validate
676
+ await assert.rejects(() => apos.http.post(
677
+ '/api/v1/@apostrophecms/login/reset',
678
+ {
679
+ body: {
680
+ session: true
681
+ },
682
+ jar
683
+ }
684
+ ), {
685
+ status: 400
686
+ });
687
+ await assert.rejects(() => apos.http.post(
688
+ '/api/v1/@apostrophecms/login/reset',
689
+ {
690
+ body: {
691
+ reset: 'invalid',
692
+ email: user.email,
693
+ password: 'new more secret',
694
+ session: true
695
+ },
696
+ jar
697
+ }
698
+ ), {
699
+ status: 400
700
+ });
701
+ await assert.rejects(() => apos.http.post(
702
+ '/api/v1/@apostrophecms/login/reset',
703
+ {
704
+ body: {
705
+ reset: resetToken,
706
+ email: 'invalid',
707
+ password: 'new more secret',
708
+ session: true
709
+ },
710
+ jar
711
+ }
712
+ ), {
713
+ status: 400
714
+ });
715
+ await assert.rejects(() => apos.http.post(
716
+ '/api/v1/@apostrophecms/login/reset',
717
+ {
718
+ body: {
719
+ reset: resetToken,
720
+ email: user.email,
721
+ password: '',
722
+ session: true
723
+ },
724
+ jar
725
+ }
726
+ ), {
727
+ status: 400
728
+ });
729
+ await assert.rejects(() => apos.http.post(
730
+ '/api/v1/@apostrophecms/login/reset',
731
+ {
732
+ body: {
733
+ // explicit check for boolean cheat!
734
+ reset: false,
735
+ email: user.email,
736
+ password: 'new more secret',
737
+ session: true
738
+ },
739
+ jar
740
+ }
741
+ ), {
742
+ status: 400
743
+ });
744
+
745
+ // Reset
746
+ await apos.http.post(
747
+ '/api/v1/@apostrophecms/login/reset',
748
+ {
749
+ body: {
750
+ reset: resetToken,
751
+ email: user.email,
752
+ password: 'new more secret',
753
+ session: true
754
+ },
755
+ jar
756
+ }
757
+ );
758
+
759
+ // Can not reset anymore
760
+ await assert.rejects(() => apos.http.post(
761
+ '/api/v1/@apostrophecms/login/reset',
762
+ {
763
+ body: {
764
+ reset: resetToken,
765
+ email: user.email,
766
+ password: 'new even more secret',
767
+ session: true
768
+ },
769
+ jar
770
+ }
771
+ ), {
772
+ status: 400
773
+ });
774
+
775
+ // Login with the new password
776
+ await apos.http.post(
777
+ '/api/v1/@apostrophecms/login/login',
778
+ {
779
+ body: {
780
+ username: user.email,
781
+ password: 'new more secret',
782
+ session: true
783
+ },
784
+ jar
785
+ }
786
+ );
787
+ const page = await apos.http.get(
788
+ '/',
789
+ {
790
+ jar
791
+ }
792
+ );
793
+ assert(page.match(/logged in/));
794
+ });
795
+
796
+ it('should find user by reset data', async function() {
797
+ let user = apos.user.newInstance();
798
+ user.title = 'getResetUser';
799
+ user.email = 'getResetUser@example.com';
800
+ user.username = 'getResetUser';
801
+ user.password = 'secret';
802
+ user.role = 'guest';
803
+ user = await apos.user.insert(apos.task.getReq(), user);
804
+
805
+ // Find by email
806
+ {
807
+ const found = await apos.login.getPasswordResetUser(user.email);
808
+ assert.equal(found._id, user._id);
809
+ }
810
+ // Find by username
811
+ {
812
+ const found = await apos.login.getPasswordResetUser(user.username);
813
+ assert.equal(found._id, user._id);
814
+ }
815
+ // Fail with no token
816
+ await assert.rejects(
817
+ () => apos.login.getPasswordResetUser(user.username, ''),
818
+ {
819
+ message: 'invalid'
820
+ }
821
+ );
822
+ await assert.rejects(
823
+ () => apos.login.getPasswordResetUser(user.username, null),
824
+ {
825
+ message: 'invalid'
826
+ }
827
+ );
828
+ await assert.rejects(
829
+ () => apos.login.getPasswordResetUser(user.username, 'invalid'),
830
+ {
831
+ message: 'notfound'
832
+ }
833
+ );
834
+
835
+ user.passwordReset = 'secret';
836
+ user.passwordResetAt = new Date();
837
+ user = await apos.user.update(apos.task.getReq(), user);
838
+ // Find by email and validate token
839
+ {
840
+ const found = await apos.login.getPasswordResetUser(user.email, 'secret');
841
+ assert.equal(found._id, user._id);
842
+ }
843
+ // // Find by username and validate token
844
+ {
845
+ const found = await apos.login.getPasswordResetUser(user.username, 'secret');
846
+ assert.equal(found._id, user._id);
847
+ }
848
+ await assert.rejects(
849
+ () => apos.login.getPasswordResetUser(user.username, 'invalid'),
850
+ {
851
+ message: 'Incorrect passwordReset'
852
+ }
853
+ );
854
+
855
+ // Expired
856
+ user.passwordResetAt = new Date(0);
857
+ user = await apos.user.update(apos.task.getReq(), user);
858
+ await assert.rejects(
859
+ () => apos.login.getPasswordResetUser(user.username, 'invalid'),
860
+ {
861
+ message: 'notfound'
862
+ }
863
+ );
864
+ });
450
865
  });
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ options: {
3
+ label: 'Company bundle override'
4
+ },
5
+ webpack: {
6
+ bundles: {
7
+ company: {}
8
+ }
9
+ }
10
+ };
@@ -0,0 +1,3 @@
1
+ export default () => {
2
+ console.log('BUNDLE_OVERRIDE_COMPANY');
3
+ };
@@ -0,0 +1,3 @@
1
+ .override-company {
2
+ font-weight: bold;
3
+ }
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ options: {
3
+ label: 'Bundle edge case test'
4
+ },
5
+ webpack: {
6
+ bundles: {
7
+ edge: {}
8
+ }
9
+ }
10
+ };
@@ -0,0 +1,3 @@
1
+ export default () => {
2
+ console.log('BUNDLE_EDGE');
3
+ };
@@ -0,0 +1,3 @@
1
+ .edge {
2
+ font-weight: bold;
3
+ }
@@ -1,7 +1,8 @@
1
1
  module.exports = {
2
- extend: '@apostrophecms/piece-page-type',
2
+ extend: 'bundle-page-type',
3
3
  webpack: {
4
4
  bundles: {
5
+ main: {},
5
6
  extra: {
6
7
  templates: [ 'show' ]
7
8
  }
@@ -1 +1,3 @@
1
- export default () => {};
1
+ export default () => {
2
+ console.log('BUNDLE_EXTRA_PAGE');
3
+ };
@@ -0,0 +1,3 @@
1
+ .extra-page {
2
+ font-weight: bold;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default () => {
2
+ console.log('BUNDLE_MAIN_PAGE');
3
+ };
@@ -0,0 +1,3 @@
1
+ .main-page {
2
+ font-weight: bold;
3
+ }
@@ -0,0 +1,12 @@
1
+ module.exports = {
2
+ extend: '@apostrophecms/piece-page-type',
3
+ webpack: {
4
+ bundles: {
5
+ main: {},
6
+ another: {}
7
+ }
8
+ },
9
+ options: {
10
+ label: 'Bundle base page type'
11
+ }
12
+ };
@@ -0,0 +1,3 @@
1
+ export default () => {
2
+ console.log('BUNDLE_ANOTHER_PAGE_TYPE');
3
+ };
@@ -0,0 +1,3 @@
1
+ export default () => {
2
+ console.log('BUNDLE_INDEX_PAGE_TYPE');
3
+ };
@@ -0,0 +1,3 @@
1
+ .index-page-type {
2
+ font-weight: bold;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default () => {
2
+ console.log('BUNDLE_MAIN_PAGE_TYPE');
3
+ };
@@ -0,0 +1,3 @@
1
+ .main-page-type {
2
+ font-weight: bold;
3
+ }
@@ -1 +1,3 @@
1
- export default () => {};
1
+ export default () => {
2
+ console.log('BUNDLE_WIDGET_EXTRA2');
3
+ };
package/test-lib/test.js CHANGED
@@ -29,7 +29,15 @@ const packageJsonInfo = {
29
29
  }
30
30
  };
31
31
  for (const dir of dirs) {
32
- packageJsonInfo.dependencies[dir] = '1.0.0';
32
+ // Add namespaced modules support
33
+ if (dir.startsWith('@')) {
34
+ const submodules = fs.readdirSync(path.join(extras, dir));
35
+ for (const submodule of submodules) {
36
+ packageJsonInfo.dependencies[`${dir}/${submodule}`] = '1.0.0';
37
+ }
38
+ } else {
39
+ packageJsonInfo.dependencies[dir] = '1.0.0';
40
+ }
33
41
  }
34
42
 
35
43
  fs.writeFileSync(packageJson, JSON.stringify(packageJsonInfo, null, ' '));