@saltcorn/server 1.0.0-beta.9 → 1.0.0-rc.2

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
@@ -306,13 +306,7 @@ const getApp = async (opts = {}) => {
306
306
  ? new Date(u.last_mobile_login).valueOf()
307
307
  : u.last_mobile_login) <= jwt_payload.iat
308
308
  ) {
309
- return done(null, {
310
- email: u.email,
311
- id: u.id,
312
- role_id: u.role_id,
313
- language: u.language,
314
- tenant: db.getTenantSchema(),
315
- });
309
+ return done(null, u.session_object);
316
310
  } else {
317
311
  return done(null, { role_id: 100 });
318
312
  }
@@ -447,6 +441,9 @@ Sitemap: ${base}sitemap.xml
447
441
  app.get("*", function (req, res) {
448
442
  res.status(404).sendWrap(req.__("Not found"), h1(req.__("Page not found")));
449
443
  });
444
+
445
+ //prevent prototype pollution
446
+ delete Object.prototype.__proto__;
450
447
  return app;
451
448
  };
452
449
  module.exports = getApp;
package/auth/admin.js CHANGED
@@ -365,6 +365,7 @@ const auth_settings_form = async (req) =>
365
365
  "signup_role",
366
366
  "elevate_verified",
367
367
  "email_mask",
368
+ "plain_password_triggers",
368
369
  ],
369
370
  action: "/useradmin/settings",
370
371
  submitLabel: req.__("Save"),
package/auth/routes.js CHANGED
@@ -1133,7 +1133,10 @@ router.post(
1133
1133
  }
1134
1134
  if (getState().get2FApolicy(req.user) === "Mandatory") {
1135
1135
  res.redirect("/auth/twofa/setup/totp");
1136
- } else if (req.body.dest && is_relative_url(req.body.dest)) {
1136
+ } else if (
1137
+ req.body.dest &&
1138
+ is_relative_url(decodeURIComponent(req.body.dest))
1139
+ ) {
1137
1140
  res.redirect(decodeURIComponent(req.body.dest));
1138
1141
  } else res.redirect("/");
1139
1142
  })
@@ -0,0 +1,11 @@
1
+ The where formula allows you to restrict the rows included in the aggregations.
2
+
3
+ The formula should take the form of a JavaScript boolean expression such as
4
+ `a === b` or `x>y && w==1` when is based on the row values in the rows in the target table for
5
+ inclusion in the aggregation. For instance, if you are displaying aggregations about
6
+ a row from a Projects table, and a Task table has a key to projects field and a Boolean
7
+ field called `completed`, you may want to show the number of not-completed tasks for the Project.
8
+ In this case use the where expression `completed == false`.
9
+
10
+ You cannot here use view state or values from the row about which you are displaying aggregations. However,
11
+ if a user is logged in, the `user` variable can be used to access user fields, such as `user.id`.
@@ -0,0 +1,28 @@
1
+ A calculated field is a column that holds values that are not entered
2
+ or changed directly but are instead calculated automatically from other
3
+ values in that row of from rows in other tables related by a key field.
4
+
5
+ There are two types of calculated field values:
6
+
7
+ *Not stored*: calculated fields that are not stored in the database are
8
+ recalculated every time the row is accessed. They are defined by a
9
+ JavaScript expression where the values of the other fields are in scope.
10
+ They are suitable for quick calculations that depend only on other values
11
+ in the same row. Because they are recalculated they are guaranteed to be
12
+ up to date. Non-stored fields cannot access data in related rows by join
13
+ fields or aggregations and cannot use asynchronous functions provided by
14
+ modules. They can use synchronous functions.
15
+
16
+ *Stored*: the values of stored fields are stored in the database table.
17
+ They are more flexible in the way they can be defined. Stored fields can
18
+ be defined either by a JavaScript expression or by an aggregation. When
19
+ defined by a JavaScript expression, join fields can be accessed by the
20
+ dot notation. Stored fields may also use both synchronous and asynchronous f
21
+ unctions provided by modules. Asynchronous functions can in principle do
22
+ input/output including calling APIs. This expression will be evaluated
23
+ every time the row changes.
24
+
25
+ Stored fields that are defined by an aggregation are automatically updated
26
+ when one of the child rows is updated. Stored fields that are based on
27
+ JavaScript expressions involving join felds are not automatically updated.
28
+ You need to run the recalculate stored fields action to update them 
@@ -0,0 +1,55 @@
1
+ You can define how a date value should be shown by specifying a date format string
2
+ which supports a range of formatting options. The formatting is provided by the
3
+ [Day.js](https://day.js.org/docs/en/display/format) library with the
4
+ [AdvancedFormat](https://day.js.org/docs/en/plugin/advanced-format) extension. Those links
5
+ give the full definition for the available format options.
6
+
7
+ When a date is rendered with a format string, certain specific character sequences are
8
+ substituted for parts of the date value. Any unrecognized characters are repeated verbatim
9
+ in the output and you can used that for separator charcters such as hyphens (-) or slashes (/).
10
+
11
+ For example, the timepoint 11.15am on Friday, 4 October 2024 is rendered, according to the
12
+ format string:
13
+
14
+ {{# const d = new Date('2024-10-04T11:15:00.000')}}
15
+
16
+ | Format string | Output |
17
+ | ------------------- | ------------ |
18
+ | YYYY-MM-DD | {{ moment(d).format("YYYY-MM-DD")}} |
19
+ | H:mm on dddd, D MMMM YYYY | {{ moment(d).format("H:mm on dddd, D MMMM YYYY")}} |
20
+
21
+ The full list of supported format sequences are:
22
+
23
+ | Format | Output | Description |
24
+ | --- | --- | --- |
25
+ | `YY` | 18 | Two-digit year |
26
+ | `YYYY` | 2018 | Four-digit year |
27
+ | `M` | 1-12 | The month, beginning at 1 |
28
+ | `MM` | 01-12 | The month, 2-digits |
29
+ | `MMM` | Jan-Dec | The abbreviated month name |
30
+ | `MMMM` | January-December | The full month name |
31
+ | `D` | 1-31 | The day of the month |
32
+ | `DD` | 01-31 | The day of the month, 2-digits |
33
+ | `d` | 0-6 | The day of the week, with Sunday as 0 |
34
+ | `dd` | Su-Sa | The min name of the day of the week |
35
+ | `ddd` | Sun-Sat | The short name of the day of the week |
36
+ | `dddd` | Sunday-Saturday | The name of the day of the week |
37
+ | `H` | 0-23 | The hour |
38
+ | `HH` | 00-23 | The hour, 2-digits |
39
+ | `h` | 1-12 | The hour, 12-hour clock |
40
+ | `hh` | 01-12 | The hour, 12-hour clock, 2-digits |
41
+ | `m` | 0-59 | The minute |
42
+ | `mm` | 00-59 | The minute, 2-digits |
43
+ | `s` | 0-59 | The second |
44
+ | `ss` | 00-59 | The second, 2-digits |
45
+ | `SSS` | 000-999 | The millisecond, 3-digits |
46
+ | `Z` | +05:00 | The offset from UTC, ±HH:mm |
47
+ | `ZZ` | +0500 | The offset from UTC, ±HHmm |
48
+ | `A` | AM PM | |
49
+ | `a` | am pm | |
50
+ | `Q` | 1-4 | Quarter |
51
+ | `Do` | 1st 2nd ... 31st | Day of Month with ordinal |
52
+ | `k` | 1-24 | The hour, beginning at 1 |
53
+ | `kk` | 01-24 | The hour, 2-digits, beginning at 1 |
54
+ | `X` | 1360013296 | Unix Timestamp in second |
55
+ | `x` | 1360013296123 | Unix Timestamp in millisecond |
@@ -0,0 +1,4 @@
1
+ Mmark a field as protected in order to set a minimum role required
2
+ to write to this field. This will be checked in addition to the role
3
+ required to write to the whole table; it will also stop changing the
4
+ field value even if the user is the owner of the row.
@@ -0,0 +1,19 @@
1
+ A snapshot is a saved format of your application that includes all of
2
+ your application structure (table and fields definitions, views pages
3
+ and configuration settings) but excludes all of your data, that is,
4
+ table rows and files. A snapshot is the same format as a pack - a snapshot
5
+ is in fact merely a full pack of the application. (To create a partial
6
+ pack of the application, use tags or the pack export in the module store).
7
+
8
+ Snapshots have several possible uses:
9
+
10
+ * If you have two different servers one for production and one for development
11
+ then you can make changes to the development server and use a snapshot to
12
+ move those changes to the production instance
13
+
14
+ * By enabling periodic snapshots Saltcorn allows you to restore versions of
15
+ views, pages and triggers after you have made changes. This allows you to
16
+ undo mistakes that you have made in building.
17
+
18
+ * If your application contains a large amount of data that can quickly be
19
+ regenerated you may want to use snapshots instead of backups to save your application.
@@ -0,0 +1,42 @@
1
+ When version history on a database table is enabled in Saltcorn,
2
+ changes to row values are recorded in a secondary, hidden table.
3
+ This allows you to restore previous versions of individual rows
4
+ and undelete deleted rows.
5
+
6
+ The downside to enabling version history on tables is that this
7
+ can lead to a lot of data being retained which may mean your
8
+ database disk requirements will grow. In addition it will impact
9
+ performance when making changes to rows. There should be no
10
+ performance impact on reading data from the database.
11
+
12
+ Note that when the history is first enabled the existing rows in
13
+ the table are not copied into the history table in their present
14
+ state. Only when a change is made to a row (or a row is created)
15
+ is the new row copied into the history table.
16
+
17
+ Enabling version history gives the administrator access to the
18
+ version history in the administrator's data edit facility (Edit
19
+ button on the table page). Each row will show the number of versions
20
+ recorded and by clicking this you will see the list of versions.
21
+ Each version shows the value for each field, the user who made the
22
+ change, the time at which the change was made and an option to
23
+ restore. This is the only facility for interacting with the version
24
+ history in core Saltcorn.
25
+
26
+ The history-control module provides additional facilities for
27
+ interacting with the history. The history-control module provides:
28
+
29
+ * Actions to undo and redo row changes that will move the current
30
+ row backwards and forwards in the table history
31
+
32
+ * A history field difference view pattern that allows users to
33
+ compare different versions of a field value
34
+
35
+ * A "History for database table" table provider that can be used to create a "virtual"
36
+ table based on the history of an existing table. This table will have
37
+ all the fields of the original table and fields for the user making
38
+ the change and the time of the change being made
39
+
40
+ * A restore_from_history action that can be used on a view of the table
41
+ history (on the provided table) to restore or undelete any version of
42
+ a specific row
package/help/index.js CHANGED
@@ -5,6 +5,7 @@ const _ = require("underscore");
5
5
  const fs = require("fs").promises;
6
6
  const MarkdownIt = require("markdown-it"),
7
7
  md = new MarkdownIt();
8
+ const moment = require("moment");
8
9
 
9
10
  const { pre } = require("@saltcorn/markup/tags");
10
11
  const path = require("path");
@@ -33,6 +34,7 @@ const get_help_markup = async (topic, query, req) => {
33
34
  scState: getState(),
34
35
  query,
35
36
  oneOf,
37
+ moment,
36
38
  };
37
39
  const mdTemplate = await get_md_file(topic);
38
40
  if (!mdTemplate) return { markup: "Topic not found" };
package/load_plugins.js CHANGED
@@ -12,6 +12,73 @@ const { isRoot } = require("@saltcorn/data/utils");
12
12
  const { eachTenant } = require("@saltcorn/admin-models/models/tenant");
13
13
 
14
14
  const PluginInstaller = require("@saltcorn/plugins-loader/plugin_installer");
15
+ const npmFetch = require("npm-registry-fetch");
16
+ const packagejson = require("./package.json");
17
+ const {
18
+ supportedVersion,
19
+ resolveLatest,
20
+ } = require("@saltcorn/plugins-loader/stable_versioning");
21
+
22
+ const isFixedPlugin = (plugin) =>
23
+ plugin.location === "@saltcorn/sbadmin2" ||
24
+ plugin.location === "@saltcorn/base-plugin";
25
+
26
+ /**
27
+ * return the cached engine infos or fetch them from npm and update the cache
28
+ * @param plugin plugin to load
29
+ */
30
+ const getEngineInfos = async (plugin, forceFetch) => {
31
+ const rootState = getRootState();
32
+ const cached = rootState.getConfig("engines_cache", {}) || {};
33
+ if (cached[plugin.location] && !forceFetch) {
34
+ return cached[plugin.location];
35
+ } else {
36
+ getState().log(5, `Fetching versions for '${plugin.location}'`);
37
+ const pkgInfo = await npmFetch.json(
38
+ `https://registry.npmjs.org/${plugin.location}`
39
+ );
40
+ const versions = pkgInfo.versions;
41
+ const newCached = {};
42
+ for (const [k, v] of Object.entries(versions)) {
43
+ newCached[k] = v.engines?.saltcorn
44
+ ? { engines: { saltcorn: v.engines.saltcorn } }
45
+ : {};
46
+ }
47
+ cached[plugin.location] = newCached;
48
+ await rootState.setConfig("engines_cache", { ...cached });
49
+ return newCached;
50
+ }
51
+ };
52
+
53
+ /**
54
+ * checks the saltcorn engine property and changes the plugin version if necessary
55
+ * @param plugin plugin to load
56
+ */
57
+ const ensurePluginSupport = async (plugin, forceFetch) => {
58
+ let versions = await getEngineInfos(plugin, forceFetch);
59
+ if (
60
+ plugin.version &&
61
+ plugin.version !== "latest" &&
62
+ !versions[plugin.version] &&
63
+ !forceFetch
64
+ ) {
65
+ versions = await getEngineInfos(plugin, true);
66
+ }
67
+ const supported = supportedVersion(
68
+ plugin.version || "latest",
69
+ versions,
70
+ packagejson.version
71
+ );
72
+ if (!supported)
73
+ throw new Error(
74
+ `Unable to find a supported version for '${plugin.location}'`
75
+ );
76
+ else if (
77
+ supported !== plugin.version ||
78
+ (plugin.version === "latest" && supported !== resolveLatest(versions))
79
+ )
80
+ plugin.version = supported;
81
+ };
15
82
 
16
83
  /**
17
84
  * Load one plugin
@@ -19,7 +86,16 @@ const PluginInstaller = require("@saltcorn/plugins-loader/plugin_installer");
19
86
  * @param plugin - plugin to load
20
87
  * @param force - force flag
21
88
  */
22
- const loadPlugin = async (plugin, force) => {
89
+ const loadPlugin = async (plugin, force, forceFetch) => {
90
+ if (plugin.source === "npm" && !isFixedPlugin(plugin)) {
91
+ try {
92
+ await ensurePluginSupport(plugin, forceFetch);
93
+ } catch (e) {
94
+ console.log(
95
+ `Warning: Unable to find a supported version for '${plugin.location}' Continuing with the installed version`
96
+ );
97
+ }
98
+ }
23
99
  // load plugin
24
100
  const loader = new PluginInstaller(plugin);
25
101
  const res = await loader.install(force);
@@ -105,7 +181,7 @@ const loadAllPlugins = async (force) => {
105
181
  }
106
182
  }
107
183
  await getState().refreshUserLayouts();
108
- await getState().refresh(true);
184
+ await getState().refresh(true, true);
109
185
  if (!isRoot()) reloadAuthFromRoot();
110
186
  };
111
187
 
@@ -151,11 +227,14 @@ const loadAndSaveNewPlugin = async (
151
227
  return;
152
228
  }
153
229
  }
154
- const msgs = [];
155
- const loader = new PluginInstaller(plugin);
156
- const { version, plugin_module, location, loadedWithReload } =
230
+ if (plugin.source === "npm") await ensurePluginSupport(plugin);
231
+ const loadMsgs = [];
232
+ const loader = new PluginInstaller(plugin, {
233
+ scVersion: packagejson.version,
234
+ });
235
+ const { version, plugin_module, location, loadedWithReload, msgs } =
157
236
  await loader.install(force);
158
-
237
+ if (msgs) loadMsgs.push(...msgs);
159
238
  // install dependecies
160
239
  for (const loc of plugin_module.dependencies || []) {
161
240
  const existing = await Plugin.findOne({ location: loc });
@@ -201,7 +280,7 @@ const loadAndSaveNewPlugin = async (
201
280
  }
202
281
  }
203
282
  if (loadedWithReload || registeredWithReload) {
204
- msgs.push(
283
+ loadMsgs.push(
205
284
  __(
206
285
  "The plugin was corrupted and had to be repaired. We recommend restarting your server.",
207
286
  plugin.name
@@ -228,7 +307,7 @@ const loadAndSaveNewPlugin = async (
228
307
  force: false, // okay ??
229
308
  });
230
309
  }
231
- return msgs;
310
+ return loadMsgs;
232
311
  };
233
312
 
234
313
  module.exports = {
@@ -236,4 +315,6 @@ module.exports = {
236
315
  loadAllPlugins,
237
316
  loadPlugin,
238
317
  requirePlugin,
318
+ getEngineInfos,
319
+ ensurePluginSupport,
239
320
  };
package/locales/en.json CHANGED
@@ -1464,5 +1464,17 @@
1464
1464
  "Zip compression level": "Zip compression level",
1465
1465
  "1=Fast, larger file, 9=Slow, smaller files": "1=Fast, larger file, 9=Slow, smaller files",
1466
1466
  "Use system zip": "Use system zip",
1467
- "Recommended. Executable <code>zip</code> must be installed": "Recommended. Executable <code>zip</code> must be installed"
1467
+ "Recommended. Executable <code>zip</code> must be installed": "Recommended. Executable <code>zip</code> must be installed",
1468
+ "Time to run": "Time to run",
1469
+ "Mobile": "Mobile",
1470
+ "Plain password trigger row": "Plain password trigger row",
1471
+ "Send plaintext password changes to Users table triggers (Insert, Update and Validate).": "Send plaintext password changes to Users table triggers (Insert, Update and Validate).",
1472
+ "Minimum user role required to create a new tenant<div class=\"alert alert-danger fst-normal\" role=\"alert\" data-show-if=\"showIfFormulaInputs($('select[name=role_to_create_tenant]'), '+role_to_create_tenant>1')\">Giving non-trusted users access to create tenants is a security risk and not recommended.</div>": "Minimum user role required to create a new tenant<div class=\"alert alert-danger fst-normal\" role=\"alert\" data-show-if=\"showIfFormulaInputs($('select[name=role_to_create_tenant]'), '+role_to_create_tenant>1')\">Giving non-trusted users access to create tenants is a security risk and not recommended.</div>",
1473
+ "Select tag": "Select tag",
1474
+ "Invalid build directory path": "Invalid build directory path",
1475
+ "Invalid build directory name": "Invalid build directory name",
1476
+ "clean node_modules": "clean node_modules",
1477
+ "After delete": "After delete",
1478
+ "Search only on exact match, not substring match. Useful for large tables": "Search only on exact match, not substring match. Useful for large tables",
1479
+ "Please select an entry point.": "Please select an entry point."
1468
1480
  }