@saltcorn/server 0.9.4-beta.12 → 0.9.4-beta.13

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/app.js CHANGED
@@ -125,7 +125,19 @@ const getApp = async (opts = {}) => {
125
125
 
126
126
  // https://www.npmjs.com/package/helmet
127
127
  // helmet is secure app by adding HTTP headers
128
- app.use(helmet());
128
+
129
+ const cross_domain_iframe = getState().getConfig(
130
+ "cross_domain_iframe",
131
+ false
132
+ );
133
+
134
+ const helmetOptions = {
135
+ contentSecurityPolicy: false,
136
+ };
137
+
138
+ if (cross_domain_iframe) helmetOptions.xFrameOptions = false;
139
+ app.use(helmet(helmetOptions));
140
+
129
141
  // TODO ch find a better solution
130
142
  app.use(cors());
131
143
  const bodyLimit = getState().getConfig("body_limit");
package/auth/admin.js CHANGED
@@ -42,6 +42,7 @@ const {
42
42
  getBaseDomain,
43
43
  hostname_matches_baseurl,
44
44
  is_hsts_tld,
45
+ check_if_restart_required,
45
46
  } = require("../markup/admin");
46
47
  const { send_verification_email } = require("@saltcorn/data/models/email");
47
48
  const { expressionValidator } = require("@saltcorn/data/models/expression");
@@ -354,6 +355,7 @@ const auth_settings_form = async (req) =>
354
355
  "signup_form",
355
356
  "user_settings_form",
356
357
  "verification_view",
358
+ "logout_url",
357
359
  "elevate_verified",
358
360
  "email_mask",
359
361
  ],
@@ -376,6 +378,7 @@ const http_settings_form = async (req) =>
376
378
  //"cookie_sessions",
377
379
  "public_cache_maxage",
378
380
  "custom_http_headers",
381
+ "cross_domain_iframe",
379
382
  "body_limit",
380
383
  "url_encoded_limit",
381
384
  ],
@@ -508,12 +511,21 @@ router.post(
508
511
  },
509
512
  });
510
513
  } else {
514
+ const restart_required = check_if_restart_required(form, req);
515
+
511
516
  await save_config_from_form(form);
512
517
 
513
518
  if (!req.xhr) {
514
519
  req.flash("success", req.__("HTTP settings updated"));
515
520
  res.redirect("/useradmin/http");
516
- } else res.json({ success: "ok" });
521
+ } else {
522
+ if (restart_required)
523
+ res.json({
524
+ success: "ok",
525
+ notify: req.__("Restart required for changes to take effect."),
526
+ });
527
+ else res.json({ success: "ok" });
528
+ }
517
529
  }
518
530
  })
519
531
  );
@@ -1017,7 +1029,10 @@ router.post(
1017
1029
  ? req.__(` with password %s`, code(password))
1018
1030
  : "";
1019
1031
 
1020
- req.flash("success", req.__(`User %s created`, email) + pwflash);
1032
+ req.flash(
1033
+ pwflash ? "warning" : "success",
1034
+ req.__(`User %s created`, email) + pwflash
1035
+ );
1021
1036
 
1022
1037
  if (rnd_password && send_pwreset_email)
1023
1038
  await send_reset_email(u, req, { creating: true });
package/auth/routes.js CHANGED
@@ -332,17 +332,18 @@ router.get("/logout", async (req, res, next) => {
332
332
  res.json({ success: true });
333
333
  } else if (req.logout) {
334
334
  req.logout(function (err) {
335
+ const destination = getState().getConfig("logout_url", "/auth/login");
335
336
  if (req.session.destroy)
336
337
  req.session.destroy((err) => {
337
338
  if (err) return next(err);
338
339
  req.logout(() => {
339
- res.redirect("/auth/login");
340
+ res.redirect(destination);
340
341
  });
341
342
  });
342
343
  else {
343
344
  req.logout(function (err) {
344
345
  req.session = null;
345
- res.redirect("/auth/login");
346
+ res.redirect(destination);
346
347
  });
347
348
  }
348
349
  });
package/locales/en.json CHANGED
@@ -1366,5 +1366,14 @@
1366
1366
  "Serve a random page, ignoring the eligible formula. Within a session, reloads will always deliver the same page. This is a basic requirement for A/B testing.": "Serve a random page, ignoring the eligible formula. Within a session, reloads will always deliver the same page. This is a basic requirement for A/B testing.",
1367
1367
  "Pagegroup %s not found": "Pagegroup %s not found",
1368
1368
  "Create trigger": "Create trigger",
1369
- "Omit the menu from this view": "Omit the menu from this view"
1370
- }
1369
+ "Omit the menu from this view": "Omit the menu from this view",
1370
+ "Cross-domain iframe": "Cross-domain iframe",
1371
+ "Allow embedding in iframe on different domains. Unsets the X-Frame-Options header": "Allow embedding in iframe on different domains. Unsets the X-Frame-Options header",
1372
+ "Logout URL": "Logout URL",
1373
+ "The URL to direct to after logout": "The URL to direct to after logout",
1374
+ "Runtime informations": "Runtime informations",
1375
+ "open logs viewer": "open logs viewer",
1376
+ "Server logs": "Server logs",
1377
+ "Timestamp": "Timestamp",
1378
+ "Message": "Message"
1379
+ }
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.9.4-beta.12",
3
+ "version": "0.9.4-beta.13",
4
4
  "description": "Server app for Saltcorn, open-source no-code platform",
5
5
  "homepage": "https://saltcorn.com",
6
6
  "main": "index.js",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
9
  "@aws-sdk/client-s3": "^3.451.0",
10
- "@saltcorn/base-plugin": "0.9.4-beta.12",
11
- "@saltcorn/builder": "0.9.4-beta.12",
12
- "@saltcorn/data": "0.9.4-beta.12",
13
- "@saltcorn/admin-models": "0.9.4-beta.12",
14
- "@saltcorn/filemanager": "0.9.4-beta.12",
15
- "@saltcorn/markup": "0.9.4-beta.12",
16
- "@saltcorn/sbadmin2": "0.9.4-beta.12",
10
+ "@saltcorn/base-plugin": "0.9.4-beta.13",
11
+ "@saltcorn/builder": "0.9.4-beta.13",
12
+ "@saltcorn/data": "0.9.4-beta.13",
13
+ "@saltcorn/admin-models": "0.9.4-beta.13",
14
+ "@saltcorn/filemanager": "0.9.4-beta.13",
15
+ "@saltcorn/markup": "0.9.4-beta.13",
16
+ "@saltcorn/sbadmin2": "0.9.4-beta.13",
17
17
  "@socket.io/cluster-adapter": "^0.2.1",
18
18
  "@socket.io/sticky": "^1.0.1",
19
19
  "adm-zip": "0.5.10",
@@ -33,7 +33,7 @@
33
33
  "express-session": "^1.17.1",
34
34
  "greenlock": "^4.0.4",
35
35
  "greenlock-express": "^4.0.3",
36
- "helmet": "^3.23.3",
36
+ "helmet": "^7.1.0",
37
37
  "i18n": "^0.15.1",
38
38
  "imapflow": "1.0.123",
39
39
  "jsonwebtoken": "^9.0.0",
@@ -0,0 +1,156 @@
1
+ var logViewerHelpers = (() => {
2
+ const messages = [];
3
+ let currentPage = 1;
4
+ const rowsPerPage = 20;
5
+ const dateOptions = {
6
+ month: "short",
7
+ day: "numeric",
8
+ hour: "2-digit",
9
+ minute: "2-digit",
10
+ second: "2-digit",
11
+ };
12
+ let lostConnection = false;
13
+
14
+ const logLevelColor = (level) => {
15
+ switch (parseInt(level)) {
16
+ case 4:
17
+ return "text-primary";
18
+ case 3:
19
+ return "text-warning";
20
+ case 2:
21
+ return "text-info";
22
+ case 1:
23
+ return "text-danger";
24
+ }
25
+ return "";
26
+ };
27
+
28
+ const buildLogRow = (date, level, text) => {
29
+ return `
30
+ <tr>
31
+ <td>
32
+ ${new Date(date).toLocaleDateString(
33
+ window.detected_locale || "en",
34
+ dateOptions
35
+ )}
36
+ </td>
37
+ <td class="${logLevelColor(level)} fw-bold">
38
+ ${text}
39
+ </td>
40
+ </tr>
41
+ `;
42
+ };
43
+
44
+ const buildPaginationItem = (n) => `
45
+ <li class="page-item">
46
+ <span
47
+ class="page-link link-style"
48
+ onclick="logViewerHelpers.goToLogsPage(${n})"
49
+ role="link">
50
+ ${n}
51
+ </span>
52
+ </li>
53
+ `;
54
+
55
+ const handleLogMsg = (msg) => {
56
+ messages.push(msg);
57
+ const tbl = $("#_sc_logs_tbl_id_");
58
+ if (currentPage === 1) {
59
+ const tbody = tbl.find("tbody");
60
+ const allTblRows = tbody.find("tr");
61
+ if (allTblRows.length >= rowsPerPage)
62
+ allTblRows[allTblRows.length - 1].remove();
63
+ tbody.prepend(buildLogRow(msg.time, msg.level, msg.text));
64
+ }
65
+ if (messages.length % rowsPerPage === 1) {
66
+ const pagination = tbl.find(".pagination");
67
+ pagination.append(
68
+ buildPaginationItem(Math.trunc(messages.length / rowsPerPage + 1))
69
+ );
70
+ }
71
+ logViewerHelpers.goToLogsPage(currentPage);
72
+ };
73
+ const startTrackingMsg = () => {
74
+ const startedMsg = {
75
+ time: new Date().toLocaleDateString("de-DE", dateOptions),
76
+ level: 5,
77
+ text: "tracking started",
78
+ };
79
+ messages.push(startedMsg);
80
+ const tbl = $("#_sc_logs_tbl_id_");
81
+ tbl
82
+ .find("tbody")
83
+ .append(buildLogRow(startedMsg.time, startedMsg.level, startedMsg.text));
84
+ };
85
+
86
+ const handleDisconnect = () => {
87
+ lostConnection = true;
88
+ $("#server-logs-card-id")
89
+ .find(".sc-error-indicator")
90
+ .css("display", "inline");
91
+ notifyAlert({
92
+ type: "danger",
93
+ text: "lost the server connection",
94
+ });
95
+ };
96
+
97
+ const handleConnect = (socket) => {
98
+ socket.emit("join_log_room", (ack) => {
99
+ if (ack) {
100
+ if (ack.status === "ok") {
101
+ $("#server-logs-card-id")
102
+ .find(".sc-error-indicator")
103
+ .css("display", "none");
104
+ if (lostConnection) {
105
+ lostConnection = false;
106
+ emptyAlerts();
107
+ notifyAlert({
108
+ type: "success",
109
+ text: "You are connected again",
110
+ });
111
+ }
112
+ } else if (ack.status === "error" && ack.msg) {
113
+ notifyAlert({
114
+ type: "danger",
115
+ text: `Unable to join the log room: ${ack.msg}`,
116
+ });
117
+ } else {
118
+ notifyAlert({
119
+ type: "danger",
120
+ text: "Unable to join the log room: Unknow error",
121
+ });
122
+ }
123
+ } else {
124
+ notifyAlert({
125
+ type: "danger",
126
+ text: "Unable to join the log room",
127
+ });
128
+ }
129
+ });
130
+ };
131
+
132
+ return {
133
+ init_log_socket: () => {
134
+ const socket = io({ transports: ["websocket"] });
135
+ startTrackingMsg();
136
+ socket.on("connect", () => handleConnect(socket));
137
+ socket.on("disconnect", handleDisconnect);
138
+ socket.on("log_msg", handleLogMsg);
139
+ },
140
+ goToLogsPage: (n) => {
141
+ currentPage = n;
142
+ $(".page-item").removeClass("active");
143
+ $(`.page-item:nth-child(${n})`).addClass("active");
144
+ let end = messages.length - rowsPerPage * n;
145
+ let start = end + rowsPerPage;
146
+ const tbl = $("#_sc_logs_tbl_id_");
147
+ const tbody = tbl.find("tbody");
148
+ tbody.empty();
149
+ for (let i = start; i > end; i--) {
150
+ if (i < 1) break;
151
+ const msg = messages[i - 1];
152
+ tbody.append(buildLogRow(msg.time, msg.level, msg.text));
153
+ }
154
+ },
155
+ };
156
+ })();
package/routes/actions.js CHANGED
@@ -11,6 +11,7 @@ const {
11
11
  addOnDoneRedirect,
12
12
  is_relative_url,
13
13
  } = require("./utils.js");
14
+ const { ppVal } = require("@saltcorn/data/utils");
14
15
  const { getState } = require("@saltcorn/data/db/state");
15
16
  const Trigger = require("@saltcorn/data/models/trigger");
16
17
  const { getTriggerList } = require("./common_lists");
@@ -631,12 +632,6 @@ router.get(
631
632
  const { id } = req.params;
632
633
  const trigger = await Trigger.findOne({ id });
633
634
  const output = [];
634
- const ppVal = (x) =>
635
- typeof x === "string"
636
- ? x
637
- : typeof x === "function"
638
- ? x.toString()
639
- : JSON.stringify(x, null, 2);
640
635
  const fakeConsole = {
641
636
  log(...s) {
642
637
  console.log(...s);
package/routes/admin.js CHANGED
@@ -2541,6 +2541,52 @@ router.post(
2541
2541
  }
2542
2542
  })
2543
2543
  );
2544
+
2545
+ router.get(
2546
+ "/dev/logs_viewer",
2547
+ isAdmin,
2548
+ error_catcher(async (req, res) => {
2549
+ return send_admin_page({
2550
+ res,
2551
+ req,
2552
+ active_sub: "Development",
2553
+ contents: {
2554
+ above: [
2555
+ {
2556
+ type: "card",
2557
+ id: "server-logs-card-id",
2558
+ title: req.__("Server logs"),
2559
+ titleErrorInidicator: true,
2560
+ contents: mkTable(
2561
+ [
2562
+ {
2563
+ label: req.__("Timestamp"),
2564
+ width: "15%",
2565
+ },
2566
+ { label: req.__("Message") },
2567
+ ],
2568
+ [],
2569
+ {
2570
+ pagination: {
2571
+ current_page: 1,
2572
+ pages: 1,
2573
+ get_page_link: () => "logViewerHelpers.goToLogsPage(1)",
2574
+ },
2575
+ tableId: "_sc_logs_tbl_id_",
2576
+ }
2577
+ ),
2578
+ },
2579
+ script({
2580
+ src: `/static_assets/${db.connectObj.version_tag}/socket.io.min.js`,
2581
+ }),
2582
+ script({ src: "/log_viewer_utils.js" }),
2583
+ script(domReady(`logViewerHelpers.init_log_socket();`)),
2584
+ ],
2585
+ },
2586
+ });
2587
+ })
2588
+ );
2589
+
2544
2590
  /**
2545
2591
  * Dev / Admin
2546
2592
  */
@@ -2574,10 +2620,28 @@ admin_config_route({
2574
2620
  req,
2575
2621
  active_sub: "Development",
2576
2622
  contents: {
2577
- type: "card",
2578
- title: req.__("Development settings"),
2579
- titleAjaxIndicator: true,
2580
- contents: [renderForm(form, req.csrfToken())],
2623
+ above: [
2624
+ {
2625
+ type: "card",
2626
+ title: req.__("Development settings"),
2627
+ titleAjaxIndicator: true,
2628
+ contents: [renderForm(form, req.csrfToken())],
2629
+ },
2630
+ {
2631
+ type: "card",
2632
+ title: req.__("Runtime informations"),
2633
+ contents: [
2634
+ div(
2635
+ { class: "row form-group" },
2636
+ a(
2637
+ { class: "d-block", href: "dev/logs_viewer" },
2638
+ req.__("open logs viewer")
2639
+ ),
2640
+ i("Open a log viewer for the current server messages")
2641
+ ),
2642
+ ],
2643
+ },
2644
+ ],
2581
2645
  },
2582
2646
  });
2583
2647
  },
@@ -524,7 +524,8 @@ const getPageList = async (
524
524
  },
525
525
  {
526
526
  label: req.__("Edit"),
527
- key: (r) => link(`/pageedit/edit/${r.name}`, req.__("Edit")),
527
+ key: (r) =>
528
+ link(`/pageedit/edit/${encodeURIComponent(r.name)}`, req.__("Edit")),
528
529
  },
529
530
  !tagId
530
531
  ? {
package/routes/utils.js CHANGED
@@ -147,9 +147,10 @@ const set_custom_http_headers = (res, req, state) => {
147
147
  /**
148
148
  * Tries to recognize tenant from HTTP Request
149
149
  * @param {object} req
150
+ * @param {number|undefined} hostPartsOffset (optional) for socketIO, to get the tenant with localhost
150
151
  * @returns {string}
151
152
  */
152
- const get_tenant_from_req = (req) => {
153
+ const get_tenant_from_req = (req, hostPartsOffset) => {
153
154
  if (req.subdomains && req.subdomains.length > 0)
154
155
  return req.subdomains[req.subdomains.length - 1];
155
156
 
@@ -157,7 +158,8 @@ const get_tenant_from_req = (req) => {
157
158
  return db.connectObj.default_schema;
158
159
  if (!req.subdomains && req.headers.host) {
159
160
  const parts = req.headers.host.split(".");
160
- if (parts.length < 3) return db.connectObj.default_schema;
161
+ if (parts.length < (!hostPartsOffset ? 3 : 3 - hostPartsOffset))
162
+ return db.connectObj.default_schema;
161
163
  else return parts[0];
162
164
  }
163
165
  };
package/serve.js CHANGED
@@ -112,6 +112,10 @@ const initMaster = async ({ disableMigrate }, useClusterAdaptor = true) => {
112
112
  const tenants = await getAllTenants();
113
113
  await init_multi_tenant(loadAllPlugins, disableMigrate, tenants);
114
114
  }
115
+ eachTenant(async () => {
116
+ const state = getState();
117
+ if (state) await state.setConfig("joined_log_socket_ids", []);
118
+ });
115
119
  if (useClusterAdaptor) setupPrimary();
116
120
  };
117
121
 
@@ -283,7 +287,7 @@ module.exports =
283
287
  })
284
288
  .ready((glx) => {
285
289
  const httpsServer = glx.httpsServer();
286
- setupSocket(httpsServer);
290
+ setupSocket(appargs?.subdomainOffset, httpsServer);
287
291
  httpsServer.setTimeout(timeout * 1000);
288
292
  process.on("message", workerDispatchMsg);
289
293
  glx.serveApp(app);
@@ -350,7 +354,7 @@ const nonGreenlockWorkerSetup = async (appargs, port) => {
350
354
  // todo timeout to config
351
355
  httpServer.setTimeout(timeout * 1000);
352
356
  httpsServer.setTimeout(timeout * 1000);
353
- setupSocket(httpServer, httpsServer);
357
+ setupSocket(appargs?.subdomainOffset, httpServer, httpsServer);
354
358
  httpServer.listen(port, () => {
355
359
  console.log("HTTP Server running on port 80");
356
360
  });
@@ -363,7 +367,7 @@ const nonGreenlockWorkerSetup = async (appargs, port) => {
363
367
  // server with http only
364
368
  const http = require("http");
365
369
  const httpServer = http.createServer(app);
366
- setupSocket(httpServer);
370
+ setupSocket(appargs?.subdomainOffset, httpServer);
367
371
 
368
372
  // todo timeout to config
369
373
  // todo refer in doc to httpserver doc
@@ -380,7 +384,7 @@ const nonGreenlockWorkerSetup = async (appargs, port) => {
380
384
  *
381
385
  * @param {...*} servers
382
386
  */
383
- const setupSocket = (...servers) => {
387
+ const setupSocket = (subdomainOffset, ...servers) => {
384
388
  // https://socket.io/docs/v4/middlewares/
385
389
  const wrap = (middleware) => (socket, next) =>
386
390
  middleware(socket.request, {}, next);
@@ -398,6 +402,12 @@ const setupSocket = (...servers) => {
398
402
  getState().setRoomEmitter((tenant, viewname, room_id, msg) => {
399
403
  io.to(`${tenant}_${viewname}_${room_id}`).emit("message", msg);
400
404
  });
405
+
406
+ getState().setLogEmitter((tenant, level, msg) => {
407
+ const time = new Date().valueOf();
408
+ io.to(`_logs_${tenant}_`).emit("log_msg", { text: msg, time, level });
409
+ });
410
+
401
411
  io.on("connection", (socket) => {
402
412
  socket.on("join_room", ([viewname, room_id]) => {
403
413
  const ten = get_tenant_from_req(socket.request) || "public";
@@ -418,5 +428,42 @@ const setupSocket = (...servers) => {
418
428
  if (ten && ten !== "public") db.runWithTenant(ten, f);
419
429
  else f();
420
430
  });
431
+
432
+ socket.on("join_log_room", async (callback) => {
433
+ const tenant =
434
+ get_tenant_from_req(socket.request, subdomainOffset) || "public";
435
+ const f = async () => {
436
+ try {
437
+ const user = socket.request.user;
438
+ if (!user || user.role_id !== 1) throw new Error("Not authorized");
439
+ else {
440
+ socket.join(`_logs_${tenant}_`);
441
+ const socketIds = await getState().getConfig(
442
+ "joined_log_socket_ids"
443
+ );
444
+ socketIds.push(socket.id);
445
+ await getState().setConfig("joined_log_socket_ids", [...socketIds]);
446
+ callback({ status: "ok" });
447
+ }
448
+ } catch (err) {
449
+ getState().log(1, `Socket join_logs stream: ${err.stack}`);
450
+ callback({ status: "error", msg: err.message || "unknown error" });
451
+ }
452
+ };
453
+ if (tenant && tenant !== "public") db.runWithTenant(tenant, f);
454
+ else await f();
455
+ });
456
+
457
+ socket.on("disconnect", async () => {
458
+ const tenant =
459
+ get_tenant_from_req(socket.request, subdomainOffset) || "public";
460
+ const f = async () => {
461
+ const socketIds = await getState().getConfig("joined_log_socket_ids");
462
+ const newSocketIds = socketIds.filter((id) => id !== socket.id);
463
+ await getState().setConfig("joined_log_socket_ids", newSocketIds);
464
+ };
465
+ if (tenant && tenant !== "public") db.runWithTenant(tenant, f);
466
+ else f();
467
+ });
421
468
  });
422
469
  };
@@ -555,7 +555,7 @@ describe("diagram", () => {
555
555
  });
556
556
 
557
557
  /**
558
- * Diagram tests
558
+ * Tags tests
559
559
  */
560
560
  describe("tags", () => {
561
561
  itShouldRedirectUnauthToLogin("/tag");
@@ -604,6 +604,12 @@ describe("tags", () => {
604
604
  .expect(toRedirect("/tag"));
605
605
  });
606
606
  });
607
+
608
+ describe("server logs viewer", () => {
609
+ itShouldRedirectUnauthToLogin("/admin/dev/logs_viewer");
610
+ itShouldIncludeTextForAdmin("/admin/dev/logs_viewer", "Server logs");
611
+ });
612
+
607
613
  /**
608
614
  * Clear all tests
609
615
  */