@eeacms/volto-cca-policy 0.1.74 → 0.1.76
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 +36 -8
- package/package.json +1 -1
- package/src/components/MigrationButtons.jsx +24 -11
- package/src/components/manage/Blocks/Listing/IndicatorCardsListingView.jsx +69 -0
- package/src/components/manage/Blocks/Listing/OrganisationCardsListingView.jsx +8 -9
- package/src/components/manage/Blocks/Listing/common.js +3 -0
- package/src/components/manage/Blocks/Listing/index.js +8 -0
- package/src/components/manage/Blocks/Listing/styles.less +40 -6
- package/src/components/theme/Header.jsx +4 -58
- package/src/components/theme/LanguageSwitch.jsx +66 -0
- package/src/components/theme/Views/PublicationReportView.jsx +3 -35
- package/src/components/theme/Views/ToolView.jsx +2 -31
- package/src/customizations/@eeacms/volto-eea-design-system/ui/Header/Header.jsx +1 -1
- package/src/customizations/volto/middleware/README.md +3 -0
- package/src/customizations/volto/middleware/api.js +347 -0
- package/src/customizations/volto/server.jsx +354 -0
- package/src/helpers/Utils.jsx +39 -0
- package/src/helpers/index.js +1 -0
- package/src/index.js +11 -6
- package/src/search/mission_projects/config-projects.js +1 -1
- package/src/search/mission_stories/config-stories.js +5 -0
- package/src/search/mission_stories/facets-stories.js +16 -0
- package/theme/globals/mission.less +4 -0
- package/theme/globals/site.overrides +4 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Api middleware.
|
|
3
|
+
* @module middleware/api
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Cookies from 'universal-cookie';
|
|
7
|
+
import jwtDecode from 'jwt-decode';
|
|
8
|
+
import { compact, flatten, union } from 'lodash';
|
|
9
|
+
import { matchPath } from 'react-router';
|
|
10
|
+
import qs from 'query-string';
|
|
11
|
+
|
|
12
|
+
import config from '@plone/volto/registry';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
GET_CONTENT,
|
|
16
|
+
LOGIN,
|
|
17
|
+
RESET_APIERROR,
|
|
18
|
+
SET_APIERROR,
|
|
19
|
+
} from '@plone/volto/constants/ActionTypes';
|
|
20
|
+
import { changeLanguage } from '@plone/volto/actions';
|
|
21
|
+
import {
|
|
22
|
+
toGettextLang,
|
|
23
|
+
toReactIntlLang,
|
|
24
|
+
getCookieOptions,
|
|
25
|
+
} from '@plone/volto/helpers';
|
|
26
|
+
let socket = null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
*
|
|
30
|
+
* Add configured expanders to an api call for an action
|
|
31
|
+
* Requirements:
|
|
32
|
+
*
|
|
33
|
+
* - It should add the expanders set in the config settings
|
|
34
|
+
* - It should preserve any query if present
|
|
35
|
+
* - It should preserve (and add) any expand parameter (if present) e.g. translations
|
|
36
|
+
* - It should take use the correct codification for arrays in querystring (repeated parameter for each member of the array)
|
|
37
|
+
*
|
|
38
|
+
* @function addExpandersToPath
|
|
39
|
+
* @param {string} path The url/path including the querystring
|
|
40
|
+
* @param {*} type The action type
|
|
41
|
+
* @returns {string} The url/path with the configured expanders added to the query string
|
|
42
|
+
*/
|
|
43
|
+
export function addExpandersToPath(path, type, isAnonymous) {
|
|
44
|
+
const { settings } = config;
|
|
45
|
+
const { apiExpanders = [] } = settings;
|
|
46
|
+
|
|
47
|
+
const {
|
|
48
|
+
url,
|
|
49
|
+
query: { expand, ...query },
|
|
50
|
+
} = qs.parseUrl(path, { decode: false });
|
|
51
|
+
|
|
52
|
+
const expandersFromConfig = apiExpanders
|
|
53
|
+
.filter((expand) => matchPath(url, expand.match) && expand[type])
|
|
54
|
+
.map((expand) => expand[type]);
|
|
55
|
+
|
|
56
|
+
const expandMerge = compact(
|
|
57
|
+
union([expand, ...flatten(expandersFromConfig)]),
|
|
58
|
+
).filter((item) => !(item === 'types' && isAnonymous)); // Remove types expander if isAnonymous
|
|
59
|
+
|
|
60
|
+
const stringifiedExpand = qs.stringify(
|
|
61
|
+
{ expand: expandMerge },
|
|
62
|
+
{
|
|
63
|
+
arrayFormat: 'comma',
|
|
64
|
+
encode: false,
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const querystringFromConfig = apiExpanders
|
|
69
|
+
.filter((expand) => matchPath(url, expand.match) && expand[type])
|
|
70
|
+
.reduce((acc, expand) => ({ ...acc, ...expand?.['querystring'] }), {});
|
|
71
|
+
|
|
72
|
+
const queryMerge = { ...query, ...querystringFromConfig };
|
|
73
|
+
|
|
74
|
+
const stringifiedQuery = qs.stringify(queryMerge, {
|
|
75
|
+
encode: false,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (stringifiedQuery && stringifiedExpand) {
|
|
79
|
+
return `${url}?${stringifiedExpand}&${stringifiedQuery}`;
|
|
80
|
+
} else if (!stringifiedQuery && stringifiedExpand) {
|
|
81
|
+
return `${url}?${stringifiedExpand}`;
|
|
82
|
+
} else if (stringifiedQuery && !stringifiedExpand) {
|
|
83
|
+
return `${url}?${stringifiedQuery}`;
|
|
84
|
+
} else {
|
|
85
|
+
return url;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Send a message on a websocket.
|
|
91
|
+
* @function sendOnSocket
|
|
92
|
+
* @param {Object} request Request object.
|
|
93
|
+
* @returns {Promise} message is send
|
|
94
|
+
*/
|
|
95
|
+
function sendOnSocket(request) {
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
switch (socket.readyState) {
|
|
98
|
+
case socket.CONNECTING:
|
|
99
|
+
socket.addEventListener('open', () => resolve(socket));
|
|
100
|
+
socket.addEventListener('error', reject);
|
|
101
|
+
break;
|
|
102
|
+
case socket.OPEN:
|
|
103
|
+
resolve(socket);
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
reject();
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}).then(() => {
|
|
110
|
+
socket.send(JSON.stringify(request));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Api middleware.
|
|
116
|
+
* @function
|
|
117
|
+
* @param {Object} api Api object.
|
|
118
|
+
* @returns {Promise} Action promise.
|
|
119
|
+
*/
|
|
120
|
+
const apiMiddlewareFactory = (api) => ({ dispatch, getState }) => (next) => (
|
|
121
|
+
action,
|
|
122
|
+
) => {
|
|
123
|
+
const { settings } = config;
|
|
124
|
+
|
|
125
|
+
const isAnonymous = !getState().userSession.token;
|
|
126
|
+
|
|
127
|
+
if (typeof action === 'function') {
|
|
128
|
+
return action(dispatch, getState);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const { request, type, mode = 'parallel', ...rest } = action;
|
|
132
|
+
const { subrequest } = action; // We want subrequest remains in `...rest` above
|
|
133
|
+
|
|
134
|
+
let actionPromise;
|
|
135
|
+
|
|
136
|
+
if (!request) {
|
|
137
|
+
return next(action);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
next({ ...rest, type: `${type}_PENDING` });
|
|
141
|
+
|
|
142
|
+
if (socket) {
|
|
143
|
+
actionPromise = Array.isArray(request)
|
|
144
|
+
? Promise.all(
|
|
145
|
+
request.map((item) =>
|
|
146
|
+
sendOnSocket({
|
|
147
|
+
...item,
|
|
148
|
+
path: addExpandersToPath(item.path, type, isAnonymous),
|
|
149
|
+
id: type,
|
|
150
|
+
}),
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
: sendOnSocket({
|
|
154
|
+
...request,
|
|
155
|
+
path: addExpandersToPath(request.path, type, isAnonymous),
|
|
156
|
+
id: type,
|
|
157
|
+
});
|
|
158
|
+
} else {
|
|
159
|
+
actionPromise = Array.isArray(request)
|
|
160
|
+
? mode === 'serial'
|
|
161
|
+
? request.reduce((prevPromise, item) => {
|
|
162
|
+
return prevPromise.then((acc) => {
|
|
163
|
+
return api[item.op](
|
|
164
|
+
addExpandersToPath(item.path, type, isAnonymous),
|
|
165
|
+
{
|
|
166
|
+
data: item.data,
|
|
167
|
+
type: item.type,
|
|
168
|
+
headers: item.headers,
|
|
169
|
+
params: request.params,
|
|
170
|
+
checkUrl: settings.actions_raising_api_errors.includes(
|
|
171
|
+
action.type,
|
|
172
|
+
),
|
|
173
|
+
},
|
|
174
|
+
).then((reqres) => {
|
|
175
|
+
return [...acc, reqres];
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}, Promise.resolve([]))
|
|
179
|
+
: Promise.all(
|
|
180
|
+
request.map((item) =>
|
|
181
|
+
api[item.op](addExpandersToPath(item.path, type, isAnonymous), {
|
|
182
|
+
data: item.data,
|
|
183
|
+
type: item.type,
|
|
184
|
+
headers: item.headers,
|
|
185
|
+
params: request.params,
|
|
186
|
+
checkUrl: settings.actions_raising_api_errors.includes(
|
|
187
|
+
action.type,
|
|
188
|
+
),
|
|
189
|
+
}),
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
: api[request.op](addExpandersToPath(request.path, type, isAnonymous), {
|
|
193
|
+
data: request.data,
|
|
194
|
+
type: request.type,
|
|
195
|
+
headers: request.headers,
|
|
196
|
+
params: request.params,
|
|
197
|
+
checkUrl: settings.actions_raising_api_errors.includes(action.type),
|
|
198
|
+
});
|
|
199
|
+
actionPromise.then(
|
|
200
|
+
(result) => {
|
|
201
|
+
const { settings } = config;
|
|
202
|
+
if (getState().apierror.connectionRefused) {
|
|
203
|
+
next({
|
|
204
|
+
...rest,
|
|
205
|
+
type: RESET_APIERROR,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (type === GET_CONTENT) {
|
|
209
|
+
// customization original: result?.language?.token
|
|
210
|
+
const lang = result?.language?.token || result.language || '';
|
|
211
|
+
// end customization
|
|
212
|
+
if (
|
|
213
|
+
lang &&
|
|
214
|
+
getState().intl.locale !== toReactIntlLang(lang) &&
|
|
215
|
+
!subrequest &&
|
|
216
|
+
config.settings.supportedLanguages.includes(lang)
|
|
217
|
+
) {
|
|
218
|
+
const langFileName = toGettextLang(lang);
|
|
219
|
+
import('~/../locales/' + langFileName + '.json').then((locale) => {
|
|
220
|
+
dispatch(changeLanguage(lang, locale.default));
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (type === LOGIN && settings.websockets) {
|
|
225
|
+
const cookies = new Cookies();
|
|
226
|
+
cookies.set(
|
|
227
|
+
'auth_token',
|
|
228
|
+
result.token,
|
|
229
|
+
getCookieOptions({
|
|
230
|
+
expires: new Date(jwtDecode(result.token).exp * 1000),
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
api.get('/@wstoken').then((res) => {
|
|
234
|
+
socket = new WebSocket(
|
|
235
|
+
`${settings.apiPath.replace('http', 'ws')}/@ws?ws_token=${
|
|
236
|
+
res.token
|
|
237
|
+
}`,
|
|
238
|
+
);
|
|
239
|
+
socket.onmessage = (message) => {
|
|
240
|
+
const packet = JSON.parse(message.data);
|
|
241
|
+
if (packet.error) {
|
|
242
|
+
dispatch({
|
|
243
|
+
type: `${packet.id}_FAIL`,
|
|
244
|
+
error: packet.error,
|
|
245
|
+
});
|
|
246
|
+
} else {
|
|
247
|
+
dispatch({
|
|
248
|
+
type: `${packet.id}_SUCCESS`,
|
|
249
|
+
result: JSON.parse(packet.data),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
return next({ ...rest, result, type: `${type}_SUCCESS` });
|
|
257
|
+
} catch (error) {
|
|
258
|
+
// There was an exception while processing reducers or downstream middleware.
|
|
259
|
+
next({
|
|
260
|
+
...rest,
|
|
261
|
+
error: { status: 500, error },
|
|
262
|
+
type: `${type}_FAIL`,
|
|
263
|
+
});
|
|
264
|
+
// Rethrow the original exception on the client side only,
|
|
265
|
+
// so it doesn't fall through to express on the server.
|
|
266
|
+
if (__CLIENT__) throw error;
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
(error) => {
|
|
270
|
+
// Only SSR can set ECONNREFUSED
|
|
271
|
+
if (error.code === 'ECONNREFUSED') {
|
|
272
|
+
next({
|
|
273
|
+
...rest,
|
|
274
|
+
error,
|
|
275
|
+
statusCode: error.code,
|
|
276
|
+
connectionRefused: true,
|
|
277
|
+
type: SET_APIERROR,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Response error is marked crossDomain if CORS error happen
|
|
282
|
+
else if (error.crossDomain) {
|
|
283
|
+
next({
|
|
284
|
+
...rest,
|
|
285
|
+
error,
|
|
286
|
+
statusCode: 'CORSERROR',
|
|
287
|
+
connectionRefused: false,
|
|
288
|
+
type: SET_APIERROR,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Check for actions who can raise api errors
|
|
293
|
+
if (settings.actions_raising_api_errors.includes(action.type)) {
|
|
294
|
+
// Gateway timeout
|
|
295
|
+
if (error?.response?.statusCode === 504) {
|
|
296
|
+
next({
|
|
297
|
+
...rest,
|
|
298
|
+
error,
|
|
299
|
+
statusCode: error.code,
|
|
300
|
+
connectionRefused: true,
|
|
301
|
+
type: SET_APIERROR,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Redirect
|
|
306
|
+
else if (error?.code === 301) {
|
|
307
|
+
next({
|
|
308
|
+
...rest,
|
|
309
|
+
error,
|
|
310
|
+
statusCode: error.code,
|
|
311
|
+
connectionRefused: false,
|
|
312
|
+
type: SET_APIERROR,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Redirect
|
|
317
|
+
else if (error?.code === 408) {
|
|
318
|
+
next({
|
|
319
|
+
...rest,
|
|
320
|
+
error,
|
|
321
|
+
statusCode: error.code,
|
|
322
|
+
connectionRefused: false,
|
|
323
|
+
type: SET_APIERROR,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Unauthorized
|
|
328
|
+
else if (error?.response?.statusCode === 401) {
|
|
329
|
+
next({
|
|
330
|
+
...rest,
|
|
331
|
+
error,
|
|
332
|
+
statusCode: error.response,
|
|
333
|
+
message: error.response.body.message,
|
|
334
|
+
connectionRefused: false,
|
|
335
|
+
type: SET_APIERROR,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return next({ ...rest, error, type: `${type}_FAIL` });
|
|
340
|
+
},
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return actionPromise;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
export default apiMiddlewareFactory;
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/* Original: https://github.com/plone/volto/blob/16.x.x/src/server.jsx */
|
|
2
|
+
/* Line: 59 - Fix crash when a supported language it's not in volto/locales folder */
|
|
3
|
+
|
|
4
|
+
/* eslint no-console: 0 */
|
|
5
|
+
import '@plone/volto/config'; // This is the bootstrap for the global config - server side
|
|
6
|
+
import { existsSync, lstatSync, readFileSync } from 'fs';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { StaticRouter } from 'react-router-dom';
|
|
9
|
+
import { Provider } from 'react-intl-redux';
|
|
10
|
+
import express from 'express';
|
|
11
|
+
import { renderToString } from 'react-dom/server';
|
|
12
|
+
import { createMemoryHistory } from 'history';
|
|
13
|
+
import { parse as parseUrl } from 'url';
|
|
14
|
+
import { keys } from 'lodash';
|
|
15
|
+
import locale from 'locale';
|
|
16
|
+
import { detect } from 'detect-browser';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import { ChunkExtractor, ChunkExtractorManager } from '@loadable/server';
|
|
19
|
+
import { resetServerContext } from 'react-beautiful-dnd';
|
|
20
|
+
import { CookiesProvider } from 'react-cookie';
|
|
21
|
+
import cookiesMiddleware from 'universal-cookie-express';
|
|
22
|
+
import debug from 'debug';
|
|
23
|
+
|
|
24
|
+
import routes from '@root/routes';
|
|
25
|
+
import config from '@plone/volto/registry';
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
flattenToAppURL,
|
|
29
|
+
Html,
|
|
30
|
+
Api,
|
|
31
|
+
persistAuthToken,
|
|
32
|
+
toBackendLang,
|
|
33
|
+
toGettextLang,
|
|
34
|
+
toReactIntlLang,
|
|
35
|
+
} from '@plone/volto/helpers';
|
|
36
|
+
import { changeLanguage } from '@plone/volto/actions';
|
|
37
|
+
|
|
38
|
+
import userSession from '@plone/volto/reducers/userSession/userSession';
|
|
39
|
+
|
|
40
|
+
import ErrorPage from '@plone/volto/error';
|
|
41
|
+
|
|
42
|
+
import languages from '@plone/volto/constants/Languages';
|
|
43
|
+
|
|
44
|
+
import configureStore from '@plone/volto/store';
|
|
45
|
+
import {
|
|
46
|
+
ReduxAsyncConnect,
|
|
47
|
+
loadOnServer,
|
|
48
|
+
} from '@plone/volto/helpers/AsyncConnect';
|
|
49
|
+
|
|
50
|
+
let locales = {};
|
|
51
|
+
|
|
52
|
+
if (config.settings) {
|
|
53
|
+
config.settings.supportedLanguages.forEach((lang) => {
|
|
54
|
+
const langFileName = toGettextLang(lang);
|
|
55
|
+
import('@root/../locales/' + langFileName + '.json')
|
|
56
|
+
.then((locale) => {
|
|
57
|
+
locales = { ...locales, [toReactIntlLang(lang)]: locale.default };
|
|
58
|
+
})
|
|
59
|
+
// start customization
|
|
60
|
+
.catch((error) => {
|
|
61
|
+
locales = { ...locales, [toReactIntlLang(lang)]: {} };
|
|
62
|
+
});
|
|
63
|
+
// end customization
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function reactIntlErrorHandler(error) {
|
|
68
|
+
debug('i18n')(error);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const supported = new locale.Locales(keys(languages), 'en');
|
|
72
|
+
|
|
73
|
+
const server = express()
|
|
74
|
+
.disable('x-powered-by')
|
|
75
|
+
.head('/*', function (req, res) {
|
|
76
|
+
// Support for HEAD requests. Required by start-test utility in CI.
|
|
77
|
+
res.send('');
|
|
78
|
+
})
|
|
79
|
+
.use(cookiesMiddleware());
|
|
80
|
+
|
|
81
|
+
const middleware = (config.settings.expressMiddleware || []).filter((m) => m);
|
|
82
|
+
|
|
83
|
+
server.all('*', setupServer);
|
|
84
|
+
if (middleware.length) server.use('/', middleware);
|
|
85
|
+
|
|
86
|
+
server.use(function (err, req, res, next) {
|
|
87
|
+
if (err) {
|
|
88
|
+
const { store } = res.locals;
|
|
89
|
+
const errorPage = (
|
|
90
|
+
<Provider store={store} onError={reactIntlErrorHandler}>
|
|
91
|
+
<StaticRouter context={{}} location={req.url}>
|
|
92
|
+
<ErrorPage message={err.message} />
|
|
93
|
+
</StaticRouter>
|
|
94
|
+
</Provider>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
res.set({
|
|
98
|
+
'Cache-Control': 'public, max-age=60, no-transform',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/* Displays error in console
|
|
102
|
+
* TODO:
|
|
103
|
+
* - get ignored codes from Plone error_log
|
|
104
|
+
*/
|
|
105
|
+
const ignoredErrors = [301, 302, 401, 404];
|
|
106
|
+
if (!ignoredErrors.includes(err.status)) console.error(err);
|
|
107
|
+
|
|
108
|
+
res
|
|
109
|
+
.status(err.status || 500) // If error happens in Volto code itself error status is undefined
|
|
110
|
+
.send(`<!doctype html> ${renderToString(errorPage)}`);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
function setupServer(req, res, next) {
|
|
115
|
+
const api = new Api(req);
|
|
116
|
+
|
|
117
|
+
const lang = toReactIntlLang(
|
|
118
|
+
new locale.Locales(
|
|
119
|
+
req.universalCookies.get('I18N_LANGUAGE') ||
|
|
120
|
+
config.settings.defaultLanguage ||
|
|
121
|
+
req.headers['accept-language'],
|
|
122
|
+
)
|
|
123
|
+
.best(supported)
|
|
124
|
+
.toString(),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Minimum initial state for the fake Redux store instance
|
|
128
|
+
const initialState = {
|
|
129
|
+
intl: {
|
|
130
|
+
defaultLocale: 'en',
|
|
131
|
+
locale: lang,
|
|
132
|
+
messages: locales[lang],
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const history = createMemoryHistory({
|
|
137
|
+
initialEntries: [req.url],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Create a fake Redux store instance for the `errorHandler` to render
|
|
141
|
+
// and for being used by the rest of the middlewares, if required
|
|
142
|
+
const store = configureStore(initialState, history, api);
|
|
143
|
+
|
|
144
|
+
function errorHandler(error) {
|
|
145
|
+
const errorPage = (
|
|
146
|
+
<Provider store={store} onError={reactIntlErrorHandler}>
|
|
147
|
+
<StaticRouter context={{}} location={req.url}>
|
|
148
|
+
<ErrorPage message={error.message} />
|
|
149
|
+
</StaticRouter>
|
|
150
|
+
</Provider>
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
res.set({
|
|
154
|
+
'Cache-Control': 'public, max-age=60, no-transform',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
/* Displays error in console
|
|
158
|
+
* TODO:
|
|
159
|
+
* - get ignored codes from Plone error_log
|
|
160
|
+
*/
|
|
161
|
+
const ignoredErrors = [301, 302, 401, 404];
|
|
162
|
+
if (!ignoredErrors.includes(error.status)) console.error(error);
|
|
163
|
+
|
|
164
|
+
res
|
|
165
|
+
.status(error.status || 500) // If error happens in Volto code itself error status is undefined
|
|
166
|
+
.send(`<!doctype html> ${renderToString(errorPage)}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!process.env.RAZZLE_API_PATH && req.headers.host) {
|
|
170
|
+
res.locals.detectedHost = `${
|
|
171
|
+
req.headers['x-forwarded-proto'] || req.protocol
|
|
172
|
+
}://${req.headers.host}`;
|
|
173
|
+
config.settings.apiPath = res.locals.detectedHost;
|
|
174
|
+
config.settings.publicURL = res.locals.detectedHost;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
res.locals = {
|
|
178
|
+
...res.locals,
|
|
179
|
+
store,
|
|
180
|
+
api,
|
|
181
|
+
errorHandler,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
next();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
server.get('/*', (req, res) => {
|
|
188
|
+
const { errorHandler } = res.locals;
|
|
189
|
+
|
|
190
|
+
const api = new Api(req);
|
|
191
|
+
|
|
192
|
+
const browserdetect = detect(req.headers['user-agent']);
|
|
193
|
+
|
|
194
|
+
const lang = toReactIntlLang(
|
|
195
|
+
new locale.Locales(
|
|
196
|
+
req.universalCookies.get('I18N_LANGUAGE') ||
|
|
197
|
+
config.settings.defaultLanguage ||
|
|
198
|
+
req.headers['accept-language'],
|
|
199
|
+
)
|
|
200
|
+
.best(supported)
|
|
201
|
+
.toString(),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const authToken = req.universalCookies.get('auth_token');
|
|
205
|
+
const initialState = {
|
|
206
|
+
userSession: { ...userSession(), token: authToken },
|
|
207
|
+
form: req.body,
|
|
208
|
+
intl: {
|
|
209
|
+
defaultLocale: 'en',
|
|
210
|
+
locale: lang,
|
|
211
|
+
messages: locales[lang],
|
|
212
|
+
},
|
|
213
|
+
browserdetect,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const history = createMemoryHistory({
|
|
217
|
+
initialEntries: [req.url],
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Create a new Redux store instance
|
|
221
|
+
const store = configureStore(initialState, history, api);
|
|
222
|
+
|
|
223
|
+
persistAuthToken(store, req);
|
|
224
|
+
|
|
225
|
+
// @loadable/server extractor
|
|
226
|
+
const buildDir = process.env.BUILD_DIR || 'build';
|
|
227
|
+
const extractor = new ChunkExtractor({
|
|
228
|
+
statsFile: path.resolve(path.join(buildDir, 'loadable-stats.json')),
|
|
229
|
+
entrypoints: ['client'],
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const url = req.originalUrl || req.url;
|
|
233
|
+
const location = parseUrl(url);
|
|
234
|
+
|
|
235
|
+
loadOnServer({ store, location, routes, api })
|
|
236
|
+
.then(() => {
|
|
237
|
+
const initialLang =
|
|
238
|
+
req.universalCookies.get('I18N_LANGUAGE') ||
|
|
239
|
+
config.settings.defaultLanguage ||
|
|
240
|
+
req.headers['accept-language'];
|
|
241
|
+
|
|
242
|
+
// The content info is in the store at this point thanks to the asynconnect
|
|
243
|
+
// features, then we can force the current language info into the store when
|
|
244
|
+
// coming from an SSR request
|
|
245
|
+
|
|
246
|
+
// TODO: there is a bug here with content that, for any reason, doesn't
|
|
247
|
+
// present the language token field, for some reason. In this case, we
|
|
248
|
+
// should follow the cookie rather then switching the language
|
|
249
|
+
const contentLang = store.getState().content.get?.error
|
|
250
|
+
? initialLang
|
|
251
|
+
: store.getState().content.data?.language?.token ||
|
|
252
|
+
config.settings.defaultLanguage;
|
|
253
|
+
|
|
254
|
+
if (toBackendLang(initialLang) !== contentLang) {
|
|
255
|
+
const newLang = toReactIntlLang(
|
|
256
|
+
new locale.Locales(contentLang).best(supported).toString(),
|
|
257
|
+
);
|
|
258
|
+
store.dispatch(changeLanguage(newLang, locales[newLang], req));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const context = {};
|
|
262
|
+
resetServerContext();
|
|
263
|
+
const markup = renderToString(
|
|
264
|
+
<ChunkExtractorManager extractor={extractor}>
|
|
265
|
+
<CookiesProvider cookies={req.universalCookies}>
|
|
266
|
+
<Provider store={store} onError={reactIntlErrorHandler}>
|
|
267
|
+
<StaticRouter context={context} location={req.url}>
|
|
268
|
+
<ReduxAsyncConnect routes={routes} helpers={api} />
|
|
269
|
+
</StaticRouter>
|
|
270
|
+
</Provider>
|
|
271
|
+
</CookiesProvider>
|
|
272
|
+
</ChunkExtractorManager>,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const readCriticalCss =
|
|
276
|
+
config.settings.serverConfig.readCriticalCss || defaultReadCriticalCss;
|
|
277
|
+
|
|
278
|
+
// If we are showing an "old browser" warning,
|
|
279
|
+
// make sure it doesn't get cached in a shared cache
|
|
280
|
+
const browserdetect = store.getState().browserdetect;
|
|
281
|
+
if (config.settings.notSupportedBrowsers.includes(browserdetect?.name)) {
|
|
282
|
+
res.set({
|
|
283
|
+
'Cache-Control': 'private',
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (context.url) {
|
|
288
|
+
res.redirect(flattenToAppURL(context.url));
|
|
289
|
+
} else if (context.error_code) {
|
|
290
|
+
res.set({
|
|
291
|
+
'Cache-Control': 'no-cache',
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
res.status(context.error_code).send(
|
|
295
|
+
`<!doctype html>
|
|
296
|
+
${renderToString(
|
|
297
|
+
<Html
|
|
298
|
+
extractor={extractor}
|
|
299
|
+
markup={markup}
|
|
300
|
+
store={store}
|
|
301
|
+
extractScripts={
|
|
302
|
+
config.settings.serverConfig.extractScripts?.errorPages ||
|
|
303
|
+
process.env.NODE_ENV !== 'production'
|
|
304
|
+
}
|
|
305
|
+
criticalCss={readCriticalCss(req)}
|
|
306
|
+
apiPath={res.locals.detectedHost || config.settings.apiPath}
|
|
307
|
+
publicURL={
|
|
308
|
+
res.locals.detectedHost || config.settings.publicURL
|
|
309
|
+
}
|
|
310
|
+
/>,
|
|
311
|
+
)}
|
|
312
|
+
`,
|
|
313
|
+
);
|
|
314
|
+
} else {
|
|
315
|
+
res.status(200).send(
|
|
316
|
+
`<!doctype html>
|
|
317
|
+
${renderToString(
|
|
318
|
+
<Html
|
|
319
|
+
extractor={extractor}
|
|
320
|
+
markup={markup}
|
|
321
|
+
store={store}
|
|
322
|
+
criticalCss={readCriticalCss(req)}
|
|
323
|
+
apiPath={res.locals.detectedHost || config.settings.apiPath}
|
|
324
|
+
publicURL={
|
|
325
|
+
res.locals.detectedHost || config.settings.publicURL
|
|
326
|
+
}
|
|
327
|
+
/>,
|
|
328
|
+
)}
|
|
329
|
+
`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}, errorHandler)
|
|
333
|
+
.catch(errorHandler);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
export const defaultReadCriticalCss = () => {
|
|
337
|
+
const { criticalCssPath } = config.settings.serverConfig;
|
|
338
|
+
|
|
339
|
+
const e = existsSync(criticalCssPath);
|
|
340
|
+
if (!e) return;
|
|
341
|
+
|
|
342
|
+
const f = lstatSync(criticalCssPath);
|
|
343
|
+
if (!f.isFile()) return;
|
|
344
|
+
|
|
345
|
+
return readFileSync(criticalCssPath, { encoding: 'utf-8' });
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// Exposed for the console bootstrap info messages
|
|
349
|
+
server.apiPath = config.settings.apiPath;
|
|
350
|
+
server.devProxyToApiPath = config.settings.devProxyToApiPath;
|
|
351
|
+
server.proxyRewriteTarget = config.settings.proxyRewriteTarget;
|
|
352
|
+
server.publicURL = config.settings.publicURL;
|
|
353
|
+
|
|
354
|
+
export default server;
|