@saltcorn/server 0.8.3-beta.0 → 0.8.3-beta.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
@@ -42,6 +42,8 @@ const TotpStrategy = require("passport-totp").Strategy;
42
42
  const JwtStrategy = require("passport-jwt").Strategy;
43
43
  const ExtractJwt = require("passport-jwt").ExtractJwt;
44
44
  const cors = require("cors");
45
+ const api = require("./routes/api");
46
+ const scapi = require("./routes/scapi");
45
47
 
46
48
  const locales = Object.keys(available_languages);
47
49
  // i18n configuration
@@ -294,6 +296,9 @@ const getApp = async (opts = {}) => {
294
296
 
295
297
  app.use(wrapper(version_tag));
296
298
 
299
+ app.use("/api", api);
300
+ app.use("/scapi", scapi);
301
+
297
302
  const csurf = csrf();
298
303
  if (!opts.disableCsrf)
299
304
  app.use(function (req, res, next) {
package/auth/admin.js CHANGED
@@ -200,6 +200,12 @@ const user_dropdown = (user, req, can_reset) =>
200
200
  '<i class="fas fa-pause"></i>&nbsp;' + req.__("Disable"),
201
201
  req
202
202
  ),
203
+ !user.disabled &&
204
+ post_dropdown_item(
205
+ `/useradmin/force-logout/${user.id}`,
206
+ '<i class="fas fa-sign-out-alt"></i>&nbsp;' + req.__("Force logout"),
207
+ req
208
+ ),
203
209
  div({ class: "dropdown-divider" }),
204
210
  post_dropdown_item(
205
211
  `/useradmin/delete/${user.id}`,
@@ -335,7 +341,7 @@ const http_settings_form = async (req) =>
335
341
  "timeout",
336
342
  "cookie_duration",
337
343
  "cookie_duration_remember",
338
- "cookie_sessions",
344
+ //"cookie_sessions",
339
345
  "public_cache_maxage",
340
346
  "custom_http_headers",
341
347
  ],
@@ -972,7 +978,8 @@ router.post(
972
978
 
973
979
  req.flash("success", req.__(`User %s created`, email) + pwflash);
974
980
 
975
- if (rnd_password && send_pwreset_email) await send_reset_email(u, req);
981
+ if (rnd_password && send_pwreset_email)
982
+ await send_reset_email(u, req, { creating: true });
976
983
  }
977
984
  }
978
985
  res.redirect(`/useradmin`);
@@ -991,7 +998,7 @@ router.post(
991
998
  error_catcher(async (req, res) => {
992
999
  const { id } = req.params;
993
1000
  const u = await User.findOne({ id });
994
- await send_reset_email(u, req);
1001
+ await send_reset_email(u, req, { from_admin: true });
995
1002
  req.flash("success", req.__(`Reset password link sent to %s`, u.email));
996
1003
 
997
1004
  res.redirect(`/useradmin`);
@@ -1136,6 +1143,23 @@ router.post(
1136
1143
  })
1137
1144
  );
1138
1145
 
1146
+ /**
1147
+ * @name post/force-logout/:id
1148
+ * @function
1149
+ * @memberof module:auth/admin~auth/adminRouter
1150
+ */
1151
+ router.post(
1152
+ "/force-logout/:id",
1153
+ isAdmin,
1154
+ error_catcher(async (req, res) => {
1155
+ const { id } = req.params;
1156
+ const u = await User.findOne({ id });
1157
+ await u.destroy_sessions();
1158
+ req.flash("success", req.__(`Logged out user %s`, u.email));
1159
+ res.redirect(`/useradmin`);
1160
+ })
1161
+ );
1162
+
1139
1163
  /**
1140
1164
  * @name post/enable/:id
1141
1165
  * @function
package/auth/resetpw.js CHANGED
@@ -13,49 +13,88 @@ const { get_base_url } = require("../routes/utils");
13
13
  * @param {object} req
14
14
  * @returns {void}
15
15
  */
16
- const generate_email = (link, user, req) => ({
17
- from: getState().getConfig("email_from"),
18
- to: user.email,
19
- subject: req.__("Reset password instructions"),
20
- text: `${req.__("Hi %s", user.email)},
16
+ const generate_email = (link, user, req, options) => {
17
+ const subject = options?.creating
18
+ ? req.__(`Welcome to %s`, getState().getConfig("site_name", "Saltcorn"))
19
+ : req.__("Reset password instructions");
20
+ const initial = options?.creating
21
+ ? req.__(
22
+ "We have created an account for you on %s. You can set your new password through this link: ",
23
+ getState().getConfig("site_name", "Saltcorn")
24
+ )
25
+ : options?.from_admin
26
+ ? req.__(
27
+ "We request that you change your password on %s. You can set your new password through this link: ",
28
+ getState().getConfig("site_name", "Saltcorn")
29
+ )
30
+ : req.__(
31
+ "You have requested a link to change your password. You can do this through this link:"
32
+ );
33
+ const base_url = getState().getConfig("base_url", "");
34
+ const final =
35
+ options?.creating && base_url
36
+ ? req.__(
37
+ "Use this link to access the application once you have set your password: %s",
38
+ `<a href="${base_url}">${base_url}</a>`
39
+ )
40
+ : "";
41
+ const finalTxt =
42
+ options?.creating && base_url
43
+ ? req.__(
44
+ "Use this link to access the application once you have set your password: %s",
45
+ base_url
46
+ )
47
+ : "";
48
+ return {
49
+ from: getState().getConfig("email_from"),
50
+ to: user.email,
51
+ subject,
52
+ text: `${req.__("Hi %s", user.email)},
21
53
 
22
- ${req.__(
23
- "You have requested a link to change your password. You can do this through this link:"
24
- )}
54
+ ${initial}
25
55
 
26
56
  ${link}
27
57
 
28
- ${req.__("If you did not request this, please ignore this email.")}
29
-
58
+ ${
59
+ !options?.creating && !options?.from_admin
60
+ ? req.__("If you did not request this, please ignore this email.") + "\n"
61
+ : ""
62
+ }
30
63
  ${req.__(
31
64
  "Your password will not change until you access the link above and set a new one."
32
65
  )}
66
+
67
+ ${finalTxt}
33
68
  `,
34
- html: `${req.__("Hi %s", user.email)},<br />
35
-
36
- ${req.__(
37
- "You have requested a link to change your password. You can do this through this link:"
38
- )}<br />
69
+ html: `${req.__("Hi %s", user.email)},<br /><br />
70
+ ${initial}<br />
39
71
  <br />
40
72
  <a href="${link}">${req.__("Change my password")}</a><br />
41
73
  <br />
42
- ${req.__("If you did not request this, please ignore this email.")}<br />
74
+ ${
75
+ !options?.creating && !options?.from_admin
76
+ ? req.__("If you did not request this, please ignore this email.") +
77
+ "<br />"
78
+ : ""
79
+ }
43
80
  <br />
44
81
  ${req.__(
45
82
  "Your password will not change until you access the link above and set a new one."
46
83
  )}<br />
84
+ ${final ? `<br />${final}<br />` : ""}
47
85
  `,
48
- });
86
+ };
87
+ };
49
88
 
50
89
  /**
51
90
  * @param {object} user
52
91
  * @param {object} req
53
92
  * @returns {Promise<void>}
54
93
  */
55
- const send_reset_email = async (user, req) => {
94
+ const send_reset_email = async (user, req, options = {}) => {
56
95
  const link = await get_reset_link(user, req);
57
96
  const transporter = getMailTransport();
58
- await transporter.sendMail(generate_email(link, user, req));
97
+ await transporter.sendMail(generate_email(link, user, req, options));
59
98
  };
60
99
 
61
100
  /**
package/auth/routes.js CHANGED
@@ -112,6 +112,9 @@ const loginForm = (req, isCreating) => {
112
112
  label: req.__("Password"),
113
113
  name: "password",
114
114
  input_type: "password",
115
+ attributes: {
116
+ autocomplete: "current-password",
117
+ },
115
118
  validator: isCreating
116
119
  ? (pw) => User.unacceptable_password_reason(pw)
117
120
  : undefined,
@@ -1203,11 +1206,17 @@ const changPwForm = (req) =>
1203
1206
  label: req.__("Old password"),
1204
1207
  name: "password",
1205
1208
  input_type: "password",
1209
+ attributes: {
1210
+ autocomplete: "current-password",
1211
+ },
1206
1212
  },
1207
1213
  {
1208
1214
  label: req.__("New password"),
1209
1215
  name: "new_password",
1210
1216
  input_type: "password",
1217
+ attributes: {
1218
+ autocomplete: "new-password",
1219
+ },
1211
1220
  validator: (pw) => User.unacceptable_password_reason(pw),
1212
1221
  },
1213
1222
  ],
@@ -1353,14 +1362,14 @@ const userSettings = async ({ req, res, pwform, user }) => {
1353
1362
  href: "/auth/twofa/disable/totp",
1354
1363
  class: "btn btn-danger mt-2",
1355
1364
  },
1356
- req.__("Disable TWA")
1365
+ req.__("Disable 2FA")
1357
1366
  )
1358
1367
  : a(
1359
1368
  {
1360
1369
  href: "/auth/twofa/setup/totp",
1361
1370
  class: "btn btn-primary mt-2",
1362
1371
  },
1363
- req.__("Enable TWA")
1372
+ req.__("Enable 2FA")
1364
1373
  )
1365
1374
  ),
1366
1375
  ],
@@ -1791,6 +1800,12 @@ const totpForm = (req, action) =>
1791
1800
  name: "totpCode",
1792
1801
  label: req.__("Code"),
1793
1802
  type: "Integer",
1803
+ attributes: {
1804
+ type: "text",
1805
+ inputmode: "numeric",
1806
+ pattern: "[0-9]*",
1807
+ autocomplete: "one-time-code",
1808
+ },
1794
1809
  required: true,
1795
1810
  },
1796
1811
  ],
@@ -1828,6 +1843,12 @@ router.get(
1828
1843
  name: "code",
1829
1844
  label: req.__("Code"),
1830
1845
  type: "Integer",
1846
+ attributes: {
1847
+ type: "text",
1848
+ inputmode: "numeric",
1849
+ pattern: "[0-9]*",
1850
+ autocomplete: "one-time-code",
1851
+ },
1831
1852
  required: true,
1832
1853
  },
1833
1854
  ],
package/auth/testhelp.js CHANGED
@@ -163,6 +163,18 @@ const itShouldRedirectUnauthToLogin = (path, dest) => {
163
163
  });
164
164
  };
165
165
 
166
+ const itShouldIncludeTextForAdmin = (path, text) => {
167
+ it(`should show admin ${text} on ${path}`, async () => {
168
+ const app = await getApp({ disableCsrf: true });
169
+ const loginCookie = await getAdminLoginCookie();
170
+ await request(app)
171
+ .get(path)
172
+ .set("Cookie", loginCookie)
173
+ .expect(200)
174
+ .expect(toInclude(text));
175
+ });
176
+ };
177
+
166
178
  /**
167
179
  * @returns {Promise<void>}
168
180
  */
@@ -234,4 +246,5 @@ module.exports = {
234
246
  respondJsonWith,
235
247
  toSucceedWithImage,
236
248
  resToLoginCookie,
249
+ itShouldIncludeTextForAdmin,
237
250
  };
package/locales/da.json CHANGED
@@ -566,7 +566,7 @@
566
566
  "Generate": "Generate",
567
567
  "Two-factor authentication": "Two-factor authentication",
568
568
  "Two-factor authentication is disabled": "Two-factor authentication is disabled",
569
- "Enable TWA": "Enable TWA",
569
+ "Enable 2FA": "Enable 2FA",
570
570
  "Pattern": "Pattern",
571
571
  "You have views with a role to access lower than the table role to read, with no table ownership. This may cause a denial of access. Users need to have table read access to any data displayed.": "You have views with a role to access lower than the table role to read, with no table ownership. This may cause a denial of access. Users need to have table read access to any data displayed.",
572
572
  "Views potentially affected": "Views potentially affected",
package/locales/de.json CHANGED
@@ -892,9 +892,9 @@
892
892
  "Strings": "Strings",
893
893
  "In default language": "In Standardsprache",
894
894
  "In %s": "In %s",
895
- "Enable TWA": "TWA aktivieren",
895
+ "Enable 2FA": "2FA aktivieren",
896
896
  "Or enter this code:": "Oder gebe diesen Code ein:",
897
- "Disable TWA": "TWA deaktivieren",
897
+ "Disable TWA": "2FA deaktivieren",
898
898
  "Cascade delete to file": "Kaskadierendes Löschen der Datei",
899
899
  "Deleting a row will also delete the file referenced by this field": "Löschen einer Zeile löscht auch die in diesem Feld referenzierte Datei",
900
900
  "Has channels?": "Has channels?",
package/locales/en.json CHANGED
@@ -892,9 +892,9 @@
892
892
  "Strings": "Strings",
893
893
  "In default language": "In default language",
894
894
  "In %s": "In %s",
895
- "Enable TWA": "Enable TWA",
895
+ "Enable 2FA": "Enable 2FA",
896
896
  "Or enter this code:": "Or enter this code:",
897
- "Disable TWA": "Disable TWA",
897
+ "Disable 2FA": "Disable 2FA",
898
898
  "Cascade delete to file": "Cascade delete to file",
899
899
  "Deleting a row will also delete the file referenced by this field": "Deleting a row will also delete the file referenced by this field",
900
900
  "Has channels?": "Has channels?",
@@ -1093,5 +1093,16 @@
1093
1093
  "Min role to access search page": "Min role to access search page",
1094
1094
  "Update": "Update",
1095
1095
  "Add": "Add",
1096
- "Recalculate dynamic": "Recalculate dynamic"
1096
+ "Recalculate dynamic": "Recalculate dynamic",
1097
+ "Force logout": "Force logout",
1098
+ "Logged out user %s": "Logged out user %s",
1099
+ "Reset password link sent to %s": "Reset password link sent to %s",
1100
+ "We request that you change your password on %s. You can set your new password through this link: ": "We request that you change your password on %s. You can set your new password through this link: ",
1101
+ "Welcome to %s": "Welcome to %s",
1102
+ "We have created an account for you on %s. You can set your new password through this link: ": "We have created an account for you on %s. You can set your new password through this link: ",
1103
+ "Use this link to access the application once you have set your password: %s": "Use this link to access the application once you have set your password: %s",
1104
+ "Tag: %s": "Tag: %s",
1105
+ "Tag entry": "Tag entry",
1106
+ "Clear": "Clear",
1107
+ "Restore a snapshot": "Restore a snapshot"
1097
1108
  }
package/locales/it.json CHANGED
@@ -478,12 +478,30 @@
478
478
  "Generate": "Generate",
479
479
  "Two-factor authentication": "Two-factor authentication",
480
480
  "Two-factor authentication is disabled": "Two-factor authentication is disabled",
481
- "Enable TWA": "Enable TWA",
481
+ "Enable 2FA": "Enable 2FA",
482
482
  "Modules": "Modules",
483
483
  "Login and Signup": "Login and Signup",
484
484
  "Table access": "Table access",
485
485
  "HTTP": "HTTP",
486
486
  "Rights": "Rights",
487
487
  "Permissions": "Permissions",
488
- "Become user": "Become user"
488
+ "Become user": "Become user",
489
+ "Force logout": "Force logout",
490
+ "Subdomain": "Subdomain",
491
+ "Creator email": "Creator email",
492
+ "Created": "Created",
493
+ "Information": "Information",
494
+ "Found %s tenants": "Found %s tenants",
495
+ "Create new tenant": "Create new tenant",
496
+ "Library": "Library",
497
+ "Languages": "Languages",
498
+ "Multitenancy": "Multitenancy",
499
+ "Tags": "Tags",
500
+ "Diagram": "Diagram",
501
+ "%s tenant statistics": "%s tenant statistics",
502
+ "First user E-mail": "First user E-mail",
503
+ "Table columns": "Table columns",
504
+ "Configuration items": "Configuration items",
505
+ "Crashlogs": "Crashlogs",
506
+ "Logged out user %s": "Logged out user %s"
489
507
  }
package/locales/ru.json CHANGED
@@ -780,8 +780,8 @@
780
780
  "Or enter this code:": "Или введите этот код:",
781
781
  "Two-factor authentication with Time-based One-Time Password enabled": "Двухфакторная аутентификаиця с использованием Time-based One-Time Password",
782
782
  "Two-factor authentication is enabled": "Двухфакторная аутентификация включена",
783
- "Disable TWA": "Отключить TWA",
784
- "Enable TWA": "Включить TWA",
783
+ "Disable 2FA": "Отключить TWA",
784
+ "Enable 2FA": "Включить TWA",
785
785
  "Deleted all rows": "Удалены все строки",
786
786
  "Use this link: <a href=\"%s\">%s</a> to revisit your application at any time.": "В дальнейшем используйте ссылку: <a href=\"%s\">%s</a> для входа в созданное приложение.",
787
787
  "To login to a previously created application, go to: ": "Чтобы авторизоваться в ранее созданном приложении, перейдите по ссылке: ",
package/package.json CHANGED
@@ -1,20 +1,21 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.8.3-beta.0",
3
+ "version": "0.8.3-beta.2",
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
- "@saltcorn/base-plugin": "0.8.3-beta.0",
10
- "@saltcorn/builder": "0.8.3-beta.0",
11
- "@saltcorn/data": "0.8.3-beta.0",
12
- "@saltcorn/admin-models": "0.8.3-beta.0",
13
- "@saltcorn/filemanager": "0.8.3-beta.0",
14
- "@saltcorn/markup": "0.8.3-beta.0",
15
- "@saltcorn/sbadmin2": "0.8.3-beta.0",
16
- "@socket.io/cluster-adapter": "^0.1.0",
9
+ "@saltcorn/base-plugin": "0.8.3-beta.2",
10
+ "@saltcorn/builder": "0.8.3-beta.2",
11
+ "@saltcorn/data": "0.8.3-beta.2",
12
+ "@saltcorn/admin-models": "0.8.3-beta.2",
13
+ "@saltcorn/filemanager": "0.8.3-beta.2",
14
+ "@saltcorn/markup": "0.8.3-beta.2",
15
+ "@saltcorn/sbadmin2": "0.8.3-beta.2",
16
+ "@socket.io/cluster-adapter": "^0.2.1",
17
17
  "@socket.io/sticky": "^1.0.1",
18
+ "adm-zip": "0.5.10",
18
19
  "aws-sdk": "^2.1037.0",
19
20
  "connect-flash": "^0.1.1",
20
21
  "connect-pg-simple": "^6.1.0",
@@ -32,7 +33,7 @@
32
33
  "greenlock": "^4.0.4",
33
34
  "greenlock-express": "^4.0.3",
34
35
  "helmet": "^3.23.3",
35
- "i18n": "^0.14.0",
36
+ "i18n": "^0.15.1",
36
37
  "jsonwebtoken": "^9.0.0",
37
38
  "live-plugin-manager": "^0.16.0",
38
39
  "moment": "^2.29.4",
@@ -198,9 +198,13 @@ function apply_showif() {
198
198
  const $def = e
199
199
  .closest(".form-namespace")
200
200
  .find("input[name=_columndef]");
201
- const def = JSON.parse($def.val());
202
- def[k] = v;
203
- $def.val(JSON.stringify(def));
201
+ try {
202
+ const def = JSON.parse($def.val());
203
+ def[k] = v;
204
+ $def.val(JSON.stringify(def));
205
+ } catch (e) {
206
+ console.error("Invalid json", e);
207
+ }
204
208
  });
205
209
  };
206
210
 
package/routes/admin.js CHANGED
@@ -69,6 +69,7 @@ const {
69
69
  restore,
70
70
  auto_backup_now,
71
71
  } = require("@saltcorn/admin-models/models/backup");
72
+ const { install_pack } = require("@saltcorn/admin-models/models/pack");
72
73
  const Snapshot = require("@saltcorn/admin-models/models/snapshot");
73
74
  const {
74
75
  runConfigurationCheck,
@@ -98,6 +99,7 @@ const { getConfigFile } = require("@saltcorn/data/db/connect");
98
99
  const os = require("os");
99
100
  const Page = require("@saltcorn/data/models/page");
100
101
  const { getSafeSaltcornCmd } = require("@saltcorn/data/utils");
102
+ const stream = require("stream");
101
103
 
102
104
  const router = new Router();
103
105
  module.exports = router;
@@ -426,6 +428,36 @@ router.get(
426
428
  a(
427
429
  { href: "/admin/snapshot-list" },
428
430
  req.__("List/download snapshots &raquo;")
431
+ ),
432
+ form(
433
+ {
434
+ method: "post",
435
+ action: "/admin/snapshot-restore-full",
436
+ encType: "multipart/form-data",
437
+ },
438
+ input({
439
+ type: "hidden",
440
+ name: "_csrf",
441
+ value: req.csrfToken(),
442
+ }),
443
+ label(
444
+ {
445
+ class: "btn-link",
446
+ for: "upload_to_snapshot",
447
+ style: { cursor: "pointer" },
448
+ },
449
+ i({ class: "fas fa-upload me-2 mt-2" }),
450
+ req.__("Restore a snapshot")
451
+ ),
452
+ input({
453
+ id: "upload_to_snapshot",
454
+ class: "d-none",
455
+ name: "file",
456
+ type: "file",
457
+ accept: ".json,application/json",
458
+ onchange:
459
+ "notifyAlert('Restoring snapshot...', true);this.form.submit();",
460
+ })
429
461
  )
430
462
  ),
431
463
  },
@@ -555,6 +587,15 @@ router.get(
555
587
  error_catcher(async (req, res) => {
556
588
  const { id } = req.params;
557
589
  const snap = await Snapshot.findOne({ id });
590
+ const readStream = new stream.PassThrough();
591
+ readStream.end(JSON.stringify(snap.pack));
592
+ res.type("application/json");
593
+ res.attachment(
594
+ `${getState().getConfig("site_name", db.getTenantSchema())}-snapshot-${
595
+ snap.id
596
+ }.json`
597
+ );
598
+ readStream.pipe(res);
558
599
  res.send(snap.pack);
559
600
  })
560
601
  );
@@ -606,6 +647,33 @@ router.post(
606
647
  res.redirect(/^[a-z]+$/g.test(type) ? `/${type}edit` : "/");
607
648
  })
608
649
  );
650
+
651
+ /**
652
+ * @name post/restore
653
+ * @function
654
+ * @memberof module:routes/admin~routes/adminRouter
655
+ */
656
+ router.post(
657
+ "/snapshot-restore-full",
658
+ setTenant, // TODO why is this needed?????
659
+ isAdmin,
660
+ error_catcher(async (req, res) => {
661
+ if (req.files?.file?.tempFilePath) {
662
+ try {
663
+ const pack = JSON.parse(fs.readFileSync(req.files?.file?.tempFilePath));
664
+ await install_pack(pack, undefined, (p) =>
665
+ load_plugins.loadAndSaveNewPlugin(p)
666
+ );
667
+ req.flash("success", req.__("Snapshot restored"));
668
+ } catch (e) {
669
+ console.error(e);
670
+ req.flash("error", e.message);
671
+ }
672
+ }
673
+ res.redirect(`/admin/backup`);
674
+ })
675
+ );
676
+
609
677
  router.get(
610
678
  "/auto-backup-download/:filename",
611
679
  isAdmin,
@@ -102,7 +102,6 @@ router.get(
102
102
  */
103
103
  router.post(
104
104
  "/",
105
- isAdmin,
106
105
  error_catcher(async (req, res) => {
107
106
  const err = {
108
107
  stack: req.body.stack,
package/routes/files.js CHANGED
@@ -25,7 +25,9 @@ const {
25
25
  } = require("../markup/admin");
26
26
  const fs = require("fs");
27
27
  const path = require("path");
28
-
28
+ const Zip = require("adm-zip");
29
+ const stream = require("stream");
30
+ const { extract } = require("@saltcorn/admin-models/models/backup");
29
31
  /**
30
32
  * @type {object}
31
33
  * @const
@@ -143,6 +145,36 @@ router.get(
143
145
  })
144
146
  );
145
147
 
148
+ router.post(
149
+ "/download-zip",
150
+ isAdmin,
151
+
152
+ error_catcher(async (req, res) => {
153
+ const role = req.user && req.user.id ? req.user.role_id : 10;
154
+ const user_id = req.user && req.user.id;
155
+ const files = req.body.files;
156
+ const location = req.body.location;
157
+ const zip = new Zip();
158
+
159
+ for (const fileNm of files) {
160
+ const file = await File.findOne(path.join(location, fileNm));
161
+ if (
162
+ file &&
163
+ (role <= file.min_role_read || (user_id && user_id === file.user_id))
164
+ ) {
165
+ zip.addLocalFile(file.location);
166
+ }
167
+ }
168
+ const readStream = new stream.PassThrough();
169
+ readStream.end(zip.toBuffer());
170
+ res.type("application/zip");
171
+ res.attachment(
172
+ `${getState().getConfig("site_name", db.getTenantSchema())}-files.zip`
173
+ );
174
+ readStream.pipe(res);
175
+ })
176
+ );
177
+
146
178
  /**
147
179
  * @name get/serve/:id
148
180
  * @function
@@ -299,6 +331,26 @@ router.post(
299
331
  })
300
332
  );
301
333
 
334
+ /**
335
+ * @name post/unzip/:id
336
+ * @function
337
+ * @memberof module:routes/files~filesRouter
338
+ * @function
339
+ */
340
+ router.post(
341
+ "/unzip/*",
342
+ isAdmin,
343
+ error_catcher(async (req, res) => {
344
+ const serve_path = req.params[0];
345
+ const filename = req.body.value;
346
+
347
+ const file = await File.findOne(serve_path);
348
+ const dir = path.dirname(file.location);
349
+ if (file) await extract(file.location, dir);
350
+ res.redirect(`/files?dir=${encodeURIComponent(file.current_folder)}`);
351
+ })
352
+ );
353
+
302
354
  router.post(
303
355
  "/new-folder",
304
356
  isAdmin,
package/routes/index.js CHANGED
@@ -58,7 +58,6 @@ const events = require("./events");
58
58
  const tenant = require("./tenant");
59
59
  const library = require("./library");
60
60
  const settings = require("./settings");
61
- const api = require("./api");
62
61
  const plugins = require("./plugins");
63
62
  const packs = require("./packs");
64
63
  const edit = require("./edit");
@@ -69,7 +68,6 @@ const del = require("./delete");
69
68
  const auth = require("../auth/routes");
70
69
  const useradmin = require("../auth/admin");
71
70
  const roleadmin = require("../auth/roleadmin");
72
- const scapi = require("./scapi");
73
71
  const tags = require("./tags");
74
72
  const tagentries = require("./tag_entries");
75
73
  const dataDiagram = require("./diagram");
@@ -102,13 +100,11 @@ module.exports =
102
100
  app.use("/search", search);
103
101
  app.use("/admin", admin);
104
102
  app.use("/tenant", tenant);
105
- app.use("/api", api);
106
103
  app.use("/viewedit", viewedit);
107
104
  app.use("/delete", del);
108
105
  app.use("/auth", auth);
109
106
  app.use("/useradmin", useradmin);
110
107
  app.use("/roleadmin", roleadmin);
111
- app.use("/scapi", scapi);
112
108
  app.use("/tag", tags);
113
109
  app.use("/tag-entries", tagentries);
114
110
  app.use("/diagram", dataDiagram);
package/routes/tables.js CHANGED
@@ -430,7 +430,7 @@ router.get(
430
430
  label: `<b>${t.name}</b>\n${t.fields
431
431
  .map((f) => `${f.name} : ${f.pretty_type}`)
432
432
  .join("\n")}`,
433
- title: t.description? t.description : t.name,
433
+ title: t.description ? t.description : t.name,
434
434
  })),
435
435
  edges,
436
436
  };
@@ -652,19 +652,21 @@ router.get(
652
652
  if (views.length > 0) {
653
653
  viewCardContents = mkTable(
654
654
  [
655
- { label: req.__("Name"), key: "name" },
656
- { label: req.__("Pattern"), key: "viewtemplate" },
657
655
  {
658
- label: req.__("Run"),
659
- key: (r) =>
660
- link(`/view/${encodeURIComponent(r.name)}`, req.__("Run")),
656
+ label: req.__("Name"),
657
+ key: (r) => link(`/view/${encodeURIComponent(r.name)}`, r.name),
661
658
  },
659
+ { label: req.__("Pattern"), key: "viewtemplate" },
662
660
  {
663
- label: req.__("Edit"),
661
+ label: req.__("Configure"),
664
662
  key: (r) =>
665
663
  link(
666
- `/viewedit/edit/${encodeURIComponent(r.name)}`,
667
- req.__("Edit")
664
+ `/viewedit/config/${encodeURIComponent(
665
+ r.name
666
+ )}?on_done_redirect=${encodeURIComponent(
667
+ `table/${table.name}`
668
+ )}`,
669
+ req.__("Configure")
668
670
  ),
669
671
  },
670
672
  {
@@ -692,7 +694,9 @@ router.get(
692
694
  viewCardContents +
693
695
  a(
694
696
  {
695
- href: `/viewedit/new?table=${encodeURIComponent(table.name)}`,
697
+ href: `/viewedit/new?table=${encodeURIComponent(
698
+ table.name
699
+ )}&on_done_redirect=${encodeURIComponent(`table/${table.name}`)}`,
696
700
  class: "btn btn-primary",
697
701
  },
698
702
  req.__("Create view")
package/routes/utils.js CHANGED
@@ -250,13 +250,13 @@ const getGitRevision = () => db.connectObj.git_commit;
250
250
  * @returns {session|cookieSession}
251
251
  */
252
252
  const getSessionStore = () => {
253
- if (getState().getConfig("cookie_sessions", false)) {
253
+ /*if (getState().getConfig("cookie_sessions", false)) {
254
254
  return cookieSession({
255
255
  keys: [db.connectObj.session_secret || is.str.generate()],
256
256
  maxAge: 30 * 24 * 60 * 60 * 1000,
257
257
  sameSite: "strict",
258
258
  });
259
- } else if (db.isSQLite) {
259
+ } else*/ if (db.isSQLite) {
260
260
  var SQLiteStore = require("connect-sqlite3")(session);
261
261
  return session({
262
262
  store: new SQLiteStore({ db: "sessions.sqlite" }),
@@ -5,6 +5,7 @@ const {
5
5
  getAdminLoginCookie,
6
6
  toRedirect,
7
7
  itShouldRedirectUnauthToLogin,
8
+ itShouldIncludeTextForAdmin,
8
9
  toInclude,
9
10
  toSucceed,
10
11
  //toNotInclude,
@@ -73,6 +74,7 @@ describe("admin page", () => {
73
74
  ["/admin/email", "Email settings"],
74
75
  ["/admin/system", "Restart server"],
75
76
  ["/admin/dev", "Development"],
77
+ ["/admin/build-mobile-app", "Build mobile app"],
76
78
  ]);
77
79
  adminPageContains([
78
80
  ["/useradmin", "Create user"],
@@ -530,9 +532,80 @@ describe("localizer", () => {
530
532
  .expect(toRedirect("/site-structure/localizer/edit/da"));
531
533
  });
532
534
  });
535
+ /**
536
+ * Diagram tests
537
+ */
538
+ describe("diagram", () => {
539
+ itShouldRedirectUnauthToLogin("/diagram");
540
+ itShouldIncludeTextForAdmin("/diagram", ">All entities<");
541
+
542
+ it("get data diagram", async () => {
543
+ const app = await getApp({ disableCsrf: true });
544
+ const loginCookie = await getAdminLoginCookie();
545
+ await request(app)
546
+ .get("/diagram/data")
547
+ .set("Cookie", loginCookie)
548
+ .expect(
549
+ respondJsonWith(
550
+ 200,
551
+ ({ elements, style }) => elements && style.length > 3
552
+ )
553
+ );
554
+ });
555
+ });
533
556
 
534
557
  /**
535
- * Pages tests
558
+ * Diagram tests
559
+ */
560
+ describe("tags", () => {
561
+ itShouldRedirectUnauthToLogin("/tag");
562
+ itShouldIncludeTextForAdmin("/tag", ">Create tag<");
563
+ itShouldIncludeTextForAdmin("/tag/new", ">Create<");
564
+ it("creates new tag", async () => {
565
+ const app = await getApp({ disableCsrf: true });
566
+ const loginCookie = await getAdminLoginCookie();
567
+ await request(app)
568
+ .post("/tag")
569
+ .set("Cookie", loginCookie)
570
+ .send("name=MyNewTestTag")
571
+ .expect(toRedirect("/tag/1?show_list=tables"));
572
+ });
573
+
574
+ itShouldIncludeTextForAdmin("/tag", "MyNewTestTag");
575
+ itShouldIncludeTextForAdmin("/tag/1", "MyNewTestTag");
576
+ itShouldIncludeTextForAdmin("/tag-entries/add/tables/1", "Add entries");
577
+ itShouldIncludeTextForAdmin("/tag-entries/add/pages/1", "Add entries");
578
+ itShouldIncludeTextForAdmin("/tag-entries/add/views/1", "Add entries");
579
+ itShouldIncludeTextForAdmin("/tag-entries/add/triggers/1", "Add entries");
580
+ it("adds view to tag ", async () => {
581
+ const app = await getApp({ disableCsrf: true });
582
+ const loginCookie = await getAdminLoginCookie();
583
+ await request(app)
584
+ .post("/tag-entries/add/views/1")
585
+ .set("Cookie", loginCookie)
586
+ .send("ids=1")
587
+ .expect(toRedirect("/tag/1?show_list=views"));
588
+ });
589
+ itShouldIncludeTextForAdmin("/diagram", "MyNewTestTag");
590
+ it("removes view from tag ", async () => {
591
+ const app = await getApp({ disableCsrf: true });
592
+ const loginCookie = await getAdminLoginCookie();
593
+ await request(app)
594
+ .post("/tag-entries/remove/views/1/1")
595
+ .set("Cookie", loginCookie)
596
+ .expect(toRedirect("/tag/1?show_list=views"));
597
+ });
598
+ it("deletes new tag", async () => {
599
+ const app = await getApp({ disableCsrf: true });
600
+ const loginCookie = await getAdminLoginCookie();
601
+ await request(app)
602
+ .post("/tag/delete/1")
603
+ .set("Cookie", loginCookie)
604
+ .expect(toRedirect("/tag"));
605
+ });
606
+ });
607
+ /**
608
+ * Clear all tests
536
609
  */
537
610
  describe("clear all page", () => {
538
611
  itShouldRedirectUnauthToLogin("/admin/clear-all");
@@ -63,9 +63,10 @@ describe("login process", () => {
63
63
  });
64
64
 
65
65
  describe("user settings", () => {
66
+ let loginCookie;
66
67
  it("should show user settings", async () => {
67
68
  const app = await getApp({ disableCsrf: true });
68
- const loginCookie = await getStaffLoginCookie();
69
+ loginCookie = await getStaffLoginCookie();
69
70
  await request(app)
70
71
  .get("/auth/settings")
71
72
  .set("Cookie", loginCookie)
@@ -74,7 +75,7 @@ describe("user settings", () => {
74
75
 
75
76
  it("should change password", async () => {
76
77
  const app = await getApp({ disableCsrf: true });
77
- const loginCookie = await getStaffLoginCookie();
78
+ //const loginCookie = await getStaffLoginCookie();
78
79
  await request(app)
79
80
  .post("/auth/settings")
80
81
  .set("Cookie", loginCookie)
@@ -96,13 +97,20 @@ describe("user settings", () => {
96
97
  .send("email=staff@foo.com")
97
98
  .send("password=foHRrr46obar")
98
99
  .expect(toRedirect("/"));
100
+ //change back
101
+ await request(app)
102
+ .post("/auth/settings")
103
+ .set("Cookie", loginCookie)
104
+ .send("password=foHRrr46obar")
105
+ .send("new_password=ghrarhr54hg")
106
+ .expect(toRedirect("/auth/settings"));
99
107
  });
100
108
  it("should change language", async () => {
101
109
  const app = await getApp({ disableCsrf: true });
102
- const loginCookie = await getAdminLoginCookie();
110
+ const adminLoginCookie = await getAdminLoginCookie();
103
111
  const res = await request(app)
104
112
  .post("/auth/setlanguage")
105
- .set("Cookie", loginCookie)
113
+ .set("Cookie", adminLoginCookie)
106
114
  .send("locale=it")
107
115
  .expect(toRedirect("/auth/settings"));
108
116
  const newCookie = resToLoginCookie(res);
@@ -260,6 +268,64 @@ describe("user admin", () => {
260
268
  const delusers = await User.find({ email: "staff2@foo.com" });
261
269
  expect(delusers.length).toBe(0);
262
270
  });
271
+ if (!db.isSQLite)
272
+ it("can be disabled", async () => {
273
+ const staffLoginCookie = await getStaffLoginCookie();
274
+ const staffUser = await User.findOne({ email: "staff@foo.com" });
275
+ const adminLoginCookie = await getAdminLoginCookie();
276
+ const app = await getApp({ disableCsrf: true });
277
+ await request(app)
278
+ .post("/auth/login/")
279
+ .send("email=staff@foo.com")
280
+ .send("password=ghrarhr54hg")
281
+ .expect(toRedirect("/"));
282
+ await request(app)
283
+ .post(`/useradmin/disable/${staffUser.id}`)
284
+ .set("Cookie", adminLoginCookie)
285
+ .expect(toRedirect("/useradmin"));
286
+ await request(app)
287
+ .get("/auth/settings")
288
+ .set("Cookie", staffLoginCookie)
289
+ .expect(toRedirect("/auth/login"));
290
+ await request(app)
291
+ .post("/auth/login/")
292
+ .send("email=staff@foo.com")
293
+ .send("password=ghrarhr54hg")
294
+ .expect(toRedirect("/auth/login"));
295
+ await request(app)
296
+ .post(`/useradmin/enable/${staffUser.id}`)
297
+ .set("Cookie", adminLoginCookie)
298
+ .expect(toRedirect("/useradmin"));
299
+ await request(app)
300
+ .post("/auth/login/")
301
+ .send("email=staff@foo.com")
302
+ .send("password=ghrarhr54hg")
303
+ .expect(toRedirect("/"));
304
+ });
305
+ if (!db.isSQLite)
306
+ it("can be force logged out", async () => {
307
+ const staffLoginCookie = await getStaffLoginCookie();
308
+ const staffUser = await User.findOne({ email: "staff@foo.com" });
309
+ const adminLoginCookie = await getAdminLoginCookie();
310
+ const app = await getApp({ disableCsrf: true });
311
+ await request(app)
312
+ .get("/auth/settings")
313
+ .set("Cookie", staffLoginCookie)
314
+ .expect(toInclude(">staff@foo.com<"));
315
+ await request(app)
316
+ .post(`/useradmin/force-logout/${staffUser.id}`)
317
+ .set("Cookie", adminLoginCookie)
318
+ .expect(toRedirect("/useradmin"));
319
+ await request(app)
320
+ .get("/auth/settings")
321
+ .set("Cookie", staffLoginCookie)
322
+ .expect(toRedirect("/auth/login"));
323
+ await request(app)
324
+ .post("/auth/login/")
325
+ .send("email=staff@foo.com")
326
+ .send("password=ghrarhr54hg")
327
+ .expect(toRedirect("/"));
328
+ });
263
329
  });
264
330
  describe("User fields", () => {
265
331
  it("should add fields", async () => {