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.
- package/CHANGELOG.md +35 -0
- package/index.js +3 -0
- package/lib/stream-proxy.js +49 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +2 -11
- package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +38 -6
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +12 -1
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +111 -41
- package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +1 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +22 -10
- package/modules/@apostrophecms/area/ui/apos/logic/AposAreaEditor.js +40 -0
- package/modules/@apostrophecms/asset/index.js +3 -2
- package/modules/@apostrophecms/attachment/index.js +270 -0
- package/modules/@apostrophecms/doc/index.js +8 -2
- package/modules/@apostrophecms/doc-type/index.js +81 -1
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +18 -2
- package/modules/@apostrophecms/express/index.js +30 -1
- package/modules/@apostrophecms/file/index.js +71 -6
- package/modules/@apostrophecms/i18n/index.js +20 -1
- package/modules/@apostrophecms/image/index.js +11 -0
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +31 -6
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +12 -10
- package/modules/@apostrophecms/login/index.js +43 -11
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +2 -1
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +5 -0
- package/modules/@apostrophecms/page/index.js +9 -11
- package/modules/@apostrophecms/page-type/index.js +6 -1
- package/modules/@apostrophecms/piece-page-type/index.js +100 -13
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +1 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +28 -12
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +1 -1
- package/modules/@apostrophecms/styles/lib/apiRoutes.js +25 -5
- package/modules/@apostrophecms/styles/lib/handlers.js +19 -0
- package/modules/@apostrophecms/styles/lib/methods.js +35 -12
- package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +7 -2
- package/modules/@apostrophecms/task/index.js +9 -1
- package/modules/@apostrophecms/template/views/outerLayoutBase.html +3 -0
- package/modules/@apostrophecms/ui/index.js +2 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposButtonGroup.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +5 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +5 -0
- package/modules/@apostrophecms/ui/ui/apos/lib/vue.js +2 -0
- package/modules/@apostrophecms/ui/ui/apos/stores/widget.js +12 -7
- package/modules/@apostrophecms/ui/ui/apos/stores/widgetGraph.js +461 -0
- package/modules/@apostrophecms/ui/ui/apos/universal/graph.js +452 -0
- package/modules/@apostrophecms/ui/ui/apos/universal/widgetGraph.js +10 -0
- package/modules/@apostrophecms/uploadfs/index.js +15 -1
- package/modules/@apostrophecms/url/index.js +419 -1
- package/package.json +6 -6
- package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
- package/test/external-front.js +1 -0
- package/test/files.js +135 -0
- package/test/login-requirements.js +145 -3
- package/test/static-build.js +2701 -0
- 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
|
+
}
|