@saltcorn/server 0.7.2-beta.0 → 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/app.js CHANGED
@@ -30,7 +30,6 @@ const {
30
30
  } = require("./routes/utils.js");
31
31
  const { getAllTenants } = require("@saltcorn/admin-models/models/tenant");
32
32
  const path = require("path");
33
- const fileUpload = require("express-fileupload");
34
33
  const helmet = require("helmet");
35
34
  const wrapper = require("./wrapper");
36
35
  const csrf = require("csurf");
@@ -40,13 +39,35 @@ const is = require("contractis/is");
40
39
  const Trigger = require("@saltcorn/data/models/trigger");
41
40
  const s3storage = require("./s3storage");
42
41
  const TotpStrategy = require("passport-totp").Strategy;
42
+ const JwtStrategy = require("passport-jwt").Strategy;
43
+ const ExtractJwt = require("passport-jwt").ExtractJwt;
44
+ const cors = require("cors");
45
+
43
46
  const locales = Object.keys(available_languages);
44
47
  // i18n configuration
45
48
  const i18n = new I18n({
46
49
  locales,
47
50
  directory: path.join(__dirname, "locales"),
48
51
  });
49
- // todo console.log app instance info when app starts - avoid to show secrets (password, etc)
52
+ // jwt config
53
+ const jwt_secret = db.connectObj.jwt_secret;
54
+ const jwt_extractor = ExtractJwt.fromExtractors([
55
+ ExtractJwt.fromAuthHeaderWithScheme("jwt"),
56
+ ExtractJwt.fromUrlQueryParameter("jwt"),
57
+ ]);
58
+ const jwtOpts = {
59
+ jwtFromRequest: jwt_extractor,
60
+ secretOrKey: jwt_secret,
61
+ issuer: "saltcorn@saltcorn",
62
+ audience: "saltcorn-mobile-app",
63
+ };
64
+
65
+ const disabledCsurf = (req, res, next) => {
66
+ req.csrfToken = () => "";
67
+ next();
68
+ };
69
+
70
+ // todo console.log app instance info when app stxarts - avoid to show secrets (password, etc)
50
71
 
51
72
  /**
52
73
  * @param {object} [opts = {}]
@@ -68,6 +89,8 @@ const getApp = async (opts = {}) => {
68
89
  // https://www.npmjs.com/package/helmet
69
90
  // helmet is secure app by adding HTTP headers
70
91
  app.use(helmet());
92
+ // TODO ch find a better solution
93
+ app.use(cors());
71
94
  app.use(
72
95
  express.json({
73
96
  limit: "5mb",
@@ -93,7 +116,14 @@ const getApp = async (opts = {}) => {
93
116
  app.use(getSessionStore());
94
117
 
95
118
  app.use(passport.initialize());
96
- app.use(passport.session());
119
+ app.use(passport.authenticate(["jwt", "session"]));
120
+ app.use((req, res, next) => {
121
+ if (jwt_extractor(req) && req.cookies && req.cookies["connect.sid"])
122
+ throw new Error(
123
+ "Don't set a session cookie and JSON Web Token at the same time."
124
+ );
125
+ next();
126
+ });
97
127
  app.use(flash());
98
128
 
99
129
  //static serving
@@ -192,6 +222,23 @@ const getApp = async (opts = {}) => {
192
222
  }
193
223
  })
194
224
  );
225
+ passport.use(
226
+ new JwtStrategy(jwtOpts, (jwt_payload, done) => {
227
+ User.findOne({ email: jwt_payload.sub }).then((u) => {
228
+ if (u) {
229
+ return done(null, {
230
+ email: u.email,
231
+ id: u.id,
232
+ role_id: u.role_id,
233
+ language: u.language,
234
+ tenant: db.getTenantSchema(),
235
+ });
236
+ } else {
237
+ return done(null, { role_id: 10 });
238
+ }
239
+ });
240
+ })
241
+ );
195
242
  passport.use(
196
243
  new TotpStrategy(function (user, done) {
197
244
  // setup function, supply key and period to done callback
@@ -217,14 +264,22 @@ const getApp = async (opts = {}) => {
217
264
  const csurf = csrf();
218
265
  if (!opts.disableCsrf)
219
266
  app.use(function (req, res, next) {
220
- if (req.url.startsWith("/api/")) return next();
267
+ if (
268
+ req.url.startsWith("/api/") ||
269
+ req.url === "/auth/login-with/jwt" ||
270
+ jwt_extractor(req)
271
+ )
272
+ return disabledCsurf(req, res, next);
221
273
  csurf(req, res, next);
222
274
  });
223
- else
224
- app.use((req, res, next) => {
225
- req.csrfToken = () => "";
226
- next();
227
- });
275
+ else app.use(disabledCsurf);
276
+
277
+ app.use(function (req, res, next) {
278
+ if (req.headers["x-saltcorn-client"] === "mobile-app") {
279
+ req.smr = true; // saltcorn-mobile-request
280
+ }
281
+ return next();
282
+ });
228
283
 
229
284
  mountRoutes(app);
230
285
  // set tenant homepage as / root
package/auth/routes.js CHANGED
@@ -61,6 +61,8 @@ const fs = require("fs");
61
61
  const base32 = require("thirty-two");
62
62
  const qrcode = require("qrcode");
63
63
  const totp = require("notp").totp;
64
+ const jwt = require("jsonwebtoken");
65
+
64
66
  /**
65
67
  * @type {object}
66
68
  * @const
@@ -197,6 +199,24 @@ const getAuthLinks = (current, noMethods) => {
197
199
  return links;
198
200
  };
199
201
 
202
+ const loginWithJwt = async (req, res) => {
203
+ const { email, password } = req.query;
204
+ const user = await User.findOne({ email });
205
+ if (user && user.checkPassword(password)) {
206
+ const jwt_secret = db.connectObj.jwt_secret;
207
+ const token = jwt.sign(
208
+ {
209
+ sub: email,
210
+ role_id: user.role_id,
211
+ iss: "saltcorn@saltcorn",
212
+ aud: "saltcorn-mobile-app",
213
+ },
214
+ jwt_secret
215
+ );
216
+ res.json(token);
217
+ }
218
+ };
219
+
200
220
  /**
201
221
  * @name get/login
202
222
  * @function
@@ -973,15 +993,19 @@ router.get(
973
993
  "/login-with/:method",
974
994
  error_catcher(async (req, res, next) => {
975
995
  const { method } = req.params;
976
- const auth = getState().auth_methods[method];
977
- if (auth) {
978
- passport.authenticate(method, auth.parameters)(req, res, next);
996
+ if (method === "jwt") {
997
+ await loginWithJwt(req, res);
979
998
  } else {
980
- req.flash(
981
- "danger",
982
- req.__("Unknown authentication method %s", text(method))
983
- );
984
- res.redirect("/");
999
+ const auth = getState().auth_methods[method];
1000
+ if (auth) {
1001
+ passport.authenticate(method, auth.parameters)(req, res, next);
1002
+ } else {
1003
+ req.flash(
1004
+ "danger",
1005
+ req.__("Unknown authentication method %s", text(method))
1006
+ );
1007
+ res.redirect("/");
1008
+ }
985
1009
  }
986
1010
  })
987
1011
  );
@@ -1014,7 +1038,7 @@ router.post(
1014
1038
  );
1015
1039
 
1016
1040
  /**
1017
- * @param {object}} req
1041
+ * @param {object} req
1018
1042
  * @param {object} res
1019
1043
  * @returns {void}
1020
1044
  */
@@ -1212,14 +1236,14 @@ const userSettings = async ({ req, res, pwform, user }) => {
1212
1236
  href: "/auth/twofa/disable/totp",
1213
1237
  class: "btn btn-danger mt-2",
1214
1238
  },
1215
- "Disable"
1239
+ req.__("Disable TWA")
1216
1240
  )
1217
1241
  : a(
1218
1242
  {
1219
1243
  href: "/auth/twofa/setup/totp",
1220
1244
  class: "btn btn-primary mt-2",
1221
1245
  },
1222
- "Enable"
1246
+ req.__("Enable TWA")
1223
1247
  )
1224
1248
  ),
1225
1249
  ],
@@ -1541,7 +1565,7 @@ router.get(
1541
1565
  contents: [
1542
1566
  h4(req.__("1. Scan this QR code in your Authenticator app")),
1543
1567
  img({ src: image }),
1544
- p("Or enter this code:"),
1568
+ p(req.__("Or enter this code:")),
1545
1569
  code(pre(encodedKey.toString())),
1546
1570
  h4(
1547
1571
  req.__(
@@ -1669,7 +1693,7 @@ const randomKey = function (len) {
1669
1693
  chars = "abcdefghijklmnopqrstuvwxyz0123456789",
1670
1694
  charlen = chars.length;
1671
1695
 
1672
- for (var i = 0; i < len; ++i) {
1696
+ for (let i = 0; i < len; ++i) {
1673
1697
  buf.push(chars[getRandomInt(0, charlen - 1)]);
1674
1698
  }
1675
1699
 
package/load_plugins.js CHANGED
@@ -13,35 +13,39 @@ const fs = require("fs");
13
13
  const proc = require("child_process");
14
14
  const tmp = require("tmp-promise");
15
15
 
16
+ const staticDependencies = {
17
+ "@saltcorn/markup": require("@saltcorn/markup"),
18
+ "@saltcorn/markup/tags": require("@saltcorn/markup/tags"),
19
+ "@saltcorn/markup/layout": require("@saltcorn/markup/layout"),
20
+ "@saltcorn/markup/helpers": require("@saltcorn/markup/helpers"),
21
+ "@saltcorn/markup/layout_utils": require("@saltcorn/markup/layout_utils"),
22
+ "@saltcorn/data": require("@saltcorn/data"),
23
+ "@saltcorn/data/db": require("@saltcorn/data/db"),
24
+ "@saltcorn/data/utils": require("@saltcorn/data/utils"),
25
+ "@saltcorn/data/db/state": require("@saltcorn/data/db/state"),
26
+ "@saltcorn/data/plugin-helper": require("@saltcorn/data/plugin-helper"),
27
+ "@saltcorn/data/plugin-testing": require("@saltcorn/data/plugin-testing"),
28
+ "@saltcorn/data/models/field": require("@saltcorn/data/models/field"),
29
+ "@saltcorn/data/models/fieldrepeat": require("@saltcorn/data/models/fieldrepeat"),
30
+ "@saltcorn/data/models/table": require("@saltcorn/data/models/table"),
31
+ "@saltcorn/data/models/form": require("@saltcorn/data/models/form"),
32
+ "@saltcorn/data/models/view": require("@saltcorn/data/models/view"),
33
+ "@saltcorn/data/models/page": require("@saltcorn/data/models/page"),
34
+ "@saltcorn/data/models/file": require("@saltcorn/data/models/file"),
35
+ "@saltcorn/data/models/user": require("@saltcorn/data/models/user"),
36
+ "@saltcorn/data/models/layout": require("@saltcorn/data/models/layout"),
37
+ "@saltcorn/data/models/expression": require("@saltcorn/data/models/expression"),
38
+ "@saltcorn/data/models/workflow": require("@saltcorn/data/models/workflow"),
39
+ };
40
+
16
41
  /**
17
42
  * Create plugin manager with default list of core plugins
18
43
  * @type {PluginManager}
19
44
  */
20
- const manager = new PluginManager({
45
+ const defaultManager = new PluginManager({
21
46
  staticDependencies: {
22
47
  contractis: require("contractis"),
23
- "@saltcorn/markup": require("@saltcorn/markup"),
24
- "@saltcorn/markup/tags": require("@saltcorn/markup/tags"),
25
- "@saltcorn/markup/layout": require("@saltcorn/markup/layout"),
26
- "@saltcorn/markup/helpers": require("@saltcorn/markup/helpers"),
27
- "@saltcorn/markup/layout_utils": require("@saltcorn/markup/layout_utils"),
28
- "@saltcorn/data": require("@saltcorn/data"),
29
- "@saltcorn/data/db": require("@saltcorn/data/db"),
30
- "@saltcorn/data/utils": require("@saltcorn/data/utils"),
31
- "@saltcorn/data/db/state": require("@saltcorn/data/db/state"),
32
- "@saltcorn/data/plugin-helper": require("@saltcorn/data/plugin-helper"),
33
- "@saltcorn/data/plugin-testing": require("@saltcorn/data/plugin-testing"),
34
- "@saltcorn/data/models/field": require("@saltcorn/data/models/field"),
35
- "@saltcorn/data/models/fieldrepeat": require("@saltcorn/data/models/fieldrepeat"),
36
- "@saltcorn/data/models/table": require("@saltcorn/data/models/table"),
37
- "@saltcorn/data/models/form": require("@saltcorn/data/models/form"),
38
- "@saltcorn/data/models/view": require("@saltcorn/data/models/view"),
39
- "@saltcorn/data/models/page": require("@saltcorn/data/models/page"),
40
- "@saltcorn/data/models/file": require("@saltcorn/data/models/file"),
41
- "@saltcorn/data/models/user": require("@saltcorn/data/models/user"),
42
- "@saltcorn/data/models/layout": require("@saltcorn/data/models/layout"),
43
- "@saltcorn/data/models/expression": require("@saltcorn/data/models/expression"),
44
- "@saltcorn/data/models/workflow": require("@saltcorn/data/models/workflow"),
48
+ ...staticDependencies,
45
49
  },
46
50
  });
47
51
 
@@ -73,9 +77,8 @@ const loadPlugin = async (plugin, force) => {
73
77
  };
74
78
 
75
79
  /**
76
- *
80
+ * Git pull or clone
77
81
  * @param plugin
78
- * @param force
79
82
  */
80
83
  const gitPullOrClone = async (plugin) => {
81
84
  await fs.promises.mkdir("git_plugins", { recursive: true });
@@ -102,9 +105,16 @@ const gitPullOrClone = async (plugin) => {
102
105
  if (plugin.deploy_private_key) await fs.promises.unlink(keyfnm);
103
106
  return dir;
104
107
  };
105
-
106
- const requirePlugin = async (plugin, force) => {
108
+ /**
109
+ * Install plugin
110
+ * @param plugin - plugin name
111
+ * @param force - force flag
112
+ * @param manager - plugin manager
113
+ * @returns {Promise<{plugin_module: *}|{plugin_module: any}>}
114
+ */
115
+ const requirePlugin = async (plugin, force, manager = defaultManager) => {
107
116
  const installed_plugins = (await manager.list()).map((p) => p.name);
117
+ // todo as idea is to make list of mandatory plugins configurable
108
118
  if (
109
119
  ["@saltcorn/base-plugin", "@saltcorn/sbadmin2"].includes(plugin.location)
110
120
  ) {
@@ -160,6 +170,7 @@ const loadAllPlugins = async () => {
160
170
  * Load Plugin and its dependencies and save into local installation
161
171
  * @param plugin
162
172
  * @param force
173
+ * @param noSignalOrDB
163
174
  * @returns {Promise<void>}
164
175
  */
165
176
  const loadAndSaveNewPlugin = async (plugin, force, noSignalOrDB) => {
@@ -207,4 +218,5 @@ module.exports = {
207
218
  loadAllPlugins,
208
219
  loadPlugin,
209
220
  requirePlugin,
221
+ staticDependencies,
210
222
  };
package/locales/da.json CHANGED
@@ -487,7 +487,7 @@
487
487
  "Role to create tenants": "Role to create tenants",
488
488
  "Minimum user role required to create a new tenant": "Minimum user role required to create a new tenant",
489
489
  "Create tenant warning": "Create tenant warning",
490
- "Show a warning to users creating a tenant disclaiming warrenty of availability or security": "Show a warning to users creating a tenant disclaiming warrenty of availability or security",
490
+ "Show a warning to users creating a tenant disclaiming warranty of availability or security": "Show a warning to users creating a tenant disclaiming warranty of availability or security",
491
491
  "Multitenancy settings": "Multitenancy settings",
492
492
  "Actions available": "Actions available",
493
493
  "Event types": "Event types",