@saltcorn/server 1.1.2-beta.10 → 1.1.2-beta.12

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 CHANGED
@@ -15,6 +15,14 @@
15
15
 
16
16
  * Upgrade a large number of dependencies (express, typescript, oclif, pg, webpack, typescript, axios, mjml, svelte). Node.js 18+ is require for this release.
17
17
 
18
+ ### Security
19
+
20
+ * View roles are now strictly enforced, including when views are embedded.
21
+
22
+ ### Fixes
23
+
24
+ * Much work on primary keys not called "id"
25
+
18
26
  ## 1.1.1 - Released 2 February 2025
19
27
 
20
28
  * Full-text search improvements:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "1.1.2-beta.10",
3
+ "version": "1.1.2-beta.12",
4
4
  "description": "Server app for Saltcorn, open-source no-code platform",
5
5
  "homepage": "https://saltcorn.com",
6
6
  "main": "index.js",
@@ -8,14 +8,14 @@
8
8
  "dependencies": {
9
9
  "@aws-sdk/client-s3": "^3.735.0",
10
10
  "@dr.pogodin/csurf": "^1.14.1",
11
- "@saltcorn/base-plugin": "1.1.2-beta.10",
12
- "@saltcorn/builder": "1.1.2-beta.10",
13
- "@saltcorn/data": "1.1.2-beta.10",
14
- "@saltcorn/admin-models": "1.1.2-beta.10",
15
- "@saltcorn/filemanager": "1.1.2-beta.10",
16
- "@saltcorn/markup": "1.1.2-beta.10",
17
- "@saltcorn/plugins-loader": "1.1.2-beta.10",
18
- "@saltcorn/sbadmin2": "1.1.2-beta.10",
11
+ "@saltcorn/base-plugin": "1.1.2-beta.12",
12
+ "@saltcorn/builder": "1.1.2-beta.12",
13
+ "@saltcorn/data": "1.1.2-beta.12",
14
+ "@saltcorn/admin-models": "1.1.2-beta.12",
15
+ "@saltcorn/filemanager": "1.1.2-beta.12",
16
+ "@saltcorn/markup": "1.1.2-beta.12",
17
+ "@saltcorn/plugins-loader": "1.1.2-beta.12",
18
+ "@saltcorn/sbadmin2": "1.1.2-beta.12",
19
19
  "@socket.io/cluster-adapter": "^0.2.1",
20
20
  "@socket.io/sticky": "^1.0.1",
21
21
  "adm-zip": "0.5.16",
package/routes/actions.js CHANGED
@@ -653,7 +653,7 @@ const getWorkflowStepForm = async (
653
653
  ...(field.showIf || {}),
654
654
  },
655
655
  };
656
- if (cfgFld.input_type === "code") cfgFld.input_type = "textarea";
656
+ //if (cfgFld.input_type === "code") cfgFld.input_type = "textarea";
657
657
  actionConfigFields.push(cfgFld);
658
658
  }
659
659
  } catch {}
@@ -1854,7 +1854,6 @@ interactive workflows for not logged in
1854
1854
  actions can declare which variables they inject into scope
1855
1855
 
1856
1856
  show unconnected steps
1857
- why is code not initialising
1858
1857
  drag and drop edges
1859
1858
 
1860
1859
  */
package/routes/admin.js CHANGED
@@ -709,8 +709,8 @@ router.post(
709
709
  type === "trigger"
710
710
  ? `/actions`
711
711
  : /^[a-z]+$/g.test(type)
712
- ? `/${type}edit`
713
- : "/"
712
+ ? `/${type}edit`
713
+ : "/"
714
714
  );
715
715
  })
716
716
  );
@@ -1195,7 +1195,7 @@ router.get(
1195
1195
  th({ valign: "top" }, req.__("Saltcorn version")),
1196
1196
  td(
1197
1197
  packagejson.version,
1198
- isRoot
1198
+ isRoot && can_update
1199
1199
  ? post_btn(
1200
1200
  "/admin/upgrade",
1201
1201
  req.__("Upgrade") + " (latest)",
@@ -1206,21 +1206,22 @@ router.get(
1206
1206
  }
1207
1207
  )
1208
1208
  : isRoot && is_latest
1209
- ? span(
1210
- { class: "badge bg-primary ms-2" },
1211
- req.__("Latest")
1212
- ) +
1213
- post_btn(
1214
- "/admin/check-for-upgrade",
1215
- req.__("Check updates"),
1216
- req.csrfToken(),
1217
- {
1218
- btnClass: "btn-primary btn-sm px-1 py-0",
1219
- formClass: "d-inline",
1220
- }
1221
- )
1222
- : "",
1223
- !git_commit &&
1209
+ ? span(
1210
+ { class: "badge bg-primary ms-2" },
1211
+ req.__("Latest")
1212
+ ) +
1213
+ post_btn(
1214
+ "/admin/check-for-upgrade",
1215
+ req.__("Check updates"),
1216
+ req.csrfToken(),
1217
+ {
1218
+ btnClass: "btn-primary btn-sm px-1 py-0",
1219
+ formClass: "d-inline",
1220
+ }
1221
+ )
1222
+ : "",
1223
+ isRoot &&
1224
+ !git_commit &&
1224
1225
  a(
1225
1226
  {
1226
1227
  id: rndid,
@@ -1563,21 +1564,30 @@ const cleanNodeModules = async () => {
1563
1564
  };
1564
1565
 
1565
1566
  const doInstall = async (req, res, version, deepClean, runPull) => {
1567
+ const state = getState();
1568
+ let res_write = (s) => {
1569
+ try {
1570
+ res.write(s);
1571
+ state.log(5, s);
1572
+ } catch (e) {
1573
+ console.error("Install write error: ", e?.message || e);
1574
+ }
1575
+ };
1566
1576
  if (db.getTenantSchema() !== db.connectObj.default_schema) {
1567
1577
  req.flash("error", req.__("Not possible for tenant"));
1568
1578
  res.redirect("/admin");
1569
1579
  } else {
1570
- res.write(
1580
+ res_write(
1571
1581
  version === "latest"
1572
1582
  ? req.__("Starting upgrade, please wait...\n")
1573
1583
  : req.__("Installing %s, please wait...\n", version)
1574
1584
  );
1575
1585
  if (deepClean) {
1576
- res.write(req.__("Cleaning node_modules...\n"));
1586
+ res_write(req.__("Cleaning node_modules...\n"));
1577
1587
  try {
1578
1588
  await cleanNodeModules();
1579
1589
  } catch (e) {
1580
- res.write(req.__("Error cleaning node_modules: %s\n", e.message));
1590
+ res_write(req.__("Error cleaning node_modules: %s\n", e.message));
1581
1591
  }
1582
1592
  }
1583
1593
  const child = spawn(
@@ -1588,33 +1598,37 @@ const doInstall = async (req, res, version, deepClean, runPull) => {
1588
1598
  }
1589
1599
  );
1590
1600
  child.stdout.on("data", (data) => {
1591
- res.write(data);
1601
+ res_write(data);
1592
1602
  });
1593
1603
  child.stderr?.on("data", (data) => {
1594
- res.write(data);
1604
+ res_write(data);
1595
1605
  });
1596
1606
  child.on("exit", async function (code, signal) {
1597
1607
  if (code === 0) {
1598
1608
  if (deepClean) {
1599
- res.write(req.__("Installing sd-notify") + "\n");
1609
+ res_write(req.__("Installing sd-notify") + "\n");
1600
1610
  const sdNotifyCode = await tryInstallSdNotify(req, res);
1601
- res.write(
1611
+ res_write(
1602
1612
  req.__("sd-notify install done with code %s", sdNotifyCode) + "\n"
1603
1613
  );
1604
1614
  }
1605
1615
  if (runPull) {
1606
- res.write(
1616
+ res_write(
1607
1617
  req.__("Pulling the capacitor-builder docker image...") + "\n"
1608
1618
  );
1609
1619
  const pullCode = await pullCapacitorBuilder(req, res, version);
1610
- res.write(req.__("Pull done with code %s", pullCode) + "\n");
1620
+ res_write(req.__("Pull done with code %s", pullCode) + "\n");
1611
1621
  if (pullCode === 0) {
1612
- res.write(req.__("Pruning docker...") + "\n");
1622
+ res_write(req.__("Pruning docker...") + "\n");
1613
1623
  const pruneCode = await pruneDocker(req, res);
1614
- res.write(req.__("Prune done with code %s", pruneCode) + "\n");
1624
+ res_write(req.__("Prune done with code %s", pruneCode) + "\n");
1615
1625
  }
1616
1626
  }
1617
1627
  }
1628
+ setTimeout(() => {
1629
+ getState().processSend("RestartServer");
1630
+ process.exit(0);
1631
+ }, 200);
1618
1632
  res.end(
1619
1633
  version === "latest"
1620
1634
  ? req.__(
@@ -1624,10 +1638,6 @@ const doInstall = async (req, res, version, deepClean, runPull) => {
1624
1638
  `Install done with code ${code}.\n\nPress the BACK button in your browser, then RELOAD the page.`
1625
1639
  )
1626
1640
  );
1627
- setTimeout(() => {
1628
- getState().processSend("RestartServer");
1629
- process.exit(0);
1630
- }, 100);
1631
1641
  });
1632
1642
  }
1633
1643
  };
@@ -1974,9 +1984,8 @@ router.get(
1974
1984
  const filename = `${moment(start).format("YYYYMMDDHHmm")}.html`;
1975
1985
  await File.new_folder("configuration_checks");
1976
1986
  const go = async () => {
1977
- const { passes, errors, pass, warnings } = await runConfigurationCheck(
1978
- req
1979
- );
1987
+ const { passes, errors, pass, warnings } =
1988
+ await runConfigurationCheck(req);
1980
1989
  const end = new Date();
1981
1990
  const secs = Math.round((end.getTime() - start.getTime()) / 1000);
1982
1991
 
@@ -3250,8 +3259,8 @@ router.get(
3250
3259
  mode === "prepare"
3251
3260
  ? "_prepare_step"
3252
3261
  : mode === "finish"
3253
- ? "_finish_step"
3254
- : "";
3262
+ ? "_finish_step"
3263
+ : "";
3255
3264
  res.json({
3256
3265
  finished: await checkFiles(out_dir_name, [
3257
3266
  `logs${stepDesc}.txt`,
@@ -3326,8 +3335,8 @@ router.get(
3326
3335
  mode === "prepare"
3327
3336
  ? "_prepare_step"
3328
3337
  : mode === "finish"
3329
- ? "_finish_step"
3330
- : "";
3338
+ ? "_finish_step"
3339
+ : "";
3331
3340
  const resultMsg = files.find(
3332
3341
  (file) => file.filename === `logs${stepDesc}.txt`
3333
3342
  )
@@ -3786,6 +3795,9 @@ router.post(
3786
3795
  }
3787
3796
  if (form.values.triggers) {
3788
3797
  await db.deleteWhere("_sc_tag_entries", { not: { trigger_id: null } });
3798
+ await db.deleteWhere("_sc_workflow_trace");
3799
+ await db.deleteWhere("_sc_workflow_runs");
3800
+ await db.deleteWhere("_sc_workflow_steps");
3789
3801
  await db.deleteWhere("_sc_triggers");
3790
3802
  await getState().refresh_triggers();
3791
3803
  }
package/routes/delete.js CHANGED
@@ -35,9 +35,11 @@ router.post(
35
35
  // todo check that works after where change
36
36
  const table = Table.findOne({ name: tableName });
37
37
  const role = req.user && req.user.id ? req.user.role_id : 100;
38
+ const where = { [table.pk_name]: id };
39
+
38
40
  try {
39
41
  if (role <= table.min_role_write)
40
- await table.deleteRows({ id }, req.user || { role_id: 100 });
42
+ await table.deleteRows(where, req.user || { role_id: 100 });
41
43
  else if (
42
44
  (table.ownership_field_id || table.ownership_formula) &&
43
45
  req.user
@@ -47,7 +49,7 @@ router.post(
47
49
  { forUser: req.user, forPublic: !req.user }
48
50
  );
49
51
  if (row && table.is_owner(req.user, row))
50
- await table.deleteRows({ id }, req.user || { role_id: 100 });
52
+ await table.deleteRows(where, req.user || { role_id: 100 });
51
53
  else req.flash("error", req.__("Not authorized"));
52
54
  } else
53
55
  req.flash(
package/routes/list.js CHANGED
@@ -282,6 +282,7 @@ router.get(
282
282
  cellClick: "__delete_tabulator_row",
283
283
  });
284
284
  const isDark = getState().getLightDarkMode(req.user) === "dark";
285
+ const pkNm = table.pk_name
285
286
  res.sendWrap(
286
287
  {
287
288
  title: req.__(`%s data table`, table.name),
@@ -428,7 +429,7 @@ router.get(
428
429
  ajax_indicator(true);
429
430
  $.ajax({
430
431
  type: "POST",
431
- url: "/api/${table.name}/" + (row.id||""),
432
+ url: "/api/${table.name}/" + (row.${pkNm}||""),
432
433
  data: row,
433
434
  headers: {
434
435
  "CSRF-Token": _sc_globalCsrf,
@@ -438,8 +439,8 @@ router.get(
438
439
  ajax_indicator(false);
439
440
  //if (item._versions) item._versions = +item._versions + 1;
440
441
  //data.resolve(fixKeys(item));
441
- if(resp.success &&(typeof resp.success ==="number" || typeof resp.success ==="string") && !row.id) {
442
- window.tabulator_table.updateRow(cell.getRow(), {id: resp.success});
442
+ if(resp.success &&(typeof resp.success ==="number" || typeof resp.success ==="string") && !row.${pkNm}) {
443
+ window.tabulator_table.updateRow(cell.getRow(), {${pkNm}: resp.success});
443
444
  }
444
445
 
445
446
  }).fail(function (resp) {
@@ -289,7 +289,7 @@ const getRootPageForm = (pages, pageGroups, roles, req) => {
289
289
  input_type: "select",
290
290
  options: [
291
291
  "",
292
- ...pages.map((p) => p.name),
292
+ ...pages.filter((p) => p.min_role >= r.id).map((p) => p.name),
293
293
  ...pageGroups.map((g) => ({
294
294
  label: `${g.name} (group)`,
295
295
  value: g.name,
package/routes/tables.js CHANGED
@@ -50,6 +50,7 @@ const {
50
50
  code,
51
51
  pre,
52
52
  button,
53
+ text_attr,
53
54
  } = require("@saltcorn/markup/tags");
54
55
  const { stringify } = require("csv-stringify");
55
56
  const TableConstraint = require("@saltcorn/data/models/table_constraints");
@@ -696,7 +697,14 @@ const typeBadges = (f, req) => {
696
697
  if (f.primary_key) s += badge("warning", req.__("Primary key"));
697
698
  if (f.required) s += badge("primary", req.__("Required"));
698
699
  if (f.is_unique) s += badge("success", req.__("Unique"));
699
- if (f.calculated) s += badge("info", req.__("Calculated"));
700
+ if (f.calculated)
701
+ s += badge(
702
+ "info",
703
+ req.__("Calculated"),
704
+ f.expression && f.expression !== "__aggregation"
705
+ ? text_attr(f.expression)
706
+ : undefined
707
+ );
700
708
  if (f.stored) s += badge("warning", req.__("Stored"));
701
709
  return s;
702
710
  };
@@ -978,8 +986,8 @@ router.get(
978
986
  table.name === "users"
979
987
  ? `/useradmin/`
980
988
  : fields.length === 1
981
- ? `javascript:;` // Fix problem with edition of table with only one column ID / Primary Key
982
- : `/list/${encodeURIComponent(table.name)}`,
989
+ ? `javascript:;` // Fix problem with edition of table with only one column ID / Primary Key
990
+ : `/list/${encodeURIComponent(table.name)}`,
983
991
  },
984
992
  i({ class: "fas fa-2x fa-edit" }),
985
993
  "<br/>",
@@ -1459,7 +1467,10 @@ router.get(
1459
1467
  res.redirect(`/table/${table.id}`);
1460
1468
  return;
1461
1469
  }
1462
- const rows = await table.getRows({}, { orderBy: "id", forUser: req.user });
1470
+ const rows = await table.getRows(
1471
+ {},
1472
+ { orderBy: table.pk_name, forUser: req.user }
1473
+ );
1463
1474
  res.setHeader("Content-Type", "text/csv");
1464
1475
  res.setHeader("Content-Disposition", `attachment; filename="${name}.csv"`);
1465
1476
  res.setHeader("Cache-Control", "no-cache");
@@ -1535,12 +1546,12 @@ router.get(
1535
1546
  r.type === "Unique"
1536
1547
  ? r.configuration.fields.join(", ")
1537
1548
  : r.type === "Index" && r.configuration?.field === "_fts"
1538
- ? "Full text search"
1539
- : r.type === "Index"
1540
- ? r.configuration.field
1541
- : r.type === "Formula"
1542
- ? r.configuration.formula
1543
- : "",
1549
+ ? "Full text search"
1550
+ : r.type === "Index"
1551
+ ? r.configuration.field
1552
+ : r.type === "Formula"
1553
+ ? r.configuration.formula
1554
+ : "",
1544
1555
  },
1545
1556
  {
1546
1557
  label: req.__("Delete"),
@@ -2011,6 +2022,7 @@ router.post(
2011
2022
  if (parse_res.error) req.flash("error", parse_res.error);
2012
2023
  else req.flash("success", parse_res.success);
2013
2024
  } catch (e) {
2025
+ console.error("CSV upload error", e);
2014
2026
  req.flash("error", e.message);
2015
2027
  }
2016
2028
  await fs.unlink(f.location);
@@ -647,6 +647,9 @@ describe("clear all page", () => {
647
647
  .send("users=on")
648
648
  .send("config=on")
649
649
  .send("plugins=on")
650
+ .send("triggers=on")
651
+ .send("library=on")
652
+ .send("eventlog=on")
650
653
  .expect(toRedirect("/auth/create_first_user"));
651
654
  });
652
655
  it("restores backup after clear all", async () => {