@saltcorn/server 0.7.1-beta.3 → 0.7.2-beta.10

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
@@ -23,9 +23,6 @@ const {
23
23
  div,
24
24
  a,
25
25
  hr,
26
- form,
27
- input,
28
- label,
29
26
  i,
30
27
  h4,
31
28
  table,
@@ -33,7 +30,6 @@ const {
33
30
  td,
34
31
  th,
35
32
  tr,
36
- button,
37
33
  span,
38
34
  p,
39
35
  code,
@@ -45,13 +41,14 @@ const {
45
41
  getState,
46
42
  restart_tenant,
47
43
  getTenant,
48
- get_other_domain_tenant,
44
+ //get_other_domain_tenant,
49
45
  get_process_init_time,
50
46
  } = require("@saltcorn/data/db/state");
51
47
  const { loadAllPlugins } = require("../load_plugins");
52
48
  const {
53
49
  create_backup,
54
50
  restore,
51
+ auto_backup_now,
55
52
  } = require("@saltcorn/admin-models/models/backup");
56
53
  const {
57
54
  runConfigurationCheck,
@@ -61,7 +58,7 @@ const load_plugins = require("../load_plugins");
61
58
  const {
62
59
  restore_backup,
63
60
  send_admin_page,
64
- send_files_page,
61
+ //send_files_page,
65
62
  config_fields_form,
66
63
  save_config_from_form,
67
64
  flash_restart_if_required,
@@ -90,8 +87,9 @@ const router = new Router();
90
87
  module.exports = router;
91
88
 
92
89
  /**
93
- * @param {object} req
94
- * @returns {Promise<Form>}
90
+ * Site identity form
91
+ * @param {object} req -http request
92
+ * @returns {Promise<Form>} form
95
93
  */
96
94
  const site_id_form = (req) =>
97
95
  config_fields_form({
@@ -106,13 +104,15 @@ const site_id_form = (req) =>
106
104
  "page_custom_html",
107
105
  "development_mode",
108
106
  "log_sql",
107
+ "plugins_store_endpoint",
108
+ "packs_store_endpoint",
109
109
  ...(getConfigFile() ? ["multitenancy_enabled"] : []),
110
110
  ],
111
111
  action: "/admin",
112
112
  submitLabel: req.__("Save"),
113
113
  });
114
114
  /**
115
- * Email settings form definition
115
+ * Email settings form
116
116
  * @param {object} req request
117
117
  * @returns {Promise<Form>} form
118
118
  */
@@ -137,6 +137,7 @@ const email_form = async (req) => {
137
137
  };
138
138
 
139
139
  /**
140
+ * Router get /
140
141
  * @name get
141
142
  * @function
142
143
  * @memberof module:routes/admin~routes/adminRouter
@@ -145,7 +146,6 @@ router.get(
145
146
  "/",
146
147
  isAdmin,
147
148
  error_catcher(async (req, res) => {
148
- const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
149
149
  const form = await site_id_form(req);
150
150
  send_admin_page({
151
151
  res,
@@ -294,41 +294,148 @@ router.get(
294
294
  "/backup",
295
295
  isAdmin,
296
296
  error_catcher(async (req, res) => {
297
+ const backupForm = autoBackupForm(req);
298
+ backupForm.values.auto_backup_frequency = getState().getConfig(
299
+ "auto_backup_frequency"
300
+ );
301
+ backupForm.values.auto_backup_destination = getState().getConfig(
302
+ "auto_backup_destination"
303
+ );
304
+ backupForm.values.auto_backup_directory = getState().getConfig(
305
+ "auto_backup_directory"
306
+ );
307
+ const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
308
+
297
309
  send_admin_page({
298
310
  res,
299
311
  req,
300
312
  active_sub: "Backup",
301
313
  contents: {
302
- type: "card",
303
- title: req.__("Backup"),
304
- contents: table(
305
- tbody(
306
- tr(
307
- td(
314
+ above: [
315
+ {
316
+ type: "card",
317
+ title: req.__("Manual backup"),
318
+ contents: {
319
+ besides: [
308
320
  div(
309
- post_btn("/admin/backup", req.__("Backup"), req.csrfToken())
310
- )
311
- ),
312
- td(p({ class: "ms-4 pt-2" }, req.__("Download a backup")))
313
- ),
314
- tr(td(div({ class: "my-4" }))),
315
- tr(
316
- td(
317
- restore_backup(req.csrfToken(), [
318
- i({ class: "fas fa-2x fa-upload" }),
319
- "<br/>",
320
- req.__("Restore"),
321
- ])
322
- ),
323
- td(p({ class: "ms-4" }, req.__("Restore a backup")))
324
- )
325
- )
326
- ),
321
+ post_btn(
322
+ "/admin/backup",
323
+ i({ class: "fas fa-download me-2" }) +
324
+ req.__("Download a backup"),
325
+ req.csrfToken(),
326
+ {
327
+ btnClass: "btn-outline-primary",
328
+ }
329
+ )
330
+ ),
331
+ div(
332
+ restore_backup(req.csrfToken(), [
333
+ i({ class: "fas fa-2x fa-upload me-2" }),
334
+ "",
335
+ req.__("Restore a backup"),
336
+ ])
337
+ ),
338
+ ],
339
+ },
340
+ },
341
+ isRoot
342
+ ? {
343
+ type: "card",
344
+ title: req.__("Automated backup"),
345
+ contents: div(renderForm(backupForm, req.csrfToken())),
346
+ }
347
+ : { type: "blank", contents: "" },
348
+ ],
327
349
  },
328
350
  });
329
351
  })
330
352
  );
331
353
 
354
+ /**
355
+ * Auto backup Form
356
+ * @param {object} req
357
+ * @returns {Form} form
358
+ */
359
+ const autoBackupForm = (req) =>
360
+ new Form({
361
+ action: "/admin/set-auto-backup",
362
+ submitButtonClass: "btn-outline-primary",
363
+ onChange: "remove_outline(this)",
364
+ submitLabel: "Save settings",
365
+ additionalButtons: [
366
+ {
367
+ label: "Backup now",
368
+ id: "btnBackupNow",
369
+ class: "btn btn-outline-secondary",
370
+ onclick: "ajax_post('/admin/auto-backup-now')",
371
+ },
372
+ ],
373
+ fields: [
374
+ {
375
+ type: "String",
376
+ label: req.__("Frequency"),
377
+ name: "auto_backup_frequency",
378
+ required: true,
379
+ attributes: { options: ["Never", "Daily", "Weekly"] },
380
+ },
381
+ {
382
+ type: "String",
383
+ label: req.__("Destination"),
384
+ name: "auto_backup_destination",
385
+ required: true,
386
+ showIf: { auto_backup_frequency: ["Daily", "Weekly"] },
387
+ attributes: { options: ["Saltcorn files", "Local directory"] },
388
+ },
389
+ {
390
+ type: "String",
391
+ label: req.__("Directory"),
392
+ name: "auto_backup_directory",
393
+ showIf: {
394
+ auto_backup_frequency: ["Daily", "Weekly"],
395
+ auto_backup_destination: "Local directory",
396
+ },
397
+ },
398
+ ],
399
+ });
400
+
401
+ router.post(
402
+ "/set-auto-backup",
403
+ isAdmin,
404
+ error_catcher(async (req, res) => {
405
+ const form = await autoBackupForm(req);
406
+ form.validate(req.body);
407
+ if (form.hasErrors) {
408
+ send_admin_page({
409
+ res,
410
+ req,
411
+ active_sub: "Backup",
412
+ contents: {
413
+ type: "card",
414
+ title: req.__("Backup settings"),
415
+ contents: [renderForm(form, req.csrfToken())],
416
+ },
417
+ });
418
+ } else {
419
+ await save_config_from_form(form);
420
+ req.flash("success", req.__("Backup settings updated"));
421
+ res.redirect("/admin/backup");
422
+ }
423
+ })
424
+ );
425
+ router.post(
426
+ "/auto-backup-now",
427
+ isAdmin,
428
+ error_catcher(async (req, res) => {
429
+ try {
430
+ await auto_backup_now();
431
+ req.flash("success", req.__("Backup successful"));
432
+ } catch (e) {
433
+ req.flash("error", e.message);
434
+ }
435
+ res.json({ reload_page: true });
436
+ })
437
+ );
438
+
332
439
  /**
333
440
  * @name get/system
334
441
  * @function
@@ -527,6 +634,9 @@ router.post(
527
634
  }
528
635
  })
529
636
  );
637
+ /**
638
+ * /check-for-updates
639
+ */
530
640
  router.post(
531
641
  "/check-for-upgrade",
532
642
  isAdmin,
@@ -548,7 +658,7 @@ router.post(
548
658
  const fileName = await create_backup();
549
659
  res.type("application/zip");
550
660
  res.attachment(fileName);
551
- var file = fs.createReadStream(fileName);
661
+ const file = fs.createReadStream(fileName);
552
662
  file.on("end", function () {
553
663
  fs.unlink(fileName, function () {});
554
664
  });
@@ -579,8 +689,9 @@ router.post(
579
689
  );
580
690
 
581
691
  /**
692
+ * Clear All Form
582
693
  * @param {object} req
583
- * @returns {Form}
694
+ * @returns {Form} form
584
695
  */
585
696
  const clearAllForm = (req) =>
586
697
  new Form({
@@ -694,6 +805,7 @@ router.post(
694
805
  try {
695
806
  const file_store = db.connectObj.file_store;
696
807
  const admin_users = await User.find({ role_id: 1 }, { orderBy: "id" });
808
+ // greenlock logic
697
809
  const Greenlock = require("greenlock");
698
810
  const greenlock = Greenlock.create({
699
811
  packageRoot: path.resolve(__dirname, ".."),
@@ -709,6 +821,7 @@ router.post(
709
821
  subject: domain,
710
822
  altnames,
711
823
  });
824
+ // letsencrypt
712
825
  await getState().setConfig("letsencrypt", true);
713
826
  req.flash(
714
827
  "success",
@@ -758,7 +871,9 @@ router.get(
758
871
  });
759
872
  })
760
873
  );
761
-
874
+ /**
875
+ * /confiuration-check
876
+ */
762
877
  router.get(
763
878
  "/configuration-check",
764
879
  isAdmin,
@@ -787,7 +902,10 @@ router.get(
787
902
  ? div(
788
903
  { class: "alert alert-success", role: "alert" },
789
904
  i({ class: "fas fa-check-circle fa-lg me-2" }),
790
- h5({ class: "d-inline" }, "No errors detected")
905
+ h5(
906
+ { class: "d-inline" },
907
+ req.__("No errors detected during configuration check")
908
+ )
791
909
  )
792
910
  : errors.map(mkError)
793
911
  ),
@@ -863,6 +981,7 @@ router.post(
863
981
  if (form.values.plugins) {
864
982
  const ps = await Plugin.find();
865
983
  for (const p of ps) {
984
+ // todo configurable list of mandatory plugins
866
985
  if (!["base", "sbadmin2"].includes(p.name)) await p.delete();
867
986
  }
868
987
  await getState().refresh_plugins();
@@ -902,6 +1021,8 @@ router.post(
902
1021
  req.logout();
903
1022
  req.session = null;
904
1023
  }
1024
+ // todo make configurable - redirect to create first user
1025
+ // redirect to create first user
905
1026
  res.redirect(`/auth/create_first_user`);
906
1027
  } else {
907
1028
  req.flash(
package/routes/api.js CHANGED
@@ -20,6 +20,7 @@ const { error_catcher } = require("./utils.js");
20
20
  //const { mkTable, renderForm, link, post_btn } = require("@saltcorn/markup");
21
21
  const { getState } = require("@saltcorn/data/db/state");
22
22
  const Table = require("@saltcorn/data/models/table");
23
+ const View = require("@saltcorn/data/models/view");
23
24
  //const Field = require("@saltcorn/data/models/field");
24
25
  const Trigger = require("@saltcorn/data/models/trigger");
25
26
  //const load_plugins = require("../load_plugins");
@@ -111,6 +112,40 @@ function accessAllowed(req, user, trigger) {
111
112
  return role <= trigger.min_role;
112
113
  }
113
114
 
115
+ router.post(
116
+ "/viewQuery/:viewName/:queryName",
117
+ error_catcher(async (req, res, next) => {
118
+ let { viewName, queryName } = req.params;
119
+ const view = await View.findOne({ name: viewName });
120
+ if (!view) {
121
+ res.status(404).json({ error: req.__("Not found") });
122
+ return;
123
+ }
124
+ await passport.authenticate(
125
+ "jwt",
126
+ { session: false },
127
+ async function (err, user, info) {
128
+ const role = user && user.id ? user.role_id : 10;
129
+ if (
130
+ role <= view.min_role ||
131
+ (await view.authorise_get({ req, ...view })) // TODO set query to state
132
+ ) {
133
+ const queries = view.queries(false, req);
134
+ if (queries[queryName]) {
135
+ const { args } = req.body;
136
+ const resp = await queries[queryName](...args, true);
137
+ res.json({ success: resp });
138
+ } else {
139
+ res.status(404).json({ error: req.__("Not found") });
140
+ }
141
+ } else {
142
+ res.status(401).json({ error: req.__("Not authorized") });
143
+ }
144
+ }
145
+ )(req, res, next);
146
+ })
147
+ );
148
+
114
149
  router.get(
115
150
  "/:tableName/distinct/:fieldName",
116
151
  //passport.authenticate("api-bearer", { session: false }),
@@ -180,7 +215,7 @@ router.get(
180
215
  }
181
216
 
182
217
  await passport.authenticate(
183
- "api-bearer",
218
+ ["api-bearer", "jwt"],
184
219
  { session: false },
185
220
  async function (err, user, info) {
186
221
  if (accessAllowedRead(req, user, table)) {
package/routes/edit.js CHANGED
@@ -44,7 +44,8 @@ router.post(
44
44
  "error",
45
45
  req.__("Not allowed to write to table %s", table.name)
46
46
  );
47
- if (req.get("referer")) res.redirect(req.get("referer"));
47
+ if (req.xhr) res.send("OK");
48
+ else if (req.get("referer")) res.redirect(req.get("referer"));
48
49
  else res.redirect(redirect || `/list/${table.name}`);
49
50
  })
50
51
  );
@@ -5,7 +5,7 @@
5
5
  * @subcategory routes
6
6
  */
7
7
  const Router = require("express-promise-router");
8
- const { isAdmin, error_catcher, get_base_url } = require("./utils.js");
8
+ const { isAdmin, error_catcher } = require("./utils.js");
9
9
  const { getState } = require("@saltcorn/data/db/state");
10
10
  const Trigger = require("@saltcorn/data/models/trigger");
11
11
 
@@ -21,19 +21,19 @@ module.exports = router;
21
21
  const {
22
22
  mkTable,
23
23
  renderForm,
24
- link,
25
- post_btn,
26
- settingsDropdown,
27
- post_dropdown_item,
24
+ //link,
25
+ //post_btn,
26
+ //settingsDropdown,
27
+ //post_dropdown_item,
28
28
  post_delete_btn,
29
29
  localeDateTime,
30
30
  } = require("@saltcorn/markup");
31
31
  const Form = require("@saltcorn/data/models/form");
32
32
  const {
33
33
  div,
34
- code,
34
+ //code,
35
35
  a,
36
- span,
36
+ //span,
37
37
  tr,
38
38
  table,
39
39
  tbody,
@@ -74,7 +74,7 @@ const logSettingsForm = async (req) => {
74
74
  name: w + "_channel",
75
75
  label: w + " channel",
76
76
  sublabel:
77
- "Channels to create events for. Separate by comma; leave blank for all",
77
+ req.__("Channels to create events for. Separate by comma; leave blank for all"),
78
78
  type: "String",
79
79
  showIf: { [w]: true },
80
80
  });
@@ -168,25 +168,25 @@ router.get(
168
168
  /**
169
169
  * @returns {Form}
170
170
  */
171
- const customEventForm = () =>
172
- new Form({
173
- action: "/eventlog/custom/new",
174
- submitButtonClass: "btn-outline-primary",
175
- onChange: "remove_outline(this)",
176
- fields: [
177
- {
178
- name: "name",
179
- label: "Name",
180
- type: "String",
181
- },
182
- {
183
- name: "hasChannel",
184
- label: "Has channels?",
185
- type: "Bool",
186
- },
187
- ],
188
- });
189
-
171
+ const customEventForm = async (req) => {
172
+ return new Form({
173
+ action: "/eventlog/custom/new",
174
+ submitButtonClass: "btn-outline-primary",
175
+ onChange: "remove_outline(this)",
176
+ fields: [
177
+ {
178
+ name: "name",
179
+ label: req.__("Event Name"),
180
+ type: "String",
181
+ },
182
+ {
183
+ name: "hasChannel",
184
+ label: req.__("Has channels?"),
185
+ type: "Bool",
186
+ },
187
+ ],
188
+ });
189
+ };
190
190
  /**
191
191
  * @name get/custom/new
192
192
  * @function
@@ -197,7 +197,7 @@ router.get(
197
197
  "/custom/new",
198
198
  isAdmin,
199
199
  error_catcher(async (req, res) => {
200
- const form = customEventForm();
200
+ const form = await customEventForm(req);
201
201
  send_events_page({
202
202
  res,
203
203
  req,
@@ -222,7 +222,7 @@ router.post(
222
222
  "/custom/new",
223
223
  isAdmin,
224
224
  error_catcher(async (req, res) => {
225
- const form = customEventForm();
225
+ const form = await customEventForm(req);
226
226
  form.validate(req.body);
227
227
  if (form.hasErrors) {
228
228
  send_events_page({
@@ -323,7 +323,7 @@ router.get(
323
323
  { orderBy: "occur_at", orderDesc: true, limit: rows_per_page, offset }
324
324
  );
325
325
  if (evlog.length === rows_per_page || current_page > 1) {
326
- const nrows = await EventLog.count();
326
+ const nrows = await EventLog.count({});
327
327
  if (nrows > rows_per_page || current_page > 1) {
328
328
  page_opts.pagination = {
329
329
  current_page,
package/routes/fields.js CHANGED
@@ -181,6 +181,7 @@ const fieldFlow = (req) =>
181
181
  var attributes = context.attributes || {};
182
182
  attributes.default = context.default;
183
183
  attributes.summary_field = context.summary_field;
184
+ attributes.include_fts = context.include_fts;
184
185
  attributes.on_delete_cascade = context.on_delete_cascade;
185
186
  const {
186
187
  table_id,
@@ -295,6 +296,12 @@ const fieldFlow = (req) =>
295
296
  input_type: "select",
296
297
  options: roles.map((r) => ({ value: r.id, label: r.role })),
297
298
  },
299
+ {
300
+ name: "also_delete_file",
301
+ type: "Bool",
302
+ label: req.__("Cascade delete to file"),
303
+ sublabel: req.__("Deleting a row will also delete the file referenced by this field")
304
+ },
298
305
  ],
299
306
  });
300
307
  } else {
@@ -370,6 +377,11 @@ const fieldFlow = (req) =>
370
377
  value: f.name,
371
378
  label: f.label,
372
379
  }));
380
+ const textfields = orderedFields
381
+ .filter(
382
+ (f) => (!f.calculated || f.stored) && f.type?.sql_name === "text"
383
+ )
384
+ .map((f) => f.name);
373
385
  return new Form({
374
386
  fields: [
375
387
  new Field({
@@ -378,6 +390,12 @@ const fieldFlow = (req) =>
378
390
  input_type: "select",
379
391
  options: keyfields,
380
392
  }),
393
+ new Field({
394
+ name: "include_fts",
395
+ label: req.__("Include in full-text search"),
396
+ type: "Bool",
397
+ showIf: { summary_field: textfields },
398
+ }),
381
399
  new Field({
382
400
  name: "on_delete_cascade",
383
401
  label: req.__("On delete cascade"),
package/routes/files.js CHANGED
@@ -1,4 +1,5 @@
1
1
  /**
2
+ * Files Route
2
3
  * @category server
3
4
  * @module routes/files
4
5
  * @subcategory routes
@@ -9,33 +10,18 @@ const File = require("@saltcorn/data/models/file");
9
10
  const User = require("@saltcorn/data/models/user");
10
11
  const { getState } = require("@saltcorn/data/db/state");
11
12
  const s3storage = require("../s3storage");
13
+ const sharp = require("sharp");
12
14
 
13
15
  const {
14
16
  mkTable,
15
17
  renderForm,
16
18
  link,
17
- post_btn,
19
+ //post_btn,
18
20
  post_delete_btn,
19
21
  } = require("@saltcorn/markup");
20
22
  const { isAdmin, error_catcher, setTenant } = require("./utils.js");
21
- const {
22
- span,
23
- h5,
24
- h1,
25
- h4,
26
- nbsp,
27
- p,
28
- a,
29
- div,
30
- form,
31
- input,
32
- select,
33
- button,
34
- option,
35
- text,
36
- label,
37
- } = require("@saltcorn/markup/tags");
38
- const { csrfField } = require("./utils");
23
+ const { h1, div, text } = require("@saltcorn/markup/tags");
24
+ // const { csrfField } = require("./utils");
39
25
  const { editRoleForm, fileUploadForm } = require("../markup/forms.js");
40
26
  const { strictParseInt } = require("@saltcorn/data/plugin-helper");
41
27
  const {
@@ -43,6 +29,8 @@ const {
43
29
  config_fields_form,
44
30
  save_config_from_form,
45
31
  } = require("../markup/admin");
32
+ // const fsp = require("fs").promises;
33
+ const fs = require("fs");
46
34
 
47
35
  /**
48
36
  * @type {object}
@@ -55,6 +43,7 @@ const router = new Router();
55
43
  module.exports = router;
56
44
 
57
45
  /**
46
+ * Edit file Role form
58
47
  * @param {*} file
59
48
  * @param {*} roles
60
49
  * @param {*} req
@@ -78,6 +67,7 @@ router.get(
78
67
  "/",
79
68
  isAdmin,
80
69
  error_catcher(async (req, res) => {
70
+ // todo limit select from file by 10 or 20
81
71
  const rows = await File.find({}, { orderBy: "filename" });
82
72
  const roles = await User.get_roles();
83
73
  send_files_page({
@@ -187,6 +177,54 @@ router.get(
187
177
  })
188
178
  );
189
179
 
180
+ /**
181
+ * @name get/resize/:id
182
+ * @function
183
+ * @memberof module:routes/files~filesRouter
184
+ * @function
185
+ */
186
+ router.get(
187
+ "/resize/:id/:width_str",
188
+ error_catcher(async (req, res) => {
189
+ const role = req.user && req.user.id ? req.user.role_id : 10;
190
+ const user_id = req.user && req.user.id;
191
+ const { id, width_str } = req.params;
192
+ let file;
193
+ if (typeof strictParseInt(id) !== "undefined")
194
+ file = await File.findOne({ id });
195
+ else file = await File.findOne({ filename: id });
196
+
197
+ if (!file) {
198
+ res
199
+ .status(404)
200
+ .sendWrap(req.__("Not found"), h1(req.__("File not found")));
201
+ return;
202
+ }
203
+ if (role <= file.min_role_read || (user_id && user_id === file.user_id)) {
204
+ res.type(file.mimetype);
205
+ const cacheability = file.min_role_read === 10 ? "public" : "private";
206
+ res.set("Cache-Control", `${cacheability}, max-age=86400`);
207
+ //TODO s3
208
+ if (file.s3_store) s3storage.serveObject(file, res, false);
209
+ else {
210
+ const width = strictParseInt(width_str);
211
+ if (!width) {
212
+ res.sendFile(file.location);
213
+ return;
214
+ }
215
+ const fnm = `${file.location}_w${width}`;
216
+ if (!fs.existsSync(fnm)) {
217
+ await sharp(file.location).resize({ width }).toFile(fnm);
218
+ }
219
+ res.sendFile(fnm);
220
+ }
221
+ } else {
222
+ req.flash("warning", req.__("Not authorized"));
223
+ res.redirect("/");
224
+ }
225
+ })
226
+ );
227
+
190
228
  /**
191
229
  * @name post/setrole/:id
192
230
  * @function