@saltcorn/server 0.8.5-beta.3 → 0.8.5-beta.4

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/admin.js CHANGED
@@ -1321,12 +1321,16 @@ router.get(
1321
1321
  "/configuration-check",
1322
1322
  isAdmin,
1323
1323
  error_catcher(async (req, res) => {
1324
- const filename = `${moment().format("YYYYMMDDHHmm")}.html`;
1324
+ const start = new Date();
1325
+ const filename = `${moment(start).format("YYYYMMDDHHmm")}.html`;
1325
1326
  await File.new_folder("configuration_checks");
1326
1327
  const go = async () => {
1327
1328
  const { passes, errors, pass, warnings } = await runConfigurationCheck(
1328
1329
  req
1329
1330
  );
1331
+ const end = new Date();
1332
+ const secs = Math.round((end.getTime() - start.getTime()) / 1000);
1333
+
1330
1334
  const mkError = (err) =>
1331
1335
  div(
1332
1336
  { class: "alert alert-danger", role: "alert" },
@@ -1355,6 +1359,11 @@ router.get(
1355
1359
  h3("Passes"),
1356
1360
 
1357
1361
  pre(code(passes.join("\n")))
1362
+ ) +
1363
+ p(
1364
+ `Configuration check completed in ${
1365
+ secs > 60 ? `${Math.floor(secs / 60)}m ${secs % 60}s` : secs + "s"
1366
+ }`
1358
1367
  );
1359
1368
  await File.from_contents(
1360
1369
  filename,
@@ -1964,8 +1973,9 @@ router.post(
1964
1973
  * @returns {Promise<Form>} form
1965
1974
  */
1966
1975
  const dev_form = async (req) => {
1967
- const role_to_create_tenant = +getRootState().getConfig(
1968
- "role_to_create_tenant"
1976
+ const tenants_set_npm_modules = getRootState().getConfig(
1977
+ "tenants_set_npm_modules",
1978
+ false
1969
1979
  );
1970
1980
  const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
1971
1981
 
@@ -1976,9 +1986,7 @@ const dev_form = async (req) => {
1976
1986
  "log_sql",
1977
1987
  "log_client_errors",
1978
1988
  "log_level",
1979
- ...(isRoot || role_to_create_tenant < 10
1980
- ? ["npm_available_js_code"]
1981
- : []),
1989
+ ...(isRoot || tenants_set_npm_modules ? ["npm_available_js_code"] : []),
1982
1990
  ],
1983
1991
  action: "/admin/dev",
1984
1992
  });
package/routes/api.js CHANGED
@@ -31,6 +31,7 @@ const {
31
31
  readState,
32
32
  strictParseInt,
33
33
  } = require("@saltcorn/data/plugin-helper");
34
+ const Crash = require("@saltcorn/data/models/crash");
34
35
 
35
36
  /**
36
37
  * @type {object}
@@ -66,7 +67,7 @@ const limitFields = (fields) => (r) => {
66
67
  * @param {Table} table
67
68
  * @returns {boolean}
68
69
  */
69
- function accessAllowedRead(req, user, table) {
70
+ function accessAllowedRead(req, user, table, allow_ownership) {
70
71
  const role =
71
72
  req.user && req.user.id
72
73
  ? req.user.role_id
@@ -74,7 +75,12 @@ function accessAllowedRead(req, user, table) {
74
75
  ? user.role_id
75
76
  : 10;
76
77
 
77
- return role <= table.min_role_read;
78
+ return (
79
+ role <= table.min_role_read ||
80
+ ((req.user?.id || user?.id) &&
81
+ allow_ownership &&
82
+ (table.ownership_field_id || table.ownership_formula))
83
+ );
78
84
  }
79
85
 
80
86
  /**
@@ -92,7 +98,11 @@ function accessAllowedWrite(req, user, table) {
92
98
  ? user.role_id
93
99
  : 10;
94
100
 
95
- return role <= table.min_role_write;
101
+ return (
102
+ role <= table.min_role_write ||
103
+ ((req.user?.id || user?.id) &&
104
+ (table.ownership_field_id || table.ownership_formula))
105
+ );
96
106
  }
97
107
  /**
98
108
  * Check that user has right to trigger call
@@ -126,6 +136,7 @@ router.post(
126
136
  const view = await View.findOne({ name: viewName });
127
137
  const db = require("@saltcorn/data/db");
128
138
  if (!view) {
139
+ getState().log(3, `API viewQuery ${view.name} not found`);
129
140
  res.status(404).json({
130
141
  error: req.__("View %s not found", viewName),
131
142
  view: viewName,
@@ -152,6 +163,10 @@ router.post(
152
163
  const resp = await queries[queryName](...args, true);
153
164
  res.json({ success: resp, alerts: getFlashes(req) });
154
165
  } else {
166
+ getState().log(
167
+ 3,
168
+ `API viewQuery ${view.name} ${queryName} not found`
169
+ );
155
170
  res.status(404).json({
156
171
  error: req.__("Query %s not found", queryName),
157
172
  view: viewName,
@@ -163,6 +178,7 @@ router.post(
163
178
  });
164
179
  }
165
180
  } else {
181
+ getState().log(3, `API viewQuery ${view.name} not authorized`);
166
182
  res.status(401).json({ error: req.__("Not authorized") });
167
183
  }
168
184
  }
@@ -208,6 +224,10 @@ router.get(
208
224
  }
209
225
  res.json({ success: dvs });
210
226
  } else {
227
+ getState().log(
228
+ 3,
229
+ `API distinct ${table.name}.${fieldName} not authorized`
230
+ );
211
231
  res.status(401).json({ error: req.__("Not authorized") });
212
232
  }
213
233
  }
@@ -234,6 +254,7 @@ router.get(
234
254
  : { name: tableName }
235
255
  );
236
256
  if (!table) {
257
+ getState().log(3, `API get ${tableName} table not found`);
237
258
  res.status(404).json({ error: req.__("Not found") });
238
259
  return;
239
260
  }
@@ -242,11 +263,13 @@ router.get(
242
263
  ["api-bearer", "jwt"],
243
264
  { session: false },
244
265
  async function (err, user, info) {
245
- if (accessAllowedRead(req, user, table)) {
266
+ if (accessAllowedRead(req, user, table, true)) {
246
267
  let rows;
247
268
  if (versioncount === "on") {
248
269
  const joinOpts = {
249
270
  orderBy: "id",
271
+ forUser: req.user || user || { role_id: 10 },
272
+ forPublic: !(req.user || user),
250
273
  aggregations: {
251
274
  _versions: {
252
275
  table: table.name + "__history",
@@ -266,12 +289,22 @@ router.get(
266
289
  state: req_query,
267
290
  table,
268
291
  });
269
- rows = await table.getRows(qstate);
292
+ rows = await table.getRows(qstate, {
293
+ forPublic: !(req.user || user),
294
+ forUser: req.user || user,
295
+ });
270
296
  } else {
271
- rows = await table.getRows();
297
+ rows = await table.getRows(
298
+ {},
299
+ {
300
+ forPublic: !(req.user || user),
301
+ forUser: req.user || user,
302
+ }
303
+ );
272
304
  }
273
305
  res.json({ success: rows.map(limitFields(fields)) });
274
306
  } else {
307
+ getState().log(3, `API get ${table.name} not authorized`);
275
308
  res.status(401).json({ error: req.__("Not authorized") });
276
309
  }
277
310
  }
@@ -301,6 +334,7 @@ router.post(
301
334
  });
302
335
 
303
336
  if (!trigger) {
337
+ getState().log(3, `API action ${actionname} not found`);
304
338
  res.status(400).json({ error: req.__("Not found") });
305
339
  return;
306
340
  }
@@ -318,9 +352,11 @@ router.post(
318
352
  });
319
353
  res.json({ success: true, data: resp });
320
354
  } catch (e) {
355
+ Crash.create(e, req);
321
356
  res.status(400).json({ success: false, error: e.message });
322
357
  }
323
358
  } else {
359
+ getState().log(3, `API action ${actionname} not authorized`);
324
360
  res.status(401).json({ error: req.__("Not authorized") });
325
361
  }
326
362
  }
@@ -340,6 +376,7 @@ router.post(
340
376
  const { tableName } = req.params;
341
377
  const table = await Table.findOne({ name: tableName });
342
378
  if (!table) {
379
+ getState().log(3, `API POST ${tableName} not found`);
343
380
  res.status(404).json({ error: req.__("Not found") });
344
381
  return;
345
382
  }
@@ -378,6 +415,10 @@ router.post(
378
415
  }
379
416
  });
380
417
  if (hasErrors) {
418
+ getState().log(
419
+ 2,
420
+ `API POST ${table.name} error: ${errors.join(", ")}`
421
+ );
381
422
  res.status(400).json({ error: errors.join(", ") });
382
423
  return;
383
424
  }
@@ -385,9 +426,12 @@ router.post(
385
426
  row,
386
427
  req.user || user || { role_id: 10 }
387
428
  );
388
- if (ins_res.error) res.status(400).json(ins_res);
389
- else res.json(ins_res);
429
+ if (ins_res.error) {
430
+ getState().log(2, `API POST ${table.name} error: ${ins_res.error}`);
431
+ res.status(400).json(ins_res);
432
+ } else res.json(ins_res);
390
433
  } else {
434
+ getState().log(3, `API POST ${table.name} not authorized`);
391
435
  res.status(401).json({ error: req.__("Not authorized") });
392
436
  }
393
437
  }
@@ -408,6 +452,7 @@ router.post(
408
452
  const { tableName, id } = req.params;
409
453
  const table = await Table.findOne({ name: tableName });
410
454
  if (!table) {
455
+ getState().log(3, `API POST ${tableName} not found`);
411
456
  res.status(404).json({ error: req.__("Not found") });
412
457
  return;
413
458
  }
@@ -445,6 +490,10 @@ router.post(
445
490
  }
446
491
  }
447
492
  if (hasErrors) {
493
+ getState().log(
494
+ 2,
495
+ `API POST ${table.name} error: ${errors.join(", ")}`
496
+ );
448
497
  res.status(400).json({ error: errors.join(", ") });
449
498
  return;
450
499
  }
@@ -454,9 +503,12 @@ router.post(
454
503
  user || req.user || { role_id: 10 }
455
504
  );
456
505
 
457
- if (ins_res.error) res.status(400).json(ins_res);
458
- else res.json(ins_res);
506
+ if (ins_res.error) {
507
+ getState().log(2, `API POST ${table.name} error: ${ins_res.error}`);
508
+ res.status(400).json(ins_res);
509
+ } else res.json(ins_res);
459
510
  } else {
511
+ getState().log(3, `API POST ${table.name} not authorized`);
460
512
  res.status(401).json({ error: req.__("Not authorized") });
461
513
  }
462
514
  }
@@ -477,6 +529,7 @@ router.delete(
477
529
  const { tableName, id } = req.params;
478
530
  const table = await Table.findOne({ name: tableName });
479
531
  if (!table) {
532
+ getState().log(3, `API DELETE ${tableName} not found`);
480
533
  res.status(404).json({ error: req.__("Not found") });
481
534
  return;
482
535
  }
@@ -502,9 +555,11 @@ router.delete(
502
555
  );
503
556
  res.json({ success: true });
504
557
  } catch (e) {
558
+ getState().log(2, `API DELETE ${table.name} error: ${e.message}`);
505
559
  res.status(400).json({ error: e.message });
506
560
  }
507
561
  } else {
562
+ getState().log(3, `API DELETE ${table.name} not authorized`);
508
563
  res.status(401).json({ error: req.__("Not authorized") });
509
564
  }
510
565
  }
package/routes/fields.js CHANGED
@@ -398,9 +398,16 @@ const fieldFlow = (req) =>
398
398
  ];
399
399
  const keyfields = orderedFields
400
400
  .filter((f) => !f.calculated || f.stored)
401
+ .sort((a, b) =>
402
+ a.type?.name === "String" && b.type?.name !== "String"
403
+ ? -1
404
+ : a.type?.name !== "String" && b.type?.name === "String"
405
+ ? 1
406
+ : 0
407
+ )
401
408
  .map((f) => ({
402
409
  value: f.name,
403
- label: f.label,
410
+ label: `${f.label} [${f.type?.name || f.type}]`,
404
411
  }));
405
412
  const textfields = orderedFields
406
413
  .filter(
@@ -412,6 +419,9 @@ const fieldFlow = (req) =>
412
419
  new Field({
413
420
  name: "summary_field",
414
421
  label: req.__("Summary field"),
422
+ sublabel: req.__(
423
+ "The field that will be shown to the user when choosing a value"
424
+ ),
415
425
  input_type: "select",
416
426
  options: keyfields,
417
427
  }),
package/routes/list.js CHANGED
@@ -241,13 +241,16 @@ router.get(
241
241
  for (const f of fields) {
242
242
  if (f.type === "File") f.attributes = { select_file_where: {} };
243
243
  await f.fill_fkey_options();
244
+
245
+ if (f.type === "File") {
246
+ //add existing values in folders
247
+ const dvs = await f.distinct_values();
248
+ dvs.forEach((dv) => {
249
+ if (dv?.value?.includes("/")) f.options.push(dv);
250
+ });
251
+ }
244
252
  }
245
253
 
246
- //console.log(fields);
247
- // todo remove keyfields - unused
248
- const keyfields = fields
249
- .filter((f) => f.type === "Key" || f.type === "File")
250
- .map((f) => ({ name: f.name, type: f.reftype }));
251
254
  const jsfields = arrangeIdFirst(fields).map((f) =>
252
255
  typeToGridType(f.type, f)
253
256
  );
package/routes/plugins.js CHANGED
@@ -14,11 +14,18 @@ const {
14
14
  post_btn,
15
15
  post_delete_btn,
16
16
  } = require("@saltcorn/markup");
17
- const { getState, restart_tenant } = require("@saltcorn/data/db/state");
17
+ const {
18
+ getState,
19
+ restart_tenant,
20
+ getRootState,
21
+ } = require("@saltcorn/data/db/state");
18
22
  const Form = require("@saltcorn/data/models/form");
19
23
  const Field = require("@saltcorn/data/models/field");
20
24
  const Plugin = require("@saltcorn/data/models/plugin");
21
25
  const { fetch_available_packs } = require("@saltcorn/admin-models/models/pack");
26
+ const {
27
+ upgrade_all_tenants_plugins,
28
+ } = require("@saltcorn/admin-models/models/tenant");
22
29
  const { getConfig, setConfig } = require("@saltcorn/data/models/config");
23
30
  const db = require("@saltcorn/data/db");
24
31
  const {
@@ -150,7 +157,10 @@ const local_has_theme = (name) => {
150
157
  const get_store_items = async () => {
151
158
  const installed_plugins = await Plugin.find({});
152
159
  const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
153
-
160
+ const tenants_unsafe_plugins = getRootState().getConfig(
161
+ "tenants_unsafe_plugins",
162
+ false
163
+ );
154
164
  const instore = await Plugin.store_plugins_available();
155
165
  const packs_available = await fetch_available_packs();
156
166
  const packs_installed = getState().getConfig("installed_packs", []);
@@ -168,7 +178,7 @@ const get_store_items = async () => {
168
178
  has_auth: plugin.has_auth,
169
179
  unsafe: plugin.unsafe,
170
180
  }))
171
- .filter((p) => !p.unsafe || isRoot);
181
+ .filter((p) => !p.unsafe || isRoot || tenants_unsafe_plugins);
172
182
  const local_logins = installed_plugins
173
183
  .filter((p) => !store_plugin_names.includes(p.name) && p.name !== "base")
174
184
  .map((plugin) => ({
@@ -424,8 +434,12 @@ const filter_items_set = (items, query) => {
424
434
  * @param {object} req
425
435
  * @returns {div}
426
436
  */
427
- const store_actions_dropdown = (req) =>
428
- div(
437
+ const store_actions_dropdown = (req) => {
438
+ const tenants_install_git = getRootState().getConfig(
439
+ "tenants_install_git",
440
+ false
441
+ );
442
+ return div(
429
443
  { class: "dropdown" },
430
444
  button(
431
445
  {
@@ -460,7 +474,8 @@ const store_actions_dropdown = (req) =>
460
474
  '<i class="far fa-arrow-alt-circle-up"></i>&nbsp;' +
461
475
  req.__("Upgrade installed modules")
462
476
  ),
463
- db.getTenantSchema() === db.connectObj.default_schema &&
477
+ (db.getTenantSchema() === db.connectObj.default_schema ||
478
+ tenants_install_git) &&
464
479
  a(
465
480
  {
466
481
  class: "dropdown-item",
@@ -488,6 +503,7 @@ const store_actions_dropdown = (req) =>
488
503
  //create pack
489
504
  )
490
505
  );
506
+ };
491
507
 
492
508
  /**
493
509
  * @param {object[]} items
@@ -926,14 +942,22 @@ router.get(
926
942
  "/upgrade",
927
943
  isAdmin,
928
944
  error_catcher(async (req, res) => {
929
- const installed_plugins = await Plugin.find({});
930
- for (const plugin of installed_plugins) {
931
- await plugin.upgrade_version((p, f) => load_plugins.loadPlugin(p, f));
945
+ const schema = db.getTenantSchema();
946
+ if (schema === db.connectObj.default_schema) {
947
+ await upgrade_all_tenants_plugins((p, f) =>
948
+ load_plugins.loadPlugin(p, f)
949
+ );
950
+ req.flash("success", req.__(`Modules up-to-date. Please restart server`));
951
+ } else {
952
+ const installed_plugins = await Plugin.find({});
953
+ for (const plugin of installed_plugins) {
954
+ await plugin.upgrade_version((p, f) => load_plugins.loadPlugin(p, f));
955
+ }
956
+ req.flash("success", req.__(`Modules up-to-date`));
957
+ await restart_tenant(loadAllPlugins);
958
+ process.send &&
959
+ process.send({ restart_tenant: true, tenant: db.getTenantSchema() });
932
960
  }
933
- req.flash("success", req.__(`Modules up-to-date`));
934
- await restart_tenant(loadAllPlugins);
935
- process.send &&
936
- process.send({ restart_tenant: true, tenant: db.getTenantSchema() });
937
961
  res.redirect(`/plugins`);
938
962
  })
939
963
  );
@@ -970,7 +994,11 @@ router.post(
970
994
  error_catcher(async (req, res) => {
971
995
  const plugin = new Plugin(req.body);
972
996
  const schema = db.getTenantSchema();
973
- if (schema !== db.connectObj.default_schema) {
997
+ const tenants_install_git = getRootState().getConfig(
998
+ "tenants_install_git",
999
+ false
1000
+ );
1001
+ if (schema !== db.connectObj.default_schema && !tenants_install_git) {
974
1002
  req.flash(
975
1003
  "error",
976
1004
  req.__(`Only store modules can be installed on tenant instances`)
@@ -1039,7 +1067,10 @@ router.post(
1039
1067
  isAdmin,
1040
1068
  error_catcher(async (req, res) => {
1041
1069
  const { name } = req.params;
1042
-
1070
+ const tenants_unsafe_plugins = getRootState().getConfig(
1071
+ "tenants_unsafe_plugins",
1072
+ false
1073
+ );
1043
1074
  const plugin = await Plugin.store_by_name(decodeURIComponent(name));
1044
1075
  if (!plugin) {
1045
1076
  req.flash(
@@ -1050,7 +1081,7 @@ router.post(
1050
1081
  return;
1051
1082
  }
1052
1083
  const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
1053
- if (!isRoot && plugin.unsafe) {
1084
+ if (!isRoot && plugin.unsafe && !tenants_unsafe_plugins) {
1054
1085
  req.flash(
1055
1086
  "error",
1056
1087
  req.__("Cannot install unsafe modules on subdomain tenants")
package/routes/tables.js CHANGED
@@ -439,8 +439,7 @@ router.get(
439
439
  title: req.__("Tables"),
440
440
  headers: [
441
441
  {
442
- script:
443
- "https://unpkg.com/vis-network@9.1.2/standalone/umd/vis-network.min.js",
442
+ script: `/static_assets/${db.connectObj.version_tag}/vis-network.min.js`,
444
443
  },
445
444
  ],
446
445
  },
package/routes/tenant.js CHANGED
@@ -426,6 +426,10 @@ const tenant_settings_form = (req) =>
426
426
  "create_tenant_warning",
427
427
  "create_tenant_warning_text",
428
428
  "tenant_template",
429
+ { section_header: "Tenant application capabilities" },
430
+ "tenants_install_git",
431
+ "tenants_set_npm_modules",
432
+ "tenants_unsafe_plugins",
429
433
  ],
430
434
  action: "/tenant/settings",
431
435
  submitLabel: req.__("Save"),
package/tests/api.test.js CHANGED
@@ -19,7 +19,7 @@ beforeAll(async () => {
19
19
  afterAll(db.close);
20
20
 
21
21
  describe("API read", () => {
22
- it("should get books for public", async () => {
22
+ it("should get books for public simple", async () => {
23
23
  const app = await getApp({ disableCsrf: true });
24
24
  await request(app)
25
25
  .get("/api/books/")
@@ -32,7 +32,7 @@ describe("API read", () => {
32
32
  )
33
33
  );
34
34
  });
35
- it("should get books for public", async () => {
35
+ it("should get books for public fts", async () => {
36
36
  const app = await getApp({ disableCsrf: true });
37
37
  await request(app)
38
38
  .get("/api/books/?_fts=Herman")
@@ -5,11 +5,13 @@ const Field = require("@saltcorn/data/models/field");
5
5
  const {
6
6
  getStaffLoginCookie,
7
7
  getAdminLoginCookie,
8
+ getUserLoginCookie,
8
9
  itShouldRedirectUnauthToLogin,
9
10
  toInclude,
10
11
  toNotInclude,
11
12
  toRedirect,
12
13
  resetToFixtures,
14
+ succeedJsonWith,
13
15
  } = require("../auth/testhelp");
14
16
  const db = require("@saltcorn/data/db");
15
17
  const User = require("@saltcorn/data/models/user");
@@ -262,7 +264,22 @@ describe("deletion to table with row ownership", () => {
262
264
  const row = await persons.insertRow({ name: "something", owner: user.id });
263
265
  expect(await persons.countRows()).toBe(1);
264
266
  const loginCookie = await getStaffLoginCookie();
267
+ const uloginCookie = await getUserLoginCookie();
265
268
  const app = await getApp({ disableCsrf: true });
269
+ await request(app).get("/api/owned").expect(401);
270
+ await request(app)
271
+ .get("/api/owned")
272
+ .set("Cookie", loginCookie)
273
+ .expect(
274
+ succeedJsonWith(
275
+ (rows) => rows.length == 1 && rows[0].name === "something"
276
+ )
277
+ );
278
+ await request(app)
279
+ .get("/api/owned")
280
+ .set("Cookie", uloginCookie)
281
+ .expect(succeedJsonWith((rows) => rows.length == 0));
282
+
266
283
  await request(app)
267
284
  .post("/delete/owned/" + row)
268
285
  .expect(toRedirect("/list/owned"));
package/wrapper.js CHANGED
@@ -58,10 +58,22 @@ const get_menu = (req) => {
58
58
  ]
59
59
  : [
60
60
  ...(allow_signup
61
- ? [{ link: "/auth/signup", label: req.__("Sign up") }]
61
+ ? [
62
+ {
63
+ link: "/auth/signup",
64
+ icon: "fas fa-user-plus",
65
+ label: req.__("Sign up"),
66
+ },
67
+ ]
62
68
  : []),
63
69
  ...(login_menu
64
- ? [{ link: "/auth/login", label: req.__("Login") }]
70
+ ? [
71
+ {
72
+ link: "/auth/login",
73
+ icon: "fas fa-sign-in-alt",
74
+ label: req.__("Login"),
75
+ },
76
+ ]
65
77
  : []),
66
78
  ];
67
79
  // const schema = db.getTenantSchema();