apostrophe 3.15.0 → 3.16.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 +13 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBar.vue +2 -2
- package/modules/@apostrophecms/admin-bar/ui/src/index.js +26 -0
- package/modules/@apostrophecms/login/index.js +21 -1
- package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +2 -0
- package/modules/@apostrophecms/module/index.js +26 -0
- package/modules/@apostrophecms/page/index.js +19 -1
- package/modules/@apostrophecms/piece-type/index.js +10 -0
- package/modules/@apostrophecms/task/index.js +6 -0
- package/modules/@apostrophecms/template/index.js +1 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +1 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +1 -0
- package/modules/@apostrophecms/user/index.js +6 -1
- package/package.json +1 -1
- package/test/login.js +62 -1
- package/test/modules/@apostrophecms/home-page/views/page.html +1 -1
- package/test/modules/@apostrophecms/user/index.js +7 -0
- package/test/pages-public-api.js +33 -0
- package/test/pages-rest.js +37 -1
- package/test/pages.js +177 -1
- package/test/pieces-public-api.js +33 -0
- package/test/pieces.js +118 -0
- package/test/templates.js +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.16.0 (2022-03-18)
|
|
4
|
+
|
|
5
|
+
### Adds
|
|
6
|
+
|
|
7
|
+
* Offers a simple way to set a Cache-Control max-age for Apostrophe page and GET REST API responses for pieces and pages.
|
|
8
|
+
* API keys and bearer tokens "win" over session cookies when both are present. Since API keys and bearer tokens are explicitly added to the request at hand, it never makes sense to ignore them in favor of a cookie, which is implicit. This also simplifies automated testing.
|
|
9
|
+
* `data-apos-test=""` selectors for certain elements frequently selected in QA tests, such as `data-apos-test="adminBar"`.
|
|
10
|
+
* To speed up functional tests, an `insecurePasswords` option has been added to the login module. This option is deliberately named to discourage use for any purpose other than functional tests in which repeated password hashing would unduly limit performance. Normally password hashing is intentionally difficult to slow down brute force attacks, especially if a database is compromised.
|
|
11
|
+
|
|
12
|
+
### Fixes
|
|
13
|
+
|
|
14
|
+
* `POST`ing a new child page with `_targetId: '_home'` now works properly in combination with `_position: 'lastChild'`.
|
|
15
|
+
|
|
3
16
|
## 3.15.0 (2022-03-02)
|
|
4
17
|
|
|
5
18
|
### Adds
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="apos-admin-bar-wrapper" :class="themeClass">
|
|
2
|
+
<div data-apos-test="adminBar" class="apos-admin-bar-wrapper" :class="themeClass">
|
|
3
3
|
<div class="apos-admin-bar-spacer" ref="spacer" />
|
|
4
4
|
<nav class="apos-admin-bar" ref="adminBar">
|
|
5
5
|
<div class="apos-admin-bar__row">
|
|
6
6
|
<AposLogoPadless class="apos-admin-bar__logo" />
|
|
7
7
|
<TheAposAdminBarMenu :items="items" />
|
|
8
8
|
<TheAposAdminBarLocale v-if="hasLocales()" />
|
|
9
|
-
<TheAposAdminBarUser class="apos-admin-bar__user" />
|
|
9
|
+
<TheAposAdminBarUser data-apos-test="authenticatedUserMenuTrigger" class="apos-admin-bar__user" />
|
|
10
10
|
</div>
|
|
11
11
|
<TheAposContextBar @mounted="setSpacer" />
|
|
12
12
|
</nav>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* If the page delivers a logged-out content but we know from session storage that a user is logged-in,
|
|
3
|
+
* we force-refresh the page to bypass the cache, in order to get the logged-in content (with admin UI).
|
|
4
|
+
*/
|
|
5
|
+
export default function() {
|
|
6
|
+
const isLoggedOutPageContent = !(apos.login && apos.login.user);
|
|
7
|
+
const isLoggedInCookie = apos.util.getCookie(`${self.apos.shortName}.loggedIn`) === 'true';
|
|
8
|
+
|
|
9
|
+
if (!isLoggedOutPageContent || !isLoggedInCookie) {
|
|
10
|
+
sessionStorage.setItem('aposRefreshedPages', '{}');
|
|
11
|
+
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const refreshedPages = JSON.parse(sessionStorage.aposRefreshedPages || '{}');
|
|
16
|
+
|
|
17
|
+
// Avoid potential refresh loops
|
|
18
|
+
if (!refreshedPages[location.href]) {
|
|
19
|
+
refreshedPages[location.href] = true;
|
|
20
|
+
sessionStorage.setItem('aposRefreshedPages', JSON.stringify(refreshedPages));
|
|
21
|
+
|
|
22
|
+
console.info('Received logged-out content from cache while logged-in, refreshing the page');
|
|
23
|
+
|
|
24
|
+
location.reload();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
@@ -45,6 +45,7 @@ const cuid = require('cuid');
|
|
|
45
45
|
const expressSession = require('express-session');
|
|
46
46
|
|
|
47
47
|
const loginAttemptsNamespace = '@apostrophecms/loginAttempt';
|
|
48
|
+
const loggedInCookieName = 'loggedIn';
|
|
48
49
|
|
|
49
50
|
module.exports = {
|
|
50
51
|
cascades: [ 'requirements' ],
|
|
@@ -151,6 +152,9 @@ module.exports = {
|
|
|
151
152
|
expireCookie.expires = new Date(0);
|
|
152
153
|
const name = self.apos.modules['@apostrophecms/express'].sessionOptions.name;
|
|
153
154
|
req.res.header('set-cookie', expireCookie.serialize(name, 'deleted'));
|
|
155
|
+
|
|
156
|
+
// TODO: get cookie name from config
|
|
157
|
+
req.res.cookie(`${self.apos.shortName}.${loggedInCookieName}`, 'false');
|
|
154
158
|
}
|
|
155
159
|
},
|
|
156
160
|
// invokes the `props(req, user)` function for the requirement specified by
|
|
@@ -789,7 +793,12 @@ module.exports = {
|
|
|
789
793
|
},
|
|
790
794
|
passportSession: {
|
|
791
795
|
before: '@apostrophecms/i18n',
|
|
792
|
-
middleware:
|
|
796
|
+
middleware: (() => {
|
|
797
|
+
// Wrap the passport middleware so that if the apikey or bearer token
|
|
798
|
+
// middleware already supplied req.user, that wins (explicit wins over implicit)
|
|
799
|
+
const passportSession = self.passport.session();
|
|
800
|
+
return (req, res, next) => req.user ? next() : passportSession(req, res, next);
|
|
801
|
+
})()
|
|
793
802
|
},
|
|
794
803
|
honorLoginInvalidBefore: {
|
|
795
804
|
before: '@apostrophecms/i18n',
|
|
@@ -812,6 +821,17 @@ module.exports = {
|
|
|
812
821
|
return next();
|
|
813
822
|
}
|
|
814
823
|
}
|
|
824
|
+
},
|
|
825
|
+
addLoggedInCookie: {
|
|
826
|
+
before: '@apostrophecms/i18n',
|
|
827
|
+
middleware(req, res, next) {
|
|
828
|
+
// TODO: get cookie name from config
|
|
829
|
+
const cookieName = `${self.apos.shortName}.${loggedInCookieName}`;
|
|
830
|
+
if (req.user && req.cookies[cookieName] !== 'true') {
|
|
831
|
+
res.cookie(cookieName, 'true');
|
|
832
|
+
}
|
|
833
|
+
return next();
|
|
834
|
+
}
|
|
815
835
|
}
|
|
816
836
|
};
|
|
817
837
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<transition name="fade-stage">
|
|
3
3
|
<div
|
|
4
4
|
class="apos-login apos-theme-dark"
|
|
5
|
+
data-apos-test="loginForm"
|
|
5
6
|
v-show="loaded"
|
|
6
7
|
:class="themeClass"
|
|
7
8
|
>
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
<!-- TODO -->
|
|
36
37
|
<!-- <a href="#" class="apos-login__link">Forgot Password</a> -->
|
|
37
38
|
<AposButton
|
|
39
|
+
data-apos-test="loginSubmit"
|
|
38
40
|
:busy="busy"
|
|
39
41
|
:disabled="disabled"
|
|
40
42
|
type="primary"
|
|
@@ -177,6 +177,11 @@ module.exports = {
|
|
|
177
177
|
return async function(req, res) {
|
|
178
178
|
try {
|
|
179
179
|
const result = await fn(req);
|
|
180
|
+
|
|
181
|
+
if (req.method === 'GET' && req.user) {
|
|
182
|
+
res.header('Cache-Control', 'no-store');
|
|
183
|
+
}
|
|
184
|
+
|
|
180
185
|
res.status(200);
|
|
181
186
|
res.send(result);
|
|
182
187
|
} catch (err) {
|
|
@@ -446,6 +451,27 @@ module.exports = {
|
|
|
446
451
|
);
|
|
447
452
|
},
|
|
448
453
|
|
|
454
|
+
setMaxAge(req, maxAge) {
|
|
455
|
+
if (typeof maxAge !== 'number') {
|
|
456
|
+
self.apos.util.warnDev(`"maxAge" property must be defined as a number in the "${self.__meta.name}" module's cache options"`);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// A cookie in session doesn't mean we can't cache, nor an empty flash or passport object.
|
|
461
|
+
// Other session properties must be assumed to be specific to the user, with a possible
|
|
462
|
+
// impact on the response, and thus mean this request must not be cached.
|
|
463
|
+
// Same rule as in [express-cache-on-demand](https://github.com/apostrophecms/express-cache-on-demand/blob/master/index.js#L102)
|
|
464
|
+
const isSessionClearForCaching = Object.entries(req.session).every(([ key, val ]) =>
|
|
465
|
+
key === 'cookie' || (
|
|
466
|
+
(key === 'flash' || key === 'passport') && _.isEmpty(val)
|
|
467
|
+
)
|
|
468
|
+
);
|
|
469
|
+
const isSafeToCache = !req.user && isSessionClearForCaching;
|
|
470
|
+
const cacheControlValue = isSafeToCache ? `max-age=${maxAge}` : 'no-store';
|
|
471
|
+
|
|
472
|
+
req.res.header('Cache-Control', cacheControlValue);
|
|
473
|
+
},
|
|
474
|
+
|
|
449
475
|
// Call from init once if this module implements the `getBrowserData` method.
|
|
450
476
|
// The data returned by `getBrowserData(req)` will then be available on
|
|
451
477
|
// `apos.modules['your-module-name']` in the browser.
|
|
@@ -119,6 +119,10 @@ module.exports = {
|
|
|
119
119
|
project: self.getAllProjection()
|
|
120
120
|
}).toObject();
|
|
121
121
|
|
|
122
|
+
if (self.options.cache && self.options.cache.api) {
|
|
123
|
+
self.setMaxAge(req, self.options.cache.api.maxAge);
|
|
124
|
+
}
|
|
125
|
+
|
|
122
126
|
if (!page) {
|
|
123
127
|
throw self.apos.error('notfound');
|
|
124
128
|
}
|
|
@@ -137,6 +141,11 @@ module.exports = {
|
|
|
137
141
|
}
|
|
138
142
|
} else {
|
|
139
143
|
const result = await self.getRestQuery(req).and({ level: 0 }).toObject();
|
|
144
|
+
|
|
145
|
+
if (self.options.cache && self.options.cache.api) {
|
|
146
|
+
self.setMaxAge(req, self.options.cache.api.maxAge);
|
|
147
|
+
}
|
|
148
|
+
|
|
140
149
|
if (!result) {
|
|
141
150
|
throw self.apos.error('notfound');
|
|
142
151
|
}
|
|
@@ -168,6 +177,11 @@ module.exports = {
|
|
|
168
177
|
self.publicApiCheck(req);
|
|
169
178
|
const criteria = self.getIdCriteria(_id);
|
|
170
179
|
const result = await self.getRestQuery(req).and(criteria).toObject();
|
|
180
|
+
|
|
181
|
+
if (self.options.cache && self.options.cache.api) {
|
|
182
|
+
self.setMaxAge(req, self.options.cache.api.maxAge);
|
|
183
|
+
}
|
|
184
|
+
|
|
171
185
|
if (!result) {
|
|
172
186
|
throw self.apos.error('notfound');
|
|
173
187
|
}
|
|
@@ -220,7 +234,7 @@ module.exports = {
|
|
|
220
234
|
}
|
|
221
235
|
|
|
222
236
|
return self.withLock(req, async () => {
|
|
223
|
-
const targetPage = await self.findForEditing(req, targetId ?
|
|
237
|
+
const targetPage = await self.findForEditing(req, targetId ? self.getIdCriteria(targetId) : { level: 0 }).ancestors(true).permission('edit').toObject();
|
|
224
238
|
if (!targetPage) {
|
|
225
239
|
throw self.apos.error('notfound');
|
|
226
240
|
}
|
|
@@ -1415,6 +1429,10 @@ database.`);
|
|
|
1415
1429
|
await self.emit('serveQuery', query);
|
|
1416
1430
|
req.data.bestPage = await query.toObject();
|
|
1417
1431
|
self.evaluatePageMatch(req);
|
|
1432
|
+
|
|
1433
|
+
if (self.options.cache && self.options.cache.page) {
|
|
1434
|
+
self.setMaxAge(req, self.options.cache.page.maxAge);
|
|
1435
|
+
}
|
|
1418
1436
|
},
|
|
1419
1437
|
// Normalize req.slug to account for unneeded trailing whitespace,
|
|
1420
1438
|
// trailing slashes other than the root, and double slash based open
|
|
@@ -200,6 +200,11 @@ module.exports = {
|
|
|
200
200
|
if (query.get('countsResults')) {
|
|
201
201
|
result.counts = query.get('countsResults');
|
|
202
202
|
}
|
|
203
|
+
|
|
204
|
+
if (self.options.cache && self.options.cache.api) {
|
|
205
|
+
self.setMaxAge(req, self.options.cache.api.maxAge);
|
|
206
|
+
}
|
|
207
|
+
|
|
203
208
|
return result;
|
|
204
209
|
}
|
|
205
210
|
],
|
|
@@ -209,6 +214,11 @@ module.exports = {
|
|
|
209
214
|
_id = self.inferIdLocaleAndMode(req, _id);
|
|
210
215
|
self.publicApiCheck(req);
|
|
211
216
|
const doc = await self.getRestQuery(req).and({ _id }).toObject();
|
|
217
|
+
|
|
218
|
+
if (self.options.cache && self.options.cache.api) {
|
|
219
|
+
self.setMaxAge(req, self.options.cache.api.maxAge);
|
|
220
|
+
}
|
|
221
|
+
|
|
212
222
|
if (!doc) {
|
|
213
223
|
throw self.apos.error('notfound');
|
|
214
224
|
}
|
|
@@ -432,7 +432,12 @@ module.exports = {
|
|
|
432
432
|
|
|
433
433
|
// Initialize the [credential](https://npmjs.org/package/credential) module.
|
|
434
434
|
initializeCredential() {
|
|
435
|
-
self.pw = credential(
|
|
435
|
+
self.pw = credential({
|
|
436
|
+
// For efficient unit tests only. Reducing the work factor
|
|
437
|
+
// for actual credentials increases the speed of brute force attacks
|
|
438
|
+
// if the database is ever compromised
|
|
439
|
+
work: self.options.insecurePasswords ? 0.01 : 1
|
|
440
|
+
});
|
|
436
441
|
},
|
|
437
442
|
|
|
438
443
|
// Implement the `@apostrophecms/user:add` command line task.
|
package/package.json
CHANGED
package/test/login.js
CHANGED
|
@@ -15,7 +15,18 @@ describe('Login', function() {
|
|
|
15
15
|
|
|
16
16
|
it('should initialize', async function() {
|
|
17
17
|
apos = await t.create({
|
|
18
|
-
root: module
|
|
18
|
+
root: module,
|
|
19
|
+
modules: {
|
|
20
|
+
'@apostrophecms/express': {
|
|
21
|
+
options: {
|
|
22
|
+
apiKeys: {
|
|
23
|
+
adminApiKey: {
|
|
24
|
+
role: 'admin'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
19
30
|
});
|
|
20
31
|
|
|
21
32
|
assert(apos.modules['@apostrophecms/login']);
|
|
@@ -39,6 +50,14 @@ describe('Login', function() {
|
|
|
39
50
|
assert(apos.user.insert);
|
|
40
51
|
const doc = await apos.user.insert(apos.task.getReq(), user);
|
|
41
52
|
assert(doc._id);
|
|
53
|
+
|
|
54
|
+
const user2 = apos.user.newInstance();
|
|
55
|
+
user2.title = 'Bob Smith';
|
|
56
|
+
user2.username = 'BobSmith';
|
|
57
|
+
user2.password = 'bobsmith';
|
|
58
|
+
user2.email = 'bobsmith@aol.com';
|
|
59
|
+
user2.role = 'guest';
|
|
60
|
+
await apos.user.insert(apos.task.getReq(), user2);
|
|
42
61
|
});
|
|
43
62
|
|
|
44
63
|
it('should throttle login attempts and show a proper error when the limit is reached', async function () {
|
|
@@ -84,6 +103,8 @@ describe('Login', function() {
|
|
|
84
103
|
});
|
|
85
104
|
|
|
86
105
|
it('should be able to login a user with their username', async function() {
|
|
106
|
+
const getLoggedInCookieValue =
|
|
107
|
+
jar => jar.toJSON().cookies.find(cookie => cookie.key === `${apos.options.shortName}.loggedIn`).value;
|
|
87
108
|
|
|
88
109
|
const jar = apos.http.jar();
|
|
89
110
|
|
|
@@ -118,6 +139,8 @@ describe('Login', function() {
|
|
|
118
139
|
);
|
|
119
140
|
|
|
120
141
|
assert(page.match(/logged in/));
|
|
142
|
+
assert(page.match(/Harry Putter/));
|
|
143
|
+
assert(getLoggedInCookieValue(jar) === 'true');
|
|
121
144
|
|
|
122
145
|
// otherwise logins are not remembered in a session
|
|
123
146
|
await apos.http.post(
|
|
@@ -141,6 +164,7 @@ describe('Login', function() {
|
|
|
141
164
|
|
|
142
165
|
// are we back to being able to log in?
|
|
143
166
|
assert(page.match(/logged out/));
|
|
167
|
+
assert(getLoggedInCookieValue(jar) === 'false');
|
|
144
168
|
});
|
|
145
169
|
|
|
146
170
|
it('should be able to login a user with their email', async function() {
|
|
@@ -386,4 +410,41 @@ describe('Login', function() {
|
|
|
386
410
|
|
|
387
411
|
});
|
|
388
412
|
|
|
413
|
+
it('api key should beat session when both are present', async function() {
|
|
414
|
+
const jar = apos.http.jar();
|
|
415
|
+
await apos.http.post(
|
|
416
|
+
'/api/v1/@apostrophecms/login/login',
|
|
417
|
+
{
|
|
418
|
+
method: 'POST',
|
|
419
|
+
body: {
|
|
420
|
+
username: 'BobSmith',
|
|
421
|
+
password: 'bobsmith',
|
|
422
|
+
session: true
|
|
423
|
+
},
|
|
424
|
+
jar
|
|
425
|
+
}
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const page = await apos.http.get(
|
|
429
|
+
'/',
|
|
430
|
+
{
|
|
431
|
+
jar
|
|
432
|
+
}
|
|
433
|
+
);
|
|
434
|
+
assert(page.match(/logged in/));
|
|
435
|
+
assert(page.match(/Bob Smith/));
|
|
436
|
+
|
|
437
|
+
const page2 = await apos.http.get(
|
|
438
|
+
'/',
|
|
439
|
+
{
|
|
440
|
+
jar,
|
|
441
|
+
headers: {
|
|
442
|
+
Authorization: 'ApiKey adminApiKey'
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
);
|
|
446
|
+
assert(page2.match(/logged in/));
|
|
447
|
+
assert(!page2.match(/Bob Smith/));
|
|
448
|
+
assert(page2.match(/System Task/));
|
|
449
|
+
});
|
|
389
450
|
});
|
package/test/pages-public-api.js
CHANGED
|
@@ -68,4 +68,37 @@ describe('Pages Public API', function() {
|
|
|
68
68
|
// But projection did apply
|
|
69
69
|
assert(!home.searchSummary);
|
|
70
70
|
});
|
|
71
|
+
|
|
72
|
+
it('should not set a "max-age" cache-control value when retrieving pages, when cache option is not set, with a public API projection', async () => {
|
|
73
|
+
apos.page.options.publicApiProjection = {
|
|
74
|
+
title: 1,
|
|
75
|
+
_url: 1
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const response1 = await apos.http.get('/api/v1/@apostrophecms/page', { fullResponse: true });
|
|
79
|
+
const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${response1.body._id}`, { fullResponse: true });
|
|
80
|
+
|
|
81
|
+
assert(response1.headers['cache-control'] === undefined);
|
|
82
|
+
assert(response2.headers['cache-control'] === undefined);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should set a "max-age" cache-control value when retrieving pages, with a public API projection', async () => {
|
|
86
|
+
apos.page.options.publicApiProjection = {
|
|
87
|
+
title: 1,
|
|
88
|
+
_url: 1
|
|
89
|
+
};
|
|
90
|
+
apos.page.options.cache = {
|
|
91
|
+
api: {
|
|
92
|
+
maxAge: 1111
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const response1 = await apos.http.get('/api/v1/@apostrophecms/page', { fullResponse: true });
|
|
97
|
+
const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${response1.body._id}`, { fullResponse: true });
|
|
98
|
+
|
|
99
|
+
assert(response1.headers['cache-control'] === 'max-age=1111');
|
|
100
|
+
assert(response2.headers['cache-control'] === 'max-age=1111');
|
|
101
|
+
|
|
102
|
+
delete apos.page.options.cache;
|
|
103
|
+
});
|
|
71
104
|
});
|
package/test/pages-rest.js
CHANGED
|
@@ -282,6 +282,15 @@ describe('Pages REST', function() {
|
|
|
282
282
|
assert(draftItems.result.ok === 1);
|
|
283
283
|
assert(draftItems.insertedCount === 7);
|
|
284
284
|
|
|
285
|
+
// Change the rank of the archive pages to preserve the constraint that it comes last
|
|
286
|
+
await apos.doc.db.updateMany({
|
|
287
|
+
type: '@apostrophecms/archive-page'
|
|
288
|
+
}, {
|
|
289
|
+
$set: {
|
|
290
|
+
rank: 3
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
285
294
|
const items = await apos.doc.db.insertMany(testItems);
|
|
286
295
|
|
|
287
296
|
assert(items.result.ok === 1);
|
|
@@ -430,7 +439,7 @@ describe('Pages REST', function() {
|
|
|
430
439
|
// Is the rank correct?
|
|
431
440
|
const home = await apos.http.get('/api/v1/@apostrophecms/page', {});
|
|
432
441
|
assert(home._children);
|
|
433
|
-
assert(home._children[
|
|
442
|
+
assert(home._children[1]._id === 'cousin:en:published');
|
|
434
443
|
});
|
|
435
444
|
|
|
436
445
|
it('is able to move root/cousin before root/parent/child', async function() {
|
|
@@ -587,6 +596,33 @@ describe('Pages REST', function() {
|
|
|
587
596
|
assert.strictEqual(page.path, `${homeId.replace(':en:published', '')}/another-parent/parent/sibling`);
|
|
588
597
|
});
|
|
589
598
|
|
|
599
|
+
it('is able to make a subpage of the homepage at the last index with _position: lastChild', async function() {
|
|
600
|
+
const body = {
|
|
601
|
+
slug: '/third-new',
|
|
602
|
+
visibility: 'public',
|
|
603
|
+
type: 'test-page',
|
|
604
|
+
title: 'Third New',
|
|
605
|
+
_targetId: '_home',
|
|
606
|
+
_position: 'lastChild'
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const page = await apos.http.post('/api/v1/@apostrophecms/page', {
|
|
610
|
+
body,
|
|
611
|
+
jar
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
assert(page);
|
|
615
|
+
assert(page.title === 'Third New');
|
|
616
|
+
// Is the path generally correct?
|
|
617
|
+
assert.strictEqual(page.path, `${homeId.replace(':en:published', '')}/${page._id.replace(':en:published', '')}`);
|
|
618
|
+
const home = await apos.http.get('/api/v1/@apostrophecms/page?children=1', {
|
|
619
|
+
jar
|
|
620
|
+
});
|
|
621
|
+
assert(home);
|
|
622
|
+
assert(home._children);
|
|
623
|
+
assert(home._children[3]._id === page._id);
|
|
624
|
+
});
|
|
625
|
+
|
|
590
626
|
it('can use PUT to modify a page', async function() {
|
|
591
627
|
const page = await apos.http.get('/api/v1/@apostrophecms/page/sibling:en:published', { jar });
|
|
592
628
|
assert(page);
|
package/test/pages.js
CHANGED
|
@@ -4,6 +4,7 @@ const _ = require('lodash');
|
|
|
4
4
|
|
|
5
5
|
let apos;
|
|
6
6
|
let homeId;
|
|
7
|
+
const apiKey = 'this is a test api key';
|
|
7
8
|
|
|
8
9
|
describe('Pages', function() {
|
|
9
10
|
|
|
@@ -19,6 +20,15 @@ describe('Pages', function() {
|
|
|
19
20
|
apos = await t.create({
|
|
20
21
|
root: module,
|
|
21
22
|
modules: {
|
|
23
|
+
'@apostrophecms/express': {
|
|
24
|
+
options: {
|
|
25
|
+
apiKeys: {
|
|
26
|
+
[apiKey]: {
|
|
27
|
+
role: 'admin'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
22
32
|
'@apostrophecms/page': {
|
|
23
33
|
options: {
|
|
24
34
|
park: [],
|
|
@@ -31,7 +41,11 @@ describe('Pages', function() {
|
|
|
31
41
|
name: 'test-page',
|
|
32
42
|
label: 'Test Page'
|
|
33
43
|
}
|
|
34
|
-
]
|
|
44
|
+
],
|
|
45
|
+
publicApiProjection: {
|
|
46
|
+
title: 1,
|
|
47
|
+
_url: 1
|
|
48
|
+
}
|
|
35
49
|
}
|
|
36
50
|
},
|
|
37
51
|
'test-page': {
|
|
@@ -481,4 +495,166 @@ describe('Pages', function() {
|
|
|
481
495
|
}
|
|
482
496
|
});
|
|
483
497
|
|
|
498
|
+
it('should not set a cache-control value when retrieving pages, when cache option is not set', async () => {
|
|
499
|
+
const response1 = await apos.http.get('/api/v1/@apostrophecms/page', { fullResponse: true });
|
|
500
|
+
const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${homeId}`, { fullResponse: true });
|
|
501
|
+
|
|
502
|
+
assert(response1.headers['cache-control'] === undefined);
|
|
503
|
+
assert(response2.headers['cache-control'] === undefined);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('should not set a cache-control value when retrieving pages, when "api" cache option is not set', async () => {
|
|
507
|
+
apos.page.options.cache = {
|
|
508
|
+
page: {
|
|
509
|
+
maxAge: 5555
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const response1 = await apos.http.get('/api/v1/@apostrophecms/page', { fullResponse: true });
|
|
514
|
+
const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${homeId}`, { fullResponse: true });
|
|
515
|
+
|
|
516
|
+
assert(response1.headers['cache-control'] === undefined);
|
|
517
|
+
assert(response2.headers['cache-control'] === undefined);
|
|
518
|
+
|
|
519
|
+
delete apos.page.options.cache;
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('should set a "max-age" cache-control value when retrieving pieces, when "api" cache option is set', async () => {
|
|
523
|
+
apos.page.options.cache = {
|
|
524
|
+
api: {
|
|
525
|
+
maxAge: 4444
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const response1 = await apos.http.get('/api/v1/@apostrophecms/page', { fullResponse: true });
|
|
530
|
+
const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${homeId}`, { fullResponse: true });
|
|
531
|
+
|
|
532
|
+
assert(response1.headers['cache-control'] === 'max-age=4444');
|
|
533
|
+
assert(response2.headers['cache-control'] === 'max-age=4444');
|
|
534
|
+
|
|
535
|
+
delete apos.page.options.cache;
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('should set a "no-store" cache-control value when retrieving pages, when user is connected', async () => {
|
|
539
|
+
const jar = apos.http.jar();
|
|
540
|
+
const user = apos.user.newInstance();
|
|
541
|
+
|
|
542
|
+
user.title = 'admin';
|
|
543
|
+
user.username = 'admin';
|
|
544
|
+
user.password = 'admin';
|
|
545
|
+
user.email = 'ad@min.com';
|
|
546
|
+
user.role = 'admin';
|
|
547
|
+
|
|
548
|
+
await apos.user.insert(apos.task.getReq(), user);
|
|
549
|
+
await apos.http.post('/api/v1/@apostrophecms/login/login', {
|
|
550
|
+
body: {
|
|
551
|
+
username: 'admin',
|
|
552
|
+
password: 'admin',
|
|
553
|
+
session: true
|
|
554
|
+
},
|
|
555
|
+
jar
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const response1 = await apos.http.get('/api/v1/@apostrophecms/page', {
|
|
559
|
+
fullResponse: true,
|
|
560
|
+
jar
|
|
561
|
+
});
|
|
562
|
+
const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${homeId}`, {
|
|
563
|
+
fullResponse: true,
|
|
564
|
+
jar
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
assert(response1.headers['cache-control'] === 'no-store');
|
|
568
|
+
assert(response2.headers['cache-control'] === 'no-store');
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('should set a "no-store" cache-control value when retrieving pages, when "api" cache option is set, when user is connected', async () => {
|
|
572
|
+
apos.page.options.cache = {
|
|
573
|
+
api: {
|
|
574
|
+
maxAge: 4444
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const jar = apos.http.jar();
|
|
579
|
+
|
|
580
|
+
await apos.http.post('/api/v1/@apostrophecms/login/login', {
|
|
581
|
+
body: {
|
|
582
|
+
username: 'admin',
|
|
583
|
+
password: 'admin',
|
|
584
|
+
session: true
|
|
585
|
+
},
|
|
586
|
+
jar
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const response1 = await apos.http.get('/api/v1/@apostrophecms/page', {
|
|
590
|
+
fullResponse: true,
|
|
591
|
+
jar
|
|
592
|
+
});
|
|
593
|
+
const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${homeId}`, {
|
|
594
|
+
fullResponse: true,
|
|
595
|
+
jar
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
assert(response1.headers['cache-control'] === 'no-store');
|
|
599
|
+
assert(response2.headers['cache-control'] === 'no-store');
|
|
600
|
+
|
|
601
|
+
delete apos.page.options.cache;
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('should set a "no-store" cache-control value when retrieving pages, when user is connected using an api key', async () => {
|
|
605
|
+
const response1 = await apos.http.get(`/api/v1/@apostrophecms/page?apiKey=${apiKey}`, { fullResponse: true });
|
|
606
|
+
const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${homeId}?apiKey=${apiKey}`, { fullResponse: true });
|
|
607
|
+
|
|
608
|
+
assert(response1.headers['cache-control'] === 'no-store');
|
|
609
|
+
assert(response2.headers['cache-control'] === 'no-store');
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('should set a "no-store" cache-control value when retrieving pages, when "api" cache option is set, when user is connected using an api key', async () => {
|
|
613
|
+
apos.page.options.cache = {
|
|
614
|
+
api: {
|
|
615
|
+
maxAge: 4444
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const response1 = await apos.http.get(`/api/v1/@apostrophecms/page?apiKey=${apiKey}`, { fullResponse: true });
|
|
620
|
+
const response2 = await apos.http.get(`/api/v1/@apostrophecms/page/${homeId}?apiKey=${apiKey}`, { fullResponse: true });
|
|
621
|
+
|
|
622
|
+
assert(response1.headers['cache-control'] === 'no-store');
|
|
623
|
+
assert(response2.headers['cache-control'] === 'no-store');
|
|
624
|
+
|
|
625
|
+
delete apos.page.options.cache;
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it('should not set a cache-control value when serving a page, when cache option is not set', async () => {
|
|
629
|
+
const response = await apos.http.get('/', { fullResponse: true });
|
|
630
|
+
|
|
631
|
+
assert(response.headers['cache-control'] === undefined);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('should not set a cache-control value when serving a page, when "page" cache option is not set', async () => {
|
|
635
|
+
apos.page.options.cache = {
|
|
636
|
+
api: {
|
|
637
|
+
maxAge: 4444
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
const response = await apos.http.get('/', { fullResponse: true });
|
|
641
|
+
|
|
642
|
+
assert(response.headers['cache-control'] === undefined);
|
|
643
|
+
|
|
644
|
+
delete apos.page.options.cache;
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('should set a cache-control value when serving a page, when "page" cache option is set', async () => {
|
|
648
|
+
apos.page.options.cache = {
|
|
649
|
+
page: {
|
|
650
|
+
maxAge: 5555
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
const response = await apos.http.get('/', { fullResponse: true });
|
|
654
|
+
|
|
655
|
+
assert(response.headers['cache-control'] === 'max-age=5555');
|
|
656
|
+
|
|
657
|
+
delete apos.page.options.cache;
|
|
658
|
+
});
|
|
659
|
+
|
|
484
660
|
});
|
|
@@ -78,4 +78,37 @@ describe('Pieces Public API', function() {
|
|
|
78
78
|
assert(!response.results[0].foo);
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
it('should not set a "max-age" cache-control value when retrieving pieces, when cache option is not set, with a public API projection', async () => {
|
|
82
|
+
apos.thing.options.publicApiProjection = {
|
|
83
|
+
title: 1,
|
|
84
|
+
_url: 1
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const response1 = await apos.http.get('/api/v1/thing', { fullResponse: true });
|
|
88
|
+
const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', { fullResponse: true });
|
|
89
|
+
|
|
90
|
+
assert(response1.headers['cache-control'] === undefined);
|
|
91
|
+
assert(response2.headers['cache-control'] === undefined);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should set a "max-age" cache-control value when retrieving pieces, with a public API projection', async () => {
|
|
95
|
+
apos.thing.options.publicApiProjection = {
|
|
96
|
+
title: 1,
|
|
97
|
+
_url: 1
|
|
98
|
+
};
|
|
99
|
+
apos.thing.options.cache = {
|
|
100
|
+
api: {
|
|
101
|
+
maxAge: 2222
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const response1 = await apos.http.get('/api/v1/thing', { fullResponse: true });
|
|
106
|
+
const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', { fullResponse: true });
|
|
107
|
+
|
|
108
|
+
assert(response1.headers['cache-control'] === 'max-age=2222');
|
|
109
|
+
assert(response2.headers['cache-control'] === 'max-age=2222');
|
|
110
|
+
|
|
111
|
+
delete apos.thing.options.cache;
|
|
112
|
+
});
|
|
113
|
+
|
|
81
114
|
});
|
package/test/pieces.js
CHANGED
|
@@ -1280,4 +1280,122 @@ describe('Pieces', function() {
|
|
|
1280
1280
|
assert(existingPiece.title === 'new product name');
|
|
1281
1281
|
assert(existingPiece.color === 'red');
|
|
1282
1282
|
});
|
|
1283
|
+
|
|
1284
|
+
it('should not set a cache-control value when retrieving pieces, when cache option is not set', async () => {
|
|
1285
|
+
const response1 = await apos.http.get('/api/v1/thing', { fullResponse: true });
|
|
1286
|
+
const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', { fullResponse: true });
|
|
1287
|
+
|
|
1288
|
+
assert(response1.headers['cache-control'] === undefined);
|
|
1289
|
+
assert(response2.headers['cache-control'] === undefined);
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
it('should not set a cache-control value when retrieving pieces, when "api" cache option is not set', async () => {
|
|
1293
|
+
apos.thing.options.cache = {
|
|
1294
|
+
page: {
|
|
1295
|
+
maxAge: 5555
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
const response1 = await apos.http.get('/api/v1/thing', { fullResponse: true });
|
|
1300
|
+
const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', { fullResponse: true });
|
|
1301
|
+
|
|
1302
|
+
assert(response1.headers['cache-control'] === undefined);
|
|
1303
|
+
assert(response2.headers['cache-control'] === undefined);
|
|
1304
|
+
|
|
1305
|
+
delete apos.thing.options.cache;
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
it('should set a "max-age" cache-control value when retrieving pieces, when "api" cache option is set', async () => {
|
|
1309
|
+
apos.thing.options.cache = {
|
|
1310
|
+
api: {
|
|
1311
|
+
maxAge: 3333
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
const response1 = await apos.http.get('/api/v1/thing', { fullResponse: true });
|
|
1316
|
+
const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', { fullResponse: true });
|
|
1317
|
+
|
|
1318
|
+
assert(response1.headers['cache-control'] === 'max-age=3333');
|
|
1319
|
+
assert(response2.headers['cache-control'] === 'max-age=3333');
|
|
1320
|
+
|
|
1321
|
+
delete apos.thing.options.cache;
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
it('should set a "no-store" cache-control value when retrieving pieces, when user is connected', async () => {
|
|
1325
|
+
await apos.http.post('/api/v1/@apostrophecms/login/login', {
|
|
1326
|
+
body: {
|
|
1327
|
+
username: 'admin',
|
|
1328
|
+
password: 'admin',
|
|
1329
|
+
session: true
|
|
1330
|
+
},
|
|
1331
|
+
jar
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
const response1 = await apos.http.get('/api/v1/thing', {
|
|
1335
|
+
fullResponse: true,
|
|
1336
|
+
jar
|
|
1337
|
+
});
|
|
1338
|
+
const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', {
|
|
1339
|
+
fullResponse: true,
|
|
1340
|
+
jar
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
assert(response1.headers['cache-control'] === 'no-store');
|
|
1344
|
+
assert(response2.headers['cache-control'] === 'no-store');
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
it('should set a "no-store" cache-control value when retrieving pieces, when "api" cache option is set, when user is connected', async () => {
|
|
1348
|
+
apos.thing.options.cache = {
|
|
1349
|
+
api: {
|
|
1350
|
+
maxAge: 3333
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
await apos.http.post('/api/v1/@apostrophecms/login/login', {
|
|
1355
|
+
body: {
|
|
1356
|
+
username: 'admin',
|
|
1357
|
+
password: 'admin',
|
|
1358
|
+
session: true
|
|
1359
|
+
},
|
|
1360
|
+
jar
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
const response1 = await apos.http.get('/api/v1/thing', {
|
|
1364
|
+
fullResponse: true,
|
|
1365
|
+
jar
|
|
1366
|
+
});
|
|
1367
|
+
const response2 = await apos.http.get('/api/v1/thing/testThing:en:published', {
|
|
1368
|
+
fullResponse: true,
|
|
1369
|
+
jar
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
assert(response1.headers['cache-control'] === 'no-store');
|
|
1373
|
+
assert(response2.headers['cache-control'] === 'no-store');
|
|
1374
|
+
|
|
1375
|
+
delete apos.thing.options.cache;
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
it('should set a "no-store" cache-control value when retrieving pieces, when user is connected using an api key', async () => {
|
|
1379
|
+
const response1 = await apos.http.get(`/api/v1/thing?apiKey=${apiKey}`, { fullResponse: true });
|
|
1380
|
+
const response2 = await apos.http.get(`/api/v1/thing/testThing:en:published?apiKey=${apiKey}`, { fullResponse: true });
|
|
1381
|
+
|
|
1382
|
+
assert(response1.headers['cache-control'] === 'no-store');
|
|
1383
|
+
assert(response2.headers['cache-control'] === 'no-store');
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
it('should set a "no-store" cache-control value when retrieving pieces, when "api" cache option is set, when user is connected using an api key', async () => {
|
|
1387
|
+
apos.thing.options.cache = {
|
|
1388
|
+
api: {
|
|
1389
|
+
maxAge: 3333
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
const response1 = await apos.http.get(`/api/v1/thing?apiKey=${apiKey}`, { fullResponse: true });
|
|
1394
|
+
const response2 = await apos.http.get(`/api/v1/thing/testThing:en:published?apiKey=${apiKey}`, { fullResponse: true });
|
|
1395
|
+
|
|
1396
|
+
assert(response1.headers['cache-control'] === 'no-store');
|
|
1397
|
+
assert(response2.headers['cache-control'] === 'no-store');
|
|
1398
|
+
|
|
1399
|
+
delete apos.thing.options.cache;
|
|
1400
|
+
});
|
|
1283
1401
|
});
|
package/test/templates.js
CHANGED
|
@@ -115,6 +115,7 @@ describe('Templates', function() {
|
|
|
115
115
|
assert($body.length);
|
|
116
116
|
const aposData = JSON.parse($body.attr('data-apos'));
|
|
117
117
|
assert(aposData);
|
|
118
|
+
assert(aposData.shortName);
|
|
118
119
|
assert(aposData.csrfCookieName);
|
|
119
120
|
assert(!aposData.modules['@apostrophecms/admin-bar']);
|
|
120
121
|
assert(result.indexOf('<title>I am the title</title>') !== -1);
|
|
@@ -129,6 +130,7 @@ describe('Templates', function() {
|
|
|
129
130
|
assert($body.length);
|
|
130
131
|
const aposData = JSON.parse($body.attr('data-apos'));
|
|
131
132
|
assert(aposData);
|
|
133
|
+
assert(aposData.shortName);
|
|
132
134
|
assert(aposData.modules['@apostrophecms/admin-bar'].items.length);
|
|
133
135
|
assert(result.indexOf('<title>I am the title</title>') !== -1);
|
|
134
136
|
assert(result.indexOf('<h2>I am the main content</h2>') !== -1);
|