@saltcorn/server 0.9.3-beta.4 → 0.9.3-beta.6

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/routes/page.js CHANGED
@@ -5,11 +5,12 @@
5
5
  */
6
6
 
7
7
  const Router = require("express-promise-router");
8
+ const { UAParser } = require("ua-parser-js");
8
9
 
9
10
  const Page = require("@saltcorn/data/models/page");
11
+ const PageGroup = require("@saltcorn/data/models/page_group");
10
12
  const Trigger = require("@saltcorn/data/models/trigger");
11
- const File = require("@saltcorn/data/models/file");
12
- const { getState } = require("@saltcorn/data/db/state");
13
+ const { getState, features } = require("@saltcorn/data/db/state");
13
14
  const {
14
15
  error_catcher,
15
16
  scan_for_page_title,
@@ -18,6 +19,7 @@ const {
18
19
  } = require("../routes/utils.js");
19
20
  const { isTest } = require("@saltcorn/data/utils");
20
21
  const { add_edit_bar } = require("../markup/admin.js");
22
+ const { script, domReady } = require("@saltcorn/markup/tags");
21
23
  const { traverseSync } = require("@saltcorn/data/models/layout");
22
24
  const { run_action_column } = require("@saltcorn/data/plugin-helper");
23
25
  const db = require("@saltcorn/data/db");
@@ -32,56 +34,134 @@ const db = require("@saltcorn/data/db");
32
34
  const router = new Router();
33
35
  module.exports = router;
34
36
 
35
- /**
36
- * @name get/:pagename
37
- * @function
38
- * @memberof module:routes/page~pageRouter
39
- * @function
40
- */
37
+ const findPageOrGroup = (pagename) => {
38
+ const page = Page.findOne({ name: pagename });
39
+ if (page) return { page, pageGroup: null };
40
+ else {
41
+ const pageGroup = PageGroup.findOne({ name: pagename });
42
+ if (pageGroup) return { page: null, pageGroup };
43
+ else return { page: null, pageGroup: null };
44
+ }
45
+ };
46
+
47
+ const runPage = async (page, req, res, tic) => {
48
+ const role = req.user && req.user.id ? req.user.role_id : 100;
49
+ if (role <= page.min_role) {
50
+ const contents = await page.run(req.query, { res, req });
51
+ const title = scan_for_page_title(contents, page.title);
52
+ const tock = new Date();
53
+ const ms = tock.getTime() - tic.getTime();
54
+ if (!isTest())
55
+ Trigger.emitEvent("PageLoad", null, req.user, {
56
+ text: req.__("Page '%s' was loaded", page.name),
57
+ type: "page",
58
+ name: page.name,
59
+ render_time: ms,
60
+ });
61
+ if (contents.html_file) await sendHtmlFile(req, res, contents.html_file);
62
+ else
63
+ res.sendWrap(
64
+ {
65
+ title,
66
+ description: page.description,
67
+ bodyClass: "page_" + db.sqlsanitize(page.name),
68
+ no_menu: page.attributes?.no_menu,
69
+ } || `${page.name} page`,
70
+ add_edit_bar({
71
+ role,
72
+ title: page.name,
73
+ what: req.__("Page"),
74
+ url: `/pageedit/edit/${encodeURIComponent(page.name)}`,
75
+ contents,
76
+ })
77
+ );
78
+ } else {
79
+ getState().log(2, `Page ${page.name} not authorized`);
80
+ res.status(404).sendWrap(` page`, req.__("Page %s not found", page.name));
81
+ }
82
+ };
83
+
84
+ const uaDevice = (req) => {
85
+ const uaParser = new UAParser(req.headers["user-agent"]);
86
+ const device = uaParser.getDevice();
87
+ if (!device.type) return "web";
88
+ else return device.type;
89
+ };
90
+
91
+ const screenInfoFromCfg = (req) => {
92
+ const device = uaDevice(req);
93
+ const uaScreenInfos = getState().getConfig("user_agent_screen_infos", {});
94
+ return { device, ...uaScreenInfos[device] };
95
+ };
96
+
97
+ const runPageGroup = async (pageGroup, req, res, tic) => {
98
+ const role = req.user && req.user.id ? req.user.role_id : 100;
99
+ if (role <= pageGroup.min_role) {
100
+ if (pageGroup.members.length === 0) {
101
+ getState().log(2, `Pagegroup ${pageGroup.name} has no members`);
102
+ res
103
+ .status(400)
104
+ .sendWrap(
105
+ ` page`,
106
+ req.__("Pagegroup %s has no members", pageGroup.name)
107
+ );
108
+ } else {
109
+ let screenInfos = null;
110
+ if (req.cookies["_sc_screen_info_"]) {
111
+ screenInfos = JSON.parse(req.cookies["_sc_screen_info_"]);
112
+ screenInfos.device = uaDevice(req);
113
+ } else {
114
+ const strategy = getState().getConfig(
115
+ "missing_screen_info_strategy",
116
+ "guess_from_user_agent"
117
+ );
118
+ if (strategy === "guess_from_user_agent")
119
+ screenInfos = screenInfoFromCfg(req);
120
+ else if (strategy === "reload" && req.query.is_reload !== "true") {
121
+ return res.sendWrap(
122
+ script(
123
+ domReady(`
124
+ setScreenInfoCookie();
125
+ window.location = updateQueryStringParameter(window.location.href, "is_reload", true);`)
126
+ )
127
+ );
128
+ }
129
+ }
130
+ const eligiblePage = await pageGroup.getEligiblePage(
131
+ screenInfos,
132
+ req.user ? req.user : { role_id: features.public_user_role },
133
+ req.getLocale()
134
+ );
135
+ if (eligiblePage) await runPage(eligiblePage, req, res, tic);
136
+ else {
137
+ getState().log(2, `Pagegroup ${pageGroup.name} has no eligible page`);
138
+ res
139
+ .status(404)
140
+ .sendWrap(` page`, req.__("%s has no eligible page", pageGroup.name));
141
+ }
142
+ }
143
+ } else {
144
+ getState().log(2, `Pagegroup ${pageGroup.name} not authorized`);
145
+ res
146
+ .status(404)
147
+ .sendWrap(` page`, req.__("Pagegroup %s not found", pageGroup.name));
148
+ }
149
+ };
150
+
41
151
  router.get(
42
152
  "/:pagename",
43
153
  error_catcher(async (req, res) => {
44
154
  const { pagename } = req.params;
45
- const state = getState();
46
- state.log(3, `Route /page/${pagename} user=${req.user?.id}`);
155
+ getState().log(3, `Route /page/${pagename} user=${req.user?.id}`);
47
156
  const tic = new Date();
48
-
49
- const role = req.user && req.user.id ? req.user.role_id : 100;
50
- const db_page = await Page.findOne({ name: pagename });
51
- if (db_page && role <= db_page.min_role) {
52
- const contents = await db_page.run(req.query, { res, req });
53
- const title = scan_for_page_title(contents, db_page.title);
54
- const tock = new Date();
55
- const ms = tock.getTime() - tic.getTime();
56
- if (!isTest())
57
- Trigger.emitEvent("PageLoad", null, req.user, {
58
- text: req.__("Page '%s' was loaded", pagename),
59
- type: "page",
60
- name: pagename,
61
- render_time: ms,
62
- });
63
- if (contents.html_file) await sendHtmlFile(req, res, contents.html_file);
64
- else
65
- res.sendWrap(
66
- {
67
- title,
68
- description: db_page.description,
69
- bodyClass: "page_" + db.sqlsanitize(pagename),
70
- no_menu: db_page.attributes?.no_menu,
71
- } || `${pagename} page`,
72
- add_edit_bar({
73
- role,
74
- title: db_page.name,
75
- what: req.__("Page"),
76
- url: `/pageedit/edit/${encodeURIComponent(db_page.name)}`,
77
- contents,
78
- })
79
- );
80
- } else {
81
- if (db_page && !req.user) {
157
+ const { page, pageGroup } = findPageOrGroup(pagename);
158
+ if (page) await runPage(page, req, res, tic);
159
+ else if (pageGroup) await runPageGroup(pageGroup, req, res, tic);
160
+ else {
161
+ if ((page || pageGroup) && !req.user) {
82
162
  res.redirect(`/auth/login?dest=${encodeURIComponent(req.originalUrl)}`);
83
163
  } else {
84
- state.log(2, `Page $pagename} not found or not authorized`);
164
+ getState().log(2, `Page ${pagename} not found or not authorized`);
85
165
  res
86
166
  .status(404)
87
167
  .sendWrap(`${pagename} page`, req.__("Page %s not found", pagename));
@@ -105,12 +185,6 @@ router.post(
105
185
  })
106
186
  );
107
187
 
108
- /**
109
- * @name post/:pagename/action/:rndid
110
- * @function
111
- * @memberof module:routes/page~pageRouter
112
- * @function
113
- */
114
188
  router.post(
115
189
  "/:pagename/action/:rndid",
116
190
  error_catcher(async (req, res) => {
@@ -0,0 +1,378 @@
1
+ const Router = require("express-promise-router");
2
+
3
+ const Form = require("@saltcorn/data/models/form");
4
+ const { error_catcher, isAdmin } = require("./utils.js");
5
+ const { send_infoarch_page } = require("../markup/admin.js");
6
+ const { getState } = require("@saltcorn/data/db/state");
7
+ const { a, div, i, p } = require("@saltcorn/markup/tags");
8
+ const {
9
+ renderForm,
10
+ link,
11
+ post_delete_btn,
12
+ mkTable,
13
+ } = require("@saltcorn/markup");
14
+
15
+ const router = new Router();
16
+ module.exports = router;
17
+
18
+ const deviceTypes = [
19
+ "mobile",
20
+ "tablet",
21
+ "console",
22
+ "smarttv",
23
+ "wearable",
24
+ "web",
25
+ ];
26
+
27
+ const deviceForm = (req, deviceValidator, device) => {
28
+ const sizeValidator = (v) => {
29
+ const n = +v;
30
+ if (isNaN(n)) return req.__("Not a number");
31
+ if (n < 0) return req.__("Must be positive");
32
+ };
33
+ return new Form({
34
+ action: `/page_group/settings/${
35
+ !device ? "add-device" : `edit-device/${device}`
36
+ }`,
37
+ fields: [
38
+ {
39
+ name: "device",
40
+ input_type: "select",
41
+ type: "String",
42
+ options: ["mobile", "tablet", "console", "smarttv", "wearable", "web"],
43
+ required: true,
44
+ validator: deviceValidator,
45
+ },
46
+ {
47
+ name: "width",
48
+ label: req.__("width"),
49
+ type: "String",
50
+ required: true,
51
+ validator: sizeValidator,
52
+ },
53
+ {
54
+ name: "height",
55
+ label: req.__("height"),
56
+ type: "String",
57
+ required: true,
58
+ validator: sizeValidator,
59
+ },
60
+ {
61
+ name: "innerWidth",
62
+ label: req.__("innerWidth"),
63
+ type: "String",
64
+ required: true,
65
+ validator: sizeValidator,
66
+ },
67
+ {
68
+ name: "innerHeight",
69
+ label: req.__("innerHeight"),
70
+ type: "String",
71
+ required: true,
72
+ validator: sizeValidator,
73
+ },
74
+ ],
75
+ additionalButtons: [
76
+ {
77
+ label: device ? req.__("Close") : req.__("Cancel"),
78
+ class: "btn btn-primary",
79
+ onclick: "location.href='/page_group/settings'",
80
+ },
81
+ ],
82
+ });
83
+ };
84
+
85
+ const loadDeviceConfigs = () => {
86
+ const cfg = getState().getConfig("user_agent_screen_infos", {});
87
+ const deviceConfigs = deviceTypes
88
+ .filter((device) => cfg[device])
89
+ .map((device) => ({
90
+ device,
91
+ ...cfg[device],
92
+ }));
93
+ return deviceConfigs;
94
+ };
95
+
96
+ const pageGroupSettingsForm = (req) => {
97
+ return new Form({
98
+ action: "/page_group/settings/config",
99
+ noSubmitButton: true,
100
+ onChange: `saveAndContinue(this)`,
101
+ fields: [
102
+ {
103
+ name: "missing_screen_info_strategy",
104
+ label: req.__("Missing screen info"),
105
+ sublabel: req.__(
106
+ "What to do if no screen info is given. Reload with parmeters or guess it from the user-agent."
107
+ ),
108
+ type: "String",
109
+ input_type: "select",
110
+ options: [
111
+ {
112
+ label: req.__("Guess from user agent"),
113
+ value: "guess_from_user_agent",
114
+ },
115
+ { label: req.__("Reload"), value: "reload" },
116
+ ],
117
+ required: true,
118
+ },
119
+ ],
120
+ });
121
+ };
122
+
123
+ /**
124
+ * load the screen-info table
125
+ */
126
+ router.get(
127
+ "/settings",
128
+ isAdmin,
129
+ error_catcher(async (req, res) => {
130
+ const deviceConfigs = loadDeviceConfigs();
131
+ const pgForm = pageGroupSettingsForm(req);
132
+ pgForm.values.missing_screen_info_strategy = getState().getConfig(
133
+ "missing_screen_info_strategy",
134
+ "guess_from_user_agent"
135
+ );
136
+ send_infoarch_page({
137
+ res,
138
+ req,
139
+ active_sub: "Pagegroups",
140
+ contents: [
141
+ {
142
+ type: "card",
143
+ title: req.__("User Agent screen infos"),
144
+ contents: [
145
+ p(
146
+ req.__(
147
+ "This screen infos are used when the browser does not send them. " +
148
+ "With 'Missing screen info' set to 'Guess from user agent', the user agent gets mapped to a device with the following values."
149
+ )
150
+ ),
151
+ mkTable(
152
+ [
153
+ {
154
+ label: "Device",
155
+ key: (r) =>
156
+ link(
157
+ `/page_group/settings/edit-device/${r.device}`,
158
+ r.device
159
+ ),
160
+ },
161
+ { label: "width", key: (r) => r.width },
162
+ { label: "height", key: (r) => r.height },
163
+ { label: "innerWidth", key: (r) => r.innerWidth },
164
+ { label: "innerHeight", key: (r) => r.innerHeight },
165
+ {
166
+ label: req.__("Delete"),
167
+ key: (r) =>
168
+ post_delete_btn(
169
+ `/page_group/settings/remove-device/${r.device}`,
170
+ req,
171
+ r.device
172
+ ),
173
+ },
174
+ ],
175
+ deviceConfigs,
176
+ {}
177
+ ),
178
+ div(
179
+ a(
180
+ {
181
+ href: "settings/add-device",
182
+ class: "btn btn-primary mt-1 me-3",
183
+ },
184
+ i({ class: "fas fa-plus-square me-1" }),
185
+ req.__("Add screen info")
186
+ )
187
+ ),
188
+ ],
189
+ },
190
+
191
+ {
192
+ type: "card",
193
+ title: req.__("Page Group settings"),
194
+ contents: [renderForm(pgForm, req.csrfToken())],
195
+ },
196
+ ],
197
+ });
198
+ })
199
+ );
200
+ /**
201
+ * load a form to add a screen-info to the config
202
+ */
203
+ router.get(
204
+ "/settings/add-device",
205
+ isAdmin,
206
+ error_catcher(async (req, res) => {
207
+ send_infoarch_page({
208
+ res,
209
+ req,
210
+ active_sub: "Pagegroups",
211
+ contents: {
212
+ type: "card",
213
+ title: req.__("Add screen info"),
214
+ contents: [
215
+ renderForm(
216
+ deviceForm(req, () => {}),
217
+ req.csrfToken()
218
+ ),
219
+ ],
220
+ },
221
+ });
222
+ })
223
+ );
224
+
225
+ /**
226
+ * add a screen-info to the config
227
+ */
228
+ router.post(
229
+ "/settings/add-device",
230
+ isAdmin,
231
+ error_catcher(async (req, res) => {
232
+ const cfg = getState().getConfig("user_agent_screen_infos", {});
233
+ const validator = (v) => {
234
+ if (cfg[v]) return req.__("Device already exists");
235
+ };
236
+ const form = deviceForm(req, validator);
237
+ form.validate(req.body);
238
+ if (form.hasErrors) {
239
+ send_infoarch_page({
240
+ res,
241
+ req,
242
+ active_sub: "Pagegroups",
243
+ contents: {
244
+ type: "card",
245
+ title: req.__("Add screen info"),
246
+ contents: [renderForm(form, req.csrfToken())],
247
+ },
248
+ });
249
+ } else {
250
+ const { device, width, height, innerWidth, innerHeight } = form.values;
251
+ const newCfg = {
252
+ ...cfg,
253
+ [device]: { width, height, innerWidth, innerHeight },
254
+ };
255
+ await getState().setConfig("user_agent_screen_infos", newCfg);
256
+ req.flash("success", req.__("Screen info added"));
257
+ res.redirect("/page_group/settings");
258
+ }
259
+ })
260
+ );
261
+
262
+ /**
263
+ * remove a screen-info from the config
264
+ */
265
+ router.post(
266
+ "/settings/remove-device/:device",
267
+ isAdmin,
268
+ error_catcher(async (req, res) => {
269
+ const { device } = req.params;
270
+ const cfg = getState().getConfig("user_agent_screen_infos", {});
271
+ const newCfg = { ...cfg };
272
+ delete newCfg[device];
273
+ await getState().setConfig("user_agent_screen_infos", newCfg);
274
+ req.flash("success", req.__("Screen info removed"));
275
+ res.redirect("/page_group/settings");
276
+ })
277
+ );
278
+
279
+ /**
280
+ * load a form to edit a screen-info
281
+ */
282
+ router.get(
283
+ "/settings/edit-device/:device",
284
+ isAdmin,
285
+ error_catcher(async (req, res) => {
286
+ const { device } = req.params;
287
+ const cfg = getState().getConfig("user_agent_screen_infos", {});
288
+ const deviceCfg = cfg[device];
289
+ const form = deviceForm(req, () => {}, device);
290
+ form.values = { device, ...deviceCfg };
291
+ send_infoarch_page({
292
+ res,
293
+ req,
294
+ active_sub: "Pagegroups",
295
+ contents: {
296
+ type: "card",
297
+ title: req.__("Edit screen info"),
298
+ contents: [renderForm(form, req.csrfToken())],
299
+ },
300
+ });
301
+ })
302
+ );
303
+
304
+ /**
305
+ * edit a screen-info
306
+ */
307
+ router.post(
308
+ "/settings/edit-device/:device",
309
+ isAdmin,
310
+ error_catcher(async (req, res) => {
311
+ const { device } = req.params;
312
+ const cfg = getState().getConfig("user_agent_screen_infos", {});
313
+ const validator = (v) => {
314
+ if (cfg[v] && v !== device) return req.__("Device already exists");
315
+ };
316
+ const form = deviceForm(req, validator, device);
317
+ const deviceCfg = cfg[device];
318
+ form.values = { device, ...deviceCfg };
319
+ form.validate(req.body);
320
+ if (form.hasErrors) {
321
+ send_infoarch_page({
322
+ res,
323
+ req,
324
+ active_sub: "Pagegroups",
325
+ contents: {
326
+ type: "card",
327
+ title: req.__("Edit device"),
328
+ contents: [renderForm(form, req.csrfToken())],
329
+ },
330
+ });
331
+ } else {
332
+ const { width, height, innerWidth, innerHeight } = form.values;
333
+ const newCfg = {
334
+ ...cfg,
335
+ [form.values.device]: { width, height, innerWidth, innerHeight },
336
+ };
337
+ if (device !== form.values.device) delete newCfg[device];
338
+ await getState().setConfig("user_agent_screen_infos", newCfg);
339
+ req.flash("success", req.__("Screen info saved"));
340
+ res.redirect("/page_group/settings");
341
+ }
342
+ })
343
+ );
344
+
345
+ /**
346
+ * save the missing_screen_info_strategy
347
+ * This configures what to do if no screen info is given (reload or guess from user-agent)
348
+ */
349
+ router.post(
350
+ "/settings/config",
351
+ isAdmin,
352
+ error_catcher(async (req, res) => {
353
+ const form = pageGroupSettingsForm(req);
354
+ form.validate(req.body);
355
+ if (form.hasErrors) {
356
+ send_infoarch_page({
357
+ res,
358
+ req,
359
+ active_sub: "Pagegroups",
360
+ contents: {
361
+ type: "card",
362
+ title: req.__("Page Group settings"),
363
+ contents: [renderForm(form, req.csrfToken())],
364
+ },
365
+ });
366
+ } else {
367
+ const { missing_screen_info_strategy } = form.values;
368
+ await getState().setConfig(
369
+ "missing_screen_info_strategy",
370
+ missing_screen_info_strategy
371
+ );
372
+ req.flash("success", req.__("Settings saved"));
373
+ res.redirect("/page_group/settings");
374
+ }
375
+ })
376
+ );
377
+
378
+ // perhaps another service to omit endless reload loops here