@saltcorn/server 0.7.3 → 0.7.4-beta.0
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/auth/admin.js +2 -1
- package/auth/routes.js +16 -6
- package/errors.js +51 -48
- package/locales/en.json +10 -1
- package/locales/zh.json +1 -1
- package/package.json +7 -7
- package/public/jquery-menu-editor.min.js +1 -1
- package/public/saltcorn-common.js +24 -9
- package/routes/admin.js +272 -10
- package/routes/api.js +9 -1
- package/routes/page.js +5 -1
- package/routes/pageedit.js +9 -1
- package/routes/utils.js +4 -0
- package/routes/view.js +18 -1
- package/routes/viewedit.js +8 -1
- package/serve.js +54 -38
- package/tests/api.test.js +17 -0
- package/tests/clientjs.test.js +11 -1
package/auth/admin.js
CHANGED
|
@@ -693,7 +693,8 @@ router.post(
|
|
|
693
693
|
} = form.values;
|
|
694
694
|
if (id) {
|
|
695
695
|
try {
|
|
696
|
-
await
|
|
696
|
+
const u = await User.findOne({ id });
|
|
697
|
+
await u.update({ email, role_id, ...rest });
|
|
697
698
|
req.flash("success", req.__(`User %s saved`, email));
|
|
698
699
|
} catch (e) {
|
|
699
700
|
req.flash("error", req.__(`Error editing user: %s`, e.message));
|
package/auth/routes.js
CHANGED
|
@@ -199,8 +199,7 @@ const getAuthLinks = (current, noMethods) => {
|
|
|
199
199
|
return links;
|
|
200
200
|
};
|
|
201
201
|
|
|
202
|
-
const loginWithJwt = async (
|
|
203
|
-
const { email, password } = req.query;
|
|
202
|
+
const loginWithJwt = async (email, password, res) => {
|
|
204
203
|
const user = await User.findOne({ email });
|
|
205
204
|
if (user && user.checkPassword(password)) {
|
|
206
205
|
const now = new Date();
|
|
@@ -208,7 +207,13 @@ const loginWithJwt = async (req, res) => {
|
|
|
208
207
|
const token = jwt.sign(
|
|
209
208
|
{
|
|
210
209
|
sub: email,
|
|
211
|
-
|
|
210
|
+
user: {
|
|
211
|
+
id: user.id,
|
|
212
|
+
email: user.email,
|
|
213
|
+
role_id: user.role_id,
|
|
214
|
+
language: user.language ? user.language : "en",
|
|
215
|
+
disabled: user.disabled,
|
|
216
|
+
},
|
|
212
217
|
iss: "saltcorn@saltcorn",
|
|
213
218
|
aud: "saltcorn-mobile-app",
|
|
214
219
|
iat: now.valueOf(),
|
|
@@ -217,6 +222,10 @@ const loginWithJwt = async (req, res) => {
|
|
|
217
222
|
);
|
|
218
223
|
if (!user.last_mobile_login) await user.updateLastMobileLogin(now);
|
|
219
224
|
res.json(token);
|
|
225
|
+
} else {
|
|
226
|
+
res.json({
|
|
227
|
+
alerts: [{ type: "danger", msg: "Incorrect user or password" }],
|
|
228
|
+
});
|
|
220
229
|
}
|
|
221
230
|
};
|
|
222
231
|
|
|
@@ -900,8 +909,8 @@ router.post(
|
|
|
900
909
|
} else {
|
|
901
910
|
const u = await User.create({ email, password });
|
|
902
911
|
await send_verification_email(u, req);
|
|
903
|
-
|
|
904
|
-
signup_login_with_user(u, req, res);
|
|
912
|
+
if (req.smr) await loginWithJwt(email, password, res);
|
|
913
|
+
else signup_login_with_user(u, req, res);
|
|
905
914
|
}
|
|
906
915
|
}
|
|
907
916
|
})
|
|
@@ -1008,7 +1017,8 @@ router.get(
|
|
|
1008
1017
|
error_catcher(async (req, res, next) => {
|
|
1009
1018
|
const { method } = req.params;
|
|
1010
1019
|
if (method === "jwt") {
|
|
1011
|
-
|
|
1020
|
+
const { email, password } = req.query;
|
|
1021
|
+
await loginWithJwt(email, password, res);
|
|
1012
1022
|
} else {
|
|
1013
1023
|
const auth = getState().auth_methods[method];
|
|
1014
1024
|
if (auth) {
|
package/errors.js
CHANGED
|
@@ -7,55 +7,58 @@ const { pre, p, text, h3 } = require("@saltcorn/markup/tags");
|
|
|
7
7
|
const Crash = require("@saltcorn/data/models/crash");
|
|
8
8
|
const { getState } = require("@saltcorn/data/db/state");
|
|
9
9
|
|
|
10
|
-
module.exports =
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
async function (err, req, res, next) {
|
|
20
|
-
|
|
10
|
+
module.exports =
|
|
11
|
+
/**
|
|
12
|
+
*
|
|
13
|
+
* @param {object} err
|
|
14
|
+
* @param {object} req
|
|
15
|
+
* @param {object} res
|
|
16
|
+
* @param {*} next
|
|
17
|
+
* @returns {Promise<void>}
|
|
18
|
+
*/
|
|
19
|
+
async function (err, req, res, next) {
|
|
20
|
+
if (!req.__) req.__ = (s) => s;
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
const devmode = getState().getConfig("development_mode", false);
|
|
23
|
+
const log_sql = getState().getConfig("log_sql", false);
|
|
24
|
+
const role = (req.user || {}).role_id || 10;
|
|
25
|
+
if (err.message && err.message.includes("invalid csrf token")) {
|
|
26
|
+
console.error(err.message);
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
req.flash("error", req.__("Invalid form data, try again"));
|
|
29
|
+
if (req.url && req.url.includes("/auth/login"))
|
|
30
|
+
res.redirect("/auth/login");
|
|
31
|
+
else res.redirect("/");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const code = err.httpCode || 500;
|
|
35
|
+
const headline = err.headline || "An error occurred";
|
|
36
|
+
const severity = err.severity || 2;
|
|
37
|
+
const createCrash = severity <= 3;
|
|
38
|
+
//console.error(err.stack);
|
|
39
|
+
if (!(devmode && log_sql) && createCrash) await Crash.create(err, req);
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
?
|
|
55
|
-
|
|
56
|
-
|
|
41
|
+
if (req.xhr) {
|
|
42
|
+
res
|
|
43
|
+
.status(code)
|
|
44
|
+
.send(
|
|
45
|
+
devmode || role === 1
|
|
46
|
+
? text(err.message)
|
|
47
|
+
: req.__("An error occurred")
|
|
48
|
+
);
|
|
49
|
+
} else
|
|
50
|
+
res
|
|
51
|
+
.status(code)
|
|
52
|
+
.sendWrap(
|
|
53
|
+
req.__(headline),
|
|
54
|
+
devmode ? pre(text(err.stack)) : h3(req.__(headline)),
|
|
55
|
+
role === 1 && !devmode ? pre(text(err.message)) : "",
|
|
56
|
+
createCrash
|
|
57
|
+
? p(
|
|
58
|
+
req.__(
|
|
59
|
+
`A report has been logged and a team of bug-squashing squirrels has been dispatched to deal with the situation.`
|
|
60
|
+
)
|
|
57
61
|
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
62
|
+
: ""
|
|
63
|
+
);
|
|
64
|
+
};
|
package/locales/en.json
CHANGED
|
@@ -921,5 +921,14 @@
|
|
|
921
921
|
"Restoring automated backup": "Restoring automated backup",
|
|
922
922
|
"No errors detected during configuration check": "No errors detected during configuration check",
|
|
923
923
|
"%s view - %s on %s": "%s view - %s on %s",
|
|
924
|
-
"
|
|
924
|
+
"Please select at least one platform (android or iOS).": "Please select at least one platform (android or iOS).",
|
|
925
|
+
"Back": "Back",
|
|
926
|
+
"Periodic snapshots enabled": "Periodic snapshots enabled",
|
|
927
|
+
"Snapshot will be made every hour if there are changes": "Snapshot will be made every hour if there are changes",
|
|
928
|
+
"Snapshots": "Snapshots",
|
|
929
|
+
"Snapshot settings updated": "Snapshot settings updated",
|
|
930
|
+
"Download snapshots": "Download snapshots",
|
|
931
|
+
"Snapshot successful": "Snapshot successful",
|
|
932
|
+
"System logging verbosity": "System logging verbosity",
|
|
933
|
+
"Destination URL Formula": "Destination URL Formula"
|
|
925
934
|
}
|
package/locales/zh.json
CHANGED
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
"Pack %s installed": "已安装包 %s",
|
|
180
180
|
"Pack %s uninstalled": "包 %s 已卸载",
|
|
181
181
|
"Uninstall": "卸载",
|
|
182
|
-
"Result preview for ": "结果预览",
|
|
182
|
+
"Result preview for ": "结果预览",
|
|
183
183
|
"Choose views for <a href=\"/search\">search results</a> for each table.<br/>Set to blank to omit table from global search.": "为每个表选择<a href=\"/search\">搜索结果</a>的视图。<br/>为空则可从全局搜索中忽略表。",
|
|
184
184
|
"These tables lack suitable views: ": "这些表缺少合适的视图:",
|
|
185
185
|
"Search configuration": "搜索配置",
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4-beta.0",
|
|
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.7.
|
|
10
|
-
"@saltcorn/builder": "0.7.
|
|
11
|
-
"@saltcorn/data": "0.7.
|
|
12
|
-
"@saltcorn/admin-models": "0.7.
|
|
13
|
-
"@saltcorn/markup": "0.7.
|
|
14
|
-
"@saltcorn/sbadmin2": "0.7.
|
|
9
|
+
"@saltcorn/base-plugin": "0.7.4-beta.0",
|
|
10
|
+
"@saltcorn/builder": "0.7.4-beta.0",
|
|
11
|
+
"@saltcorn/data": "0.7.4-beta.0",
|
|
12
|
+
"@saltcorn/admin-models": "0.7.4-beta.0",
|
|
13
|
+
"@saltcorn/markup": "0.7.4-beta.0",
|
|
14
|
+
"@saltcorn/sbadmin2": "0.7.4-beta.0",
|
|
15
15
|
"@socket.io/cluster-adapter": "^0.1.0",
|
|
16
16
|
"@socket.io/sticky": "^1.0.1",
|
|
17
17
|
"aws-sdk": "^2.1037.0",
|
|
@@ -39,16 +39,26 @@ function apply_showif() {
|
|
|
39
39
|
$("[data-show-if]").each(function (ix, element) {
|
|
40
40
|
var e = $(element);
|
|
41
41
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
let to_show = e.data("data-show-if-fun");
|
|
43
|
+
if (!to_show) {
|
|
44
|
+
to_show = new Function(
|
|
45
|
+
"e",
|
|
46
|
+
"return " + decodeURIComponent(e.attr("data-show-if"))
|
|
47
|
+
);
|
|
48
|
+
e.data("data-show-if-fun", to_show);
|
|
49
|
+
}
|
|
50
|
+
if (!e.data("data-closest-form-ns"))
|
|
51
|
+
e.data("data-closest-form-ns", e.closest(".form-namespace"));
|
|
46
52
|
if (to_show(e))
|
|
47
53
|
e.show()
|
|
48
54
|
.find("input, textarea, button, select")
|
|
49
55
|
.prop("disabled", e.attr("data-disabled") || false);
|
|
50
56
|
else
|
|
51
|
-
e.hide()
|
|
57
|
+
e.hide()
|
|
58
|
+
.find(
|
|
59
|
+
"input:enabled, textarea:enabled, button:enabled, select:enabled"
|
|
60
|
+
)
|
|
61
|
+
.prop("disabled", true);
|
|
52
62
|
} catch (e) {
|
|
53
63
|
console.error(e);
|
|
54
64
|
}
|
|
@@ -111,7 +121,12 @@ function apply_showif() {
|
|
|
111
121
|
`<option ${
|
|
112
122
|
`${current}` === `${r[dynwhere.refname]}` ? "selected" : ""
|
|
113
123
|
} value="${r[dynwhere.refname]}">${
|
|
114
|
-
|
|
124
|
+
dynwhere.label_formula
|
|
125
|
+
? new Function(
|
|
126
|
+
`{${Object.keys(r).join(",")}}`,
|
|
127
|
+
"return " + dynwhere.label_formula
|
|
128
|
+
)(r)
|
|
129
|
+
: r[dynwhere.summary_field]
|
|
115
130
|
}</option>`
|
|
116
131
|
)
|
|
117
132
|
);
|
|
@@ -510,7 +525,7 @@ const repeaterCopyValuesToForm = (form, editor, noTriggerChange) => {
|
|
|
510
525
|
if ($e.length) $e.val(v);
|
|
511
526
|
else
|
|
512
527
|
form.append(
|
|
513
|
-
`<input type="hidden" name="${k}_${ix}" value="${v}"></input>`
|
|
528
|
+
`<input type="hidden" data-repeater-ix="${ix}" name="${k}_${ix}" value="${v}"></input>`
|
|
514
529
|
);
|
|
515
530
|
};
|
|
516
531
|
vs.forEach((v, ix) => {
|
|
@@ -521,8 +536,8 @@ const repeaterCopyValuesToForm = (form, editor, noTriggerChange) => {
|
|
|
521
536
|
});
|
|
522
537
|
});
|
|
523
538
|
//delete
|
|
524
|
-
//for (let ix = vs.length; ix < vs.length +
|
|
525
|
-
//
|
|
539
|
+
//for (let ix = vs.length; ix < vs.length + 5; ix++) {
|
|
540
|
+
// $(`input[data-repeater-ix="${ix}"]`).remove();
|
|
526
541
|
//}
|
|
527
542
|
$(`input[type=hidden]`).each(function () {
|
|
528
543
|
const name = $(this).attr("name");
|
package/routes/admin.js
CHANGED
|
@@ -18,7 +18,13 @@ const { spawn } = require("child_process");
|
|
|
18
18
|
const User = require("@saltcorn/data/models/user");
|
|
19
19
|
const path = require("path");
|
|
20
20
|
const { getAllTenants } = require("@saltcorn/admin-models/models/tenant");
|
|
21
|
-
const {
|
|
21
|
+
const {
|
|
22
|
+
post_btn,
|
|
23
|
+
renderForm,
|
|
24
|
+
mkTable,
|
|
25
|
+
link,
|
|
26
|
+
localeDateTime,
|
|
27
|
+
} = require("@saltcorn/markup");
|
|
22
28
|
const {
|
|
23
29
|
div,
|
|
24
30
|
a,
|
|
@@ -42,7 +48,6 @@ const {
|
|
|
42
48
|
select,
|
|
43
49
|
option,
|
|
44
50
|
fieldset,
|
|
45
|
-
legend,
|
|
46
51
|
ul,
|
|
47
52
|
li,
|
|
48
53
|
ol,
|
|
@@ -63,6 +68,7 @@ const {
|
|
|
63
68
|
restore,
|
|
64
69
|
auto_backup_now,
|
|
65
70
|
} = require("@saltcorn/admin-models/models/backup");
|
|
71
|
+
const Snapshot = require("@saltcorn/admin-models/models/snapshot");
|
|
66
72
|
const {
|
|
67
73
|
runConfigurationCheck,
|
|
68
74
|
} = require("@saltcorn/admin-models/models/config-check");
|
|
@@ -89,6 +95,7 @@ const moment = require("moment");
|
|
|
89
95
|
const View = require("@saltcorn/data/models/view");
|
|
90
96
|
const { getConfigFile } = require("@saltcorn/data/db/connect");
|
|
91
97
|
const os = require("os");
|
|
98
|
+
const Page = require("@saltcorn/data/models/page");
|
|
92
99
|
|
|
93
100
|
/**
|
|
94
101
|
* @type {object}
|
|
@@ -118,6 +125,7 @@ const site_id_form = (req) =>
|
|
|
118
125
|
"page_custom_html",
|
|
119
126
|
"development_mode",
|
|
120
127
|
"log_sql",
|
|
128
|
+
"log_level",
|
|
121
129
|
"plugins_store_endpoint",
|
|
122
130
|
"packs_store_endpoint",
|
|
123
131
|
...(getConfigFile() ? ["multitenancy_enabled"] : []),
|
|
@@ -337,6 +345,9 @@ router.get(
|
|
|
337
345
|
backupForm.values.auto_backup_expire_days = getState().getConfig(
|
|
338
346
|
"auto_backup_expire_days"
|
|
339
347
|
);
|
|
348
|
+
const aSnapshotForm = snapshotForm(req);
|
|
349
|
+
aSnapshotForm.values.snapshots_enabled =
|
|
350
|
+
getState().getConfig("snapshots_enabled");
|
|
340
351
|
const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
|
|
341
352
|
|
|
342
353
|
send_admin_page({
|
|
@@ -389,6 +400,22 @@ router.get(
|
|
|
389
400
|
),
|
|
390
401
|
}
|
|
391
402
|
: { type: "blank", contents: "" },
|
|
403
|
+
{
|
|
404
|
+
type: "card",
|
|
405
|
+
title: req.__("Snapshots"),
|
|
406
|
+
contents: div(
|
|
407
|
+
p(
|
|
408
|
+
i(
|
|
409
|
+
"Snapshots store your application structure and definition, without the table data. Individual views and pages can be restored from snapshots from the <a href='/viewedit'>view</a> or <a href='/pageedit'>pages</a> overviews (\"Restore\" from individual page or view dropdowns)."
|
|
410
|
+
)
|
|
411
|
+
),
|
|
412
|
+
renderForm(aSnapshotForm, req.csrfToken()),
|
|
413
|
+
a(
|
|
414
|
+
{ href: "/admin/snapshot-list" },
|
|
415
|
+
"List/download snapshots »"
|
|
416
|
+
)
|
|
417
|
+
),
|
|
418
|
+
},
|
|
392
419
|
],
|
|
393
420
|
},
|
|
394
421
|
});
|
|
@@ -464,6 +491,103 @@ router.get(
|
|
|
464
491
|
})
|
|
465
492
|
);
|
|
466
493
|
|
|
494
|
+
router.get(
|
|
495
|
+
"/snapshot-list",
|
|
496
|
+
isAdmin,
|
|
497
|
+
error_catcher(async (req, res) => {
|
|
498
|
+
const snaps = await Snapshot.find();
|
|
499
|
+
send_admin_page({
|
|
500
|
+
res,
|
|
501
|
+
req,
|
|
502
|
+
active_sub: "Backup",
|
|
503
|
+
contents: {
|
|
504
|
+
above: [
|
|
505
|
+
{
|
|
506
|
+
type: "card",
|
|
507
|
+
title: req.__("Download snapshots"),
|
|
508
|
+
contents: div(
|
|
509
|
+
ul(
|
|
510
|
+
snaps.map((snap) =>
|
|
511
|
+
li(
|
|
512
|
+
a(
|
|
513
|
+
{
|
|
514
|
+
href: `/admin/snapshot-download/${encodeURIComponent(
|
|
515
|
+
snap.id
|
|
516
|
+
)}`,
|
|
517
|
+
target: "_blank",
|
|
518
|
+
},
|
|
519
|
+
`${localeDateTime(snap.created)} (${moment(
|
|
520
|
+
snap.created
|
|
521
|
+
).fromNow()})`
|
|
522
|
+
)
|
|
523
|
+
)
|
|
524
|
+
)
|
|
525
|
+
)
|
|
526
|
+
),
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
})
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
router.get(
|
|
535
|
+
"/snapshot-download/:id",
|
|
536
|
+
isAdmin,
|
|
537
|
+
error_catcher(async (req, res) => {
|
|
538
|
+
const { id } = req.params;
|
|
539
|
+
const snap = await Snapshot.findOne({ id });
|
|
540
|
+
res.send(snap.pack);
|
|
541
|
+
})
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
router.get(
|
|
545
|
+
"/snapshot-restore/:type/:name",
|
|
546
|
+
isAdmin,
|
|
547
|
+
error_catcher(async (req, res) => {
|
|
548
|
+
const { type, name } = req.params;
|
|
549
|
+
const snaps = await Snapshot.entity_history(type, name);
|
|
550
|
+
res.send(
|
|
551
|
+
mkTable(
|
|
552
|
+
[
|
|
553
|
+
{
|
|
554
|
+
label: "When",
|
|
555
|
+
key: (r) =>
|
|
556
|
+
`${localeDateTime(r.created)} (${moment(r.created).fromNow()})`,
|
|
557
|
+
},
|
|
558
|
+
|
|
559
|
+
{
|
|
560
|
+
label: req.__("Restore"),
|
|
561
|
+
key: (r) =>
|
|
562
|
+
post_btn(
|
|
563
|
+
`/admin/snapshot-restore/${type}/${name}/${r.id}`,
|
|
564
|
+
req.__("Restore"),
|
|
565
|
+
req.csrfToken()
|
|
566
|
+
),
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
snaps
|
|
570
|
+
)
|
|
571
|
+
);
|
|
572
|
+
})
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
router.post(
|
|
576
|
+
"/snapshot-restore/:type/:name/:id",
|
|
577
|
+
isAdmin,
|
|
578
|
+
error_catcher(async (req, res) => {
|
|
579
|
+
const { type, name, id } = req.params;
|
|
580
|
+
const snap = await Snapshot.findOne({ id });
|
|
581
|
+
await snap.restore_entity(type, name);
|
|
582
|
+
req.flash(
|
|
583
|
+
"success",
|
|
584
|
+
`${type} ${name} restored to snapshot saved ${moment(
|
|
585
|
+
snap.created
|
|
586
|
+
).fromNow()}`
|
|
587
|
+
);
|
|
588
|
+
res.redirect(`/${type}edit`);
|
|
589
|
+
})
|
|
590
|
+
);
|
|
467
591
|
router.get(
|
|
468
592
|
"/auto-backup-download/:filename",
|
|
469
593
|
isAdmin,
|
|
@@ -540,6 +664,43 @@ const autoBackupForm = (req) =>
|
|
|
540
664
|
],
|
|
541
665
|
});
|
|
542
666
|
|
|
667
|
+
const snapshotForm = (req) =>
|
|
668
|
+
new Form({
|
|
669
|
+
action: "/admin/set-snapshot",
|
|
670
|
+
onChange: `saveAndContinue(this);`,
|
|
671
|
+
noSubmitButton: true,
|
|
672
|
+
additionalButtons: [
|
|
673
|
+
{
|
|
674
|
+
label: "Snapshot now",
|
|
675
|
+
id: "btnSnapNow",
|
|
676
|
+
class: "btn btn-outline-secondary",
|
|
677
|
+
onclick: "ajax_post('/admin/snapshot-now')",
|
|
678
|
+
},
|
|
679
|
+
],
|
|
680
|
+
fields: [
|
|
681
|
+
{
|
|
682
|
+
type: "Bool",
|
|
683
|
+
label: req.__("Periodic snapshots enabled"),
|
|
684
|
+
name: "snapshots_enabled",
|
|
685
|
+
sublabel: req.__(
|
|
686
|
+
"Snapshot will be made every hour if there are changes"
|
|
687
|
+
),
|
|
688
|
+
},
|
|
689
|
+
],
|
|
690
|
+
});
|
|
691
|
+
router.post(
|
|
692
|
+
"/set-snapshot",
|
|
693
|
+
isAdmin,
|
|
694
|
+
error_catcher(async (req, res) => {
|
|
695
|
+
const form = await snapshotForm(req);
|
|
696
|
+
form.validate(req.body);
|
|
697
|
+
|
|
698
|
+
await save_config_from_form(form);
|
|
699
|
+
req.flash("success", req.__("Snapshot settings updated"));
|
|
700
|
+
if (!req.xhr) res.redirect("/admin/backup");
|
|
701
|
+
else res.json({ success: "ok" });
|
|
702
|
+
})
|
|
703
|
+
);
|
|
543
704
|
router.post(
|
|
544
705
|
"/set-auto-backup",
|
|
545
706
|
isAdmin,
|
|
@@ -579,6 +740,22 @@ router.post(
|
|
|
579
740
|
})
|
|
580
741
|
);
|
|
581
742
|
|
|
743
|
+
router.post(
|
|
744
|
+
"/snapshot-now",
|
|
745
|
+
isAdmin,
|
|
746
|
+
error_catcher(async (req, res) => {
|
|
747
|
+
try {
|
|
748
|
+
const taken = await Snapshot.take_if_changed();
|
|
749
|
+
if (taken) req.flash("success", req.__("Snapshot successful"));
|
|
750
|
+
else
|
|
751
|
+
req.flash("success", req.__("No changes detected, snapshot skipped"));
|
|
752
|
+
} catch (e) {
|
|
753
|
+
req.flash("error", e.message);
|
|
754
|
+
}
|
|
755
|
+
res.json({ reload_page: true });
|
|
756
|
+
})
|
|
757
|
+
);
|
|
758
|
+
|
|
582
759
|
/**
|
|
583
760
|
* @name get/system
|
|
584
761
|
* @function
|
|
@@ -1064,12 +1241,39 @@ router.get(
|
|
|
1064
1241
|
})
|
|
1065
1242
|
);
|
|
1066
1243
|
|
|
1244
|
+
const dialogScript = `<script>
|
|
1245
|
+
function swapEntryInputs(activeTab, activeInput, disabledTab, disabledInput) {
|
|
1246
|
+
activeTab.addClass("active");
|
|
1247
|
+
activeInput.removeClass("d-none");
|
|
1248
|
+
activeInput.addClass("d-block");
|
|
1249
|
+
activeInput.attr("name", "entryPoint");
|
|
1250
|
+
disabledTab.removeClass("active");
|
|
1251
|
+
disabledInput.removeClass("d-block");
|
|
1252
|
+
disabledInput.addClass("d-none");
|
|
1253
|
+
disabledInput.removeAttr("name");
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function showEntrySelect(type) {
|
|
1257
|
+
const viewNavLin = $("#viewNavLinkID");
|
|
1258
|
+
const pageNavLink = $("#pageNavLinkID");
|
|
1259
|
+
const viewInp = $("#viewInputID");
|
|
1260
|
+
const pageInp = $("#pageInputID");
|
|
1261
|
+
if (type === "page") {
|
|
1262
|
+
swapEntryInputs(pageNavLink, pageInp, viewNavLin, viewInp);
|
|
1263
|
+
}
|
|
1264
|
+
else if (type === "view") {
|
|
1265
|
+
swapEntryInputs(viewNavLin, viewInp, pageNavLink, pageInp);
|
|
1266
|
+
}
|
|
1267
|
+
$("#entryPointTypeID").attr("value", type);
|
|
1268
|
+
}
|
|
1269
|
+
</script>`;
|
|
1270
|
+
|
|
1067
1271
|
router.get(
|
|
1068
1272
|
"/build-mobile-app",
|
|
1069
1273
|
isAdmin,
|
|
1070
1274
|
error_catcher(async (req, res) => {
|
|
1071
|
-
const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
|
|
1072
1275
|
const views = await View.find();
|
|
1276
|
+
const pages = await Page.find();
|
|
1073
1277
|
const execBuildMsg =
|
|
1074
1278
|
"This is still under development and might run longer.";
|
|
1075
1279
|
|
|
@@ -1077,6 +1281,11 @@ router.get(
|
|
|
1077
1281
|
res,
|
|
1078
1282
|
req,
|
|
1079
1283
|
active_sub: "Mobile app",
|
|
1284
|
+
headers: [
|
|
1285
|
+
{
|
|
1286
|
+
headerTag: dialogScript,
|
|
1287
|
+
},
|
|
1288
|
+
],
|
|
1080
1289
|
contents: {
|
|
1081
1290
|
above: [
|
|
1082
1291
|
{
|
|
@@ -1094,11 +1303,17 @@ router.get(
|
|
|
1094
1303
|
name: "_csrf",
|
|
1095
1304
|
value: req.csrfToken(),
|
|
1096
1305
|
}),
|
|
1306
|
+
input({
|
|
1307
|
+
type: "hidden",
|
|
1308
|
+
name: "entryPointType",
|
|
1309
|
+
value: "view",
|
|
1310
|
+
id: "entryPointTypeID",
|
|
1311
|
+
}),
|
|
1097
1312
|
div(
|
|
1098
1313
|
{ class: "container ps-2" },
|
|
1099
1314
|
div(
|
|
1100
1315
|
{ class: "row pb-2" },
|
|
1101
|
-
div({ class: "col-sm-4 fw-bold" }, "Entry
|
|
1316
|
+
div({ class: "col-sm-4 fw-bold" }, "Entry point"),
|
|
1102
1317
|
div({ class: "col-sm-4 fw-bold" }, "Platform"),
|
|
1103
1318
|
div(
|
|
1104
1319
|
{
|
|
@@ -1111,17 +1326,54 @@ router.get(
|
|
|
1111
1326
|
{ class: "row" },
|
|
1112
1327
|
div(
|
|
1113
1328
|
{ class: "col-sm-4" },
|
|
1329
|
+
// 'view/page' tabs
|
|
1330
|
+
ul(
|
|
1331
|
+
{ class: "nav nav-pills" },
|
|
1332
|
+
li(
|
|
1333
|
+
{
|
|
1334
|
+
class: "nav-item",
|
|
1335
|
+
onClick: "showEntrySelect('view')",
|
|
1336
|
+
},
|
|
1337
|
+
div(
|
|
1338
|
+
{ class: "nav-link active", id: "viewNavLinkID" },
|
|
1339
|
+
"View"
|
|
1340
|
+
)
|
|
1341
|
+
),
|
|
1342
|
+
li(
|
|
1343
|
+
{
|
|
1344
|
+
class: "nav-item",
|
|
1345
|
+
onClick: "showEntrySelect('page')",
|
|
1346
|
+
},
|
|
1347
|
+
div(
|
|
1348
|
+
{ class: "nav-link", id: "pageNavLinkID" },
|
|
1349
|
+
"Page"
|
|
1350
|
+
)
|
|
1351
|
+
)
|
|
1352
|
+
),
|
|
1353
|
+
// select entry-view
|
|
1114
1354
|
select(
|
|
1115
1355
|
{
|
|
1116
1356
|
class: "form-control",
|
|
1117
|
-
name: "
|
|
1118
|
-
id: "
|
|
1357
|
+
name: "entryPoint",
|
|
1358
|
+
id: "viewInputID",
|
|
1119
1359
|
},
|
|
1120
1360
|
views
|
|
1121
1361
|
.map((view) =>
|
|
1122
1362
|
option({ value: view.name }, view.name)
|
|
1123
1363
|
)
|
|
1124
1364
|
.join(",")
|
|
1365
|
+
),
|
|
1366
|
+
// select entry-page
|
|
1367
|
+
select(
|
|
1368
|
+
{
|
|
1369
|
+
class: "form-control d-none",
|
|
1370
|
+
id: "pageInputID",
|
|
1371
|
+
},
|
|
1372
|
+
pages
|
|
1373
|
+
.map((page) =>
|
|
1374
|
+
option({ value: page.name }, page.name)
|
|
1375
|
+
)
|
|
1376
|
+
.join(",")
|
|
1125
1377
|
)
|
|
1126
1378
|
),
|
|
1127
1379
|
div(
|
|
@@ -1203,7 +1455,7 @@ router.get(
|
|
|
1203
1455
|
class: "form-control",
|
|
1204
1456
|
name: "serverURL",
|
|
1205
1457
|
id: "serverURLInputId",
|
|
1206
|
-
placeholder:
|
|
1458
|
+
placeholder: getState().getConfig("base_url") || "",
|
|
1207
1459
|
})
|
|
1208
1460
|
)
|
|
1209
1461
|
)
|
|
@@ -1232,7 +1484,8 @@ router.post(
|
|
|
1232
1484
|
isAdmin,
|
|
1233
1485
|
error_catcher(async (req, res) => {
|
|
1234
1486
|
let {
|
|
1235
|
-
|
|
1487
|
+
entryPoint,
|
|
1488
|
+
entryPointType,
|
|
1236
1489
|
androidPlatform,
|
|
1237
1490
|
iOSPlatform,
|
|
1238
1491
|
useDocker,
|
|
@@ -1251,11 +1504,20 @@ router.post(
|
|
|
1251
1504
|
return res.redirect("/admin/build-mobile-app");
|
|
1252
1505
|
}
|
|
1253
1506
|
if (appFile && !appFile.endsWith(".apk")) appFile = `${appFile}.apk`;
|
|
1507
|
+
if (!serverURL || serverURL.length == 0) {
|
|
1508
|
+
serverURL = getState().getConfig("base_url") || "";
|
|
1509
|
+
}
|
|
1510
|
+
if (!serverURL.startsWith("http")) {
|
|
1511
|
+
req.flash("error", req.__("Please enter a valid server URL."));
|
|
1512
|
+
return res.redirect("/admin/build-mobile-app");
|
|
1513
|
+
}
|
|
1254
1514
|
const appOut = path.join(__dirname, "..", "mobile-app-out");
|
|
1255
1515
|
const spawnParams = [
|
|
1256
1516
|
"build-app",
|
|
1257
|
-
"-
|
|
1258
|
-
|
|
1517
|
+
"-e",
|
|
1518
|
+
entryPoint,
|
|
1519
|
+
"-t",
|
|
1520
|
+
entryPointType,
|
|
1259
1521
|
"-c",
|
|
1260
1522
|
appOut,
|
|
1261
1523
|
"-b",
|
package/routes/api.js
CHANGED
|
@@ -112,6 +112,13 @@ function accessAllowed(req, user, trigger) {
|
|
|
112
112
|
return role <= trigger.min_role;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
const getFlashes = (req) =>
|
|
116
|
+
["error", "success", "danger", "warning", "information"]
|
|
117
|
+
.map((type) => {
|
|
118
|
+
return { type, msg: req.flash(type) };
|
|
119
|
+
})
|
|
120
|
+
.filter((a) => a.msg && a.msg.length && a.msg.length > 0);
|
|
121
|
+
|
|
115
122
|
router.post(
|
|
116
123
|
"/viewQuery/:viewName/:queryName",
|
|
117
124
|
error_catcher(async (req, res, next) => {
|
|
@@ -134,7 +141,7 @@ router.post(
|
|
|
134
141
|
if (queries[queryName]) {
|
|
135
142
|
const { args } = req.body;
|
|
136
143
|
const resp = await queries[queryName](...args, true);
|
|
137
|
-
res.json({ success: resp });
|
|
144
|
+
res.json({ success: resp, alerts: getFlashes(req) });
|
|
138
145
|
} else {
|
|
139
146
|
res.status(404).json({ error: req.__("Not found") });
|
|
140
147
|
}
|
|
@@ -235,6 +242,7 @@ router.get(
|
|
|
235
242
|
rows = await table.getJoinedRows(joinOpts);
|
|
236
243
|
} else if (req_query && req_query !== {}) {
|
|
237
244
|
const tbl_fields = await table.getFields();
|
|
245
|
+
readState(req_query, tbl_fields, req);
|
|
238
246
|
const qstate = await stateFieldsToWhere({
|
|
239
247
|
fields: tbl_fields,
|
|
240
248
|
approximate: !!approximate,
|
package/routes/page.js
CHANGED
|
@@ -36,6 +36,8 @@ router.get(
|
|
|
36
36
|
"/:pagename",
|
|
37
37
|
error_catcher(async (req, res) => {
|
|
38
38
|
const { pagename } = req.params;
|
|
39
|
+
const state = getState();
|
|
40
|
+
state.log(3, `Route /page/${pagename} user=${req.user?.id}`);
|
|
39
41
|
|
|
40
42
|
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
41
43
|
const db_page = await Page.findOne({ name: pagename });
|
|
@@ -56,10 +58,12 @@ router.get(
|
|
|
56
58
|
contents,
|
|
57
59
|
})
|
|
58
60
|
);
|
|
59
|
-
} else
|
|
61
|
+
} else {
|
|
62
|
+
state.log(2, `Page $pagename} not found or not authorized`);
|
|
60
63
|
res
|
|
61
64
|
.status(404)
|
|
62
65
|
.sendWrap(`${pagename} page`, req.__("Page %s not found", pagename));
|
|
66
|
+
}
|
|
63
67
|
})
|
|
64
68
|
);
|
|
65
69
|
|
package/routes/pageedit.js
CHANGED
|
@@ -90,6 +90,13 @@ const page_dropdown = (page, req) =>
|
|
|
90
90
|
'<i class="far fa-copy"></i> ' + req.__("Duplicate"),
|
|
91
91
|
req
|
|
92
92
|
),
|
|
93
|
+
a(
|
|
94
|
+
{
|
|
95
|
+
class: "dropdown-item",
|
|
96
|
+
href: `javascript:ajax_modal('/admin/snapshot-restore/page/${page.name}')`,
|
|
97
|
+
},
|
|
98
|
+
'<i class="fas fa-undo-alt"></i> ' + req.__("Restore")
|
|
99
|
+
),
|
|
93
100
|
div({ class: "dropdown-divider" }),
|
|
94
101
|
post_dropdown_item(
|
|
95
102
|
`/pageedit/delete/${page.id}`,
|
|
@@ -180,7 +187,8 @@ const pageBuilderData = async (req, context) => {
|
|
|
180
187
|
const fixed_state_fields = {};
|
|
181
188
|
for (const view of views) {
|
|
182
189
|
fixed_state_fields[view.name] = [];
|
|
183
|
-
const table = Table.findOne(
|
|
190
|
+
const table = Table.findOne(view.table_id || view.exttable_name);
|
|
191
|
+
|
|
184
192
|
const fs = await view.get_state_fields();
|
|
185
193
|
for (const frec of fs) {
|
|
186
194
|
const f = new Field(frec);
|
package/routes/utils.js
CHANGED
|
@@ -128,6 +128,7 @@ const setTenant = (req, res, next) => {
|
|
|
128
128
|
} else {
|
|
129
129
|
db.runWithTenant(other_domain, () => {
|
|
130
130
|
setLanguage(req, res, state);
|
|
131
|
+
state.log(5, `${req.method} ${req.originalUrl}`);
|
|
131
132
|
next();
|
|
132
133
|
});
|
|
133
134
|
}
|
|
@@ -140,12 +141,14 @@ const setTenant = (req, res, next) => {
|
|
|
140
141
|
} else {
|
|
141
142
|
db.runWithTenant(ten, () => {
|
|
142
143
|
setLanguage(req, res, state);
|
|
144
|
+
state.log(5, `${req.method} ${req.originalUrl}`);
|
|
143
145
|
next();
|
|
144
146
|
});
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
} else {
|
|
148
150
|
setLanguage(req, res);
|
|
151
|
+
getState().log(5, `${req.method} ${req.originalUrl}`);
|
|
149
152
|
next();
|
|
150
153
|
}
|
|
151
154
|
};
|
|
@@ -240,4 +243,5 @@ module.exports = {
|
|
|
240
243
|
getGitRevision,
|
|
241
244
|
getSessionStore,
|
|
242
245
|
setTenant,
|
|
246
|
+
get_tenant_from_req
|
|
243
247
|
};
|
package/routes/view.js
CHANGED
|
@@ -20,6 +20,7 @@ const {
|
|
|
20
20
|
} = require("../routes/utils.js");
|
|
21
21
|
const { add_edit_bar } = require("../markup/admin.js");
|
|
22
22
|
const { InvalidConfiguration } = require("@saltcorn/data/utils");
|
|
23
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* @type {object}
|
|
@@ -44,8 +45,11 @@ router.get(
|
|
|
44
45
|
const query = { ...req.query };
|
|
45
46
|
const view = await View.findOne({ name: viewname });
|
|
46
47
|
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
48
|
+
const state = getState();
|
|
49
|
+
state.log(3, `Route /view/${viewname} user=${req.user?.id}`);
|
|
47
50
|
if (!view) {
|
|
48
51
|
req.flash("danger", req.__(`No such view: %s`, text(viewname)));
|
|
52
|
+
state.log(2, `View ${viewname} not found`);
|
|
49
53
|
res.redirect("/");
|
|
50
54
|
return;
|
|
51
55
|
}
|
|
@@ -56,6 +60,7 @@ router.get(
|
|
|
56
60
|
!(await view.authorise_get({ query, req, ...view }))
|
|
57
61
|
) {
|
|
58
62
|
req.flash("danger", req.__("Not authorized"));
|
|
63
|
+
state.log(2, `View ${viewname} not authorized`);
|
|
59
64
|
res.redirect("/");
|
|
60
65
|
return;
|
|
61
66
|
}
|
|
@@ -123,13 +128,21 @@ router.post(
|
|
|
123
128
|
error_catcher(async (req, res) => {
|
|
124
129
|
const { viewname, route } = req.params;
|
|
125
130
|
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
131
|
+
const state = getState();
|
|
132
|
+
state.log(
|
|
133
|
+
3,
|
|
134
|
+
`Route /view/${viewname} viewroute ${route} user=${req.user?.id}`
|
|
135
|
+
);
|
|
126
136
|
|
|
127
137
|
const view = await View.findOne({ name: viewname });
|
|
128
138
|
if (!view) {
|
|
129
139
|
req.flash("danger", req.__(`No such view: %s`, text(viewname)));
|
|
140
|
+
state.log(2, `View ${viewname} not found`);
|
|
130
141
|
res.redirect("/");
|
|
131
142
|
} else if (role > view.min_role) {
|
|
132
143
|
req.flash("danger", req.__("Not authorized"));
|
|
144
|
+
state.log(2, `View ${viewname} viewroute ${route} not authorized`);
|
|
145
|
+
|
|
133
146
|
res.redirect("/");
|
|
134
147
|
} else {
|
|
135
148
|
await view.runRoute(route, req.body, res, { res, req });
|
|
@@ -150,10 +163,12 @@ router.post(
|
|
|
150
163
|
const { viewname } = req.params;
|
|
151
164
|
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
152
165
|
const query = { ...req.query };
|
|
153
|
-
|
|
166
|
+
const state = getState();
|
|
167
|
+
state.log(3, `Route /view/${viewname} POST user=${req.user?.id}`);
|
|
154
168
|
const view = await View.findOne({ name: viewname });
|
|
155
169
|
if (!view) {
|
|
156
170
|
req.flash("danger", req.__(`No such view: %s`, text(viewname)));
|
|
171
|
+
state.log(2, `View ${viewname} not found`);
|
|
157
172
|
res.redirect("/");
|
|
158
173
|
return;
|
|
159
174
|
}
|
|
@@ -164,6 +179,8 @@ router.post(
|
|
|
164
179
|
!(await view.authorise_post({ body: req.body, req, ...view }))
|
|
165
180
|
) {
|
|
166
181
|
req.flash("danger", req.__("Not authorized"));
|
|
182
|
+
state.log(2, `View ${viewname} POST not authorized`);
|
|
183
|
+
|
|
167
184
|
res.redirect("/");
|
|
168
185
|
} else if (!view.runPost) {
|
|
169
186
|
throw new InvalidConfiguration(
|
package/routes/viewedit.js
CHANGED
|
@@ -98,6 +98,13 @@ const view_dropdown = (view, req) =>
|
|
|
98
98
|
'<i class="far fa-copy"></i> ' + req.__("Duplicate"),
|
|
99
99
|
req
|
|
100
100
|
),
|
|
101
|
+
a(
|
|
102
|
+
{
|
|
103
|
+
class: "dropdown-item",
|
|
104
|
+
href: `javascript:ajax_modal('/admin/snapshot-restore/view/${view.name}')`,
|
|
105
|
+
},
|
|
106
|
+
'<i class="fas fa-undo-alt"></i> ' + req.__("Restore")
|
|
107
|
+
),
|
|
101
108
|
div({ class: "dropdown-divider" }),
|
|
102
109
|
post_dropdown_item(
|
|
103
110
|
`/viewedit/delete/${view.id}`,
|
|
@@ -355,7 +362,7 @@ router.get(
|
|
|
355
362
|
}
|
|
356
363
|
const tables = await Table.find_with_external();
|
|
357
364
|
const currentTable = tables.find(
|
|
358
|
-
(t) => t.id === viewrow.table_id || t.name === viewrow.exttable_name
|
|
365
|
+
(t) => (t.id && t.id === viewrow.table_id) || t.name === viewrow.exttable_name
|
|
359
366
|
);
|
|
360
367
|
viewrow.table_name = currentTable && currentTable.name;
|
|
361
368
|
if (viewrow.slug && currentTable) {
|
package/serve.js
CHANGED
|
@@ -30,7 +30,7 @@ const { getConfig } = require("@saltcorn/data/models/config");
|
|
|
30
30
|
const { migrate } = require("@saltcorn/data/migrate");
|
|
31
31
|
const socketio = require("socket.io");
|
|
32
32
|
const { createAdapter, setupPrimary } = require("@socket.io/cluster-adapter");
|
|
33
|
-
const { setTenant, getSessionStore } = require("./routes/utils");
|
|
33
|
+
const { setTenant, getSessionStore, get_tenant_from_req } = require("./routes/utils");
|
|
34
34
|
const passport = require("passport");
|
|
35
35
|
const { authenticate } = require("passport");
|
|
36
36
|
const View = require("@saltcorn/data/models/view");
|
|
@@ -44,6 +44,11 @@ const {
|
|
|
44
44
|
getAllTenants,
|
|
45
45
|
} = require("@saltcorn/admin-models/models/tenant");
|
|
46
46
|
const { auto_backup_now } = require("@saltcorn/admin-models/models/backup");
|
|
47
|
+
const Snapshot = require("@saltcorn/admin-models/models/snapshot");
|
|
48
|
+
|
|
49
|
+
const take_snapshot = async () => {
|
|
50
|
+
return await Snapshot.take_if_changed();
|
|
51
|
+
};
|
|
47
52
|
|
|
48
53
|
// helpful https://gist.github.com/jpoehls/2232358
|
|
49
54
|
/**
|
|
@@ -132,32 +137,33 @@ const workerDispatchMsg = ({ tenant, ...msg }) => {
|
|
|
132
137
|
*/
|
|
133
138
|
const onMessageFromWorker =
|
|
134
139
|
(masterState, { port, watchReaper, disableScheduler, pid }) =>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
140
|
+
(msg) => {
|
|
141
|
+
//console.log("worker msg", typeof msg, msg);
|
|
142
|
+
if (msg === "Start" && !masterState.started) {
|
|
143
|
+
masterState.started = true;
|
|
144
|
+
runScheduler({
|
|
145
|
+
port,
|
|
146
|
+
watchReaper,
|
|
147
|
+
disableScheduler,
|
|
148
|
+
eachTenant,
|
|
149
|
+
auto_backup_now,
|
|
150
|
+
take_snapshot,
|
|
151
|
+
});
|
|
152
|
+
require("./systemd")({ port });
|
|
153
|
+
return true;
|
|
154
|
+
} else if (msg === "RestartServer") {
|
|
155
|
+
process.exit(0);
|
|
156
|
+
return true;
|
|
157
|
+
} else if (msg.tenant || msg.createTenant) {
|
|
158
|
+
///ie from saltcorn
|
|
159
|
+
//broadcast
|
|
160
|
+
Object.entries(cluster.workers).forEach(([wpid, w]) => {
|
|
161
|
+
if (wpid !== pid) w.send(msg);
|
|
162
|
+
});
|
|
163
|
+
workerDispatchMsg(msg); //also master
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
161
167
|
|
|
162
168
|
module.exports =
|
|
163
169
|
/**
|
|
@@ -274,6 +280,7 @@ module.exports =
|
|
|
274
280
|
disableScheduler,
|
|
275
281
|
eachTenant,
|
|
276
282
|
auto_backup_now,
|
|
283
|
+
take_snapshot,
|
|
277
284
|
});
|
|
278
285
|
}
|
|
279
286
|
Trigger.emitEvent("Startup");
|
|
@@ -345,24 +352,33 @@ const setupSocket = (...servers) => {
|
|
|
345
352
|
io.attach(server);
|
|
346
353
|
}
|
|
347
354
|
|
|
348
|
-
io.use(wrap(setTenant));
|
|
355
|
+
//io.use(wrap(setTenant));
|
|
349
356
|
io.use(wrap(getSessionStore()));
|
|
350
357
|
io.use(wrap(passport.initialize()));
|
|
351
358
|
io.use(wrap(passport.authenticate(["jwt", "session"])));
|
|
352
359
|
if (process.send && !cluster.isMaster) io.adapter(createAdapter());
|
|
353
|
-
getState().setRoomEmitter((viewname, room_id, msg) => {
|
|
354
|
-
io.to(`${viewname}_${room_id}`).emit("message", msg);
|
|
360
|
+
getState().setRoomEmitter((tenant, viewname, room_id, msg) => {
|
|
361
|
+
io.to(`${tenant}_${viewname}_${room_id}`).emit("message", msg);
|
|
355
362
|
});
|
|
356
363
|
io.on("connection", (socket) => {
|
|
357
364
|
socket.on("join_room", ([viewname, room_id]) => {
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
.
|
|
362
|
-
.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
365
|
+
const ten = get_tenant_from_req(socket.request) || "public";
|
|
366
|
+
const f = () => {
|
|
367
|
+
try {
|
|
368
|
+
const view = View.findOne({ name: viewname });
|
|
369
|
+
if (view.viewtemplateObj.authorize_join) {
|
|
370
|
+
view.viewtemplateObj
|
|
371
|
+
.authorize_join(view.configuration, room_id, socket.request.user)
|
|
372
|
+
.then((authorized) => {
|
|
373
|
+
if (authorized) socket.join(`${ten}_${viewname}_${room_id}`);
|
|
374
|
+
});
|
|
375
|
+
} else socket.join(`${ten}_${viewname}_${room_id}`);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
getState().log(1, `Socket join_room error: ${err.stack}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (ten && ten !== "public") db.runWithTenant(ten, f);
|
|
381
|
+
else f();
|
|
366
382
|
});
|
|
367
383
|
});
|
|
368
384
|
};
|
package/tests/api.test.js
CHANGED
|
@@ -84,6 +84,23 @@ describe("API read", () => {
|
|
|
84
84
|
)
|
|
85
85
|
);
|
|
86
86
|
});
|
|
87
|
+
it("should handle fkey args ", async () => {
|
|
88
|
+
const loginCookie = await getAdminLoginCookie();
|
|
89
|
+
const app = await getApp({ disableCsrf: true });
|
|
90
|
+
await request(app)
|
|
91
|
+
.get("/api/patients/?favbook=1")
|
|
92
|
+
.set("Cookie", loginCookie)
|
|
93
|
+
.expect(succeedJsonWith((rows) => rows.length == 1));
|
|
94
|
+
});
|
|
95
|
+
it("should handle fkey args with no value", async () => {
|
|
96
|
+
const loginCookie = await getAdminLoginCookie();
|
|
97
|
+
const app = await getApp({ disableCsrf: true });
|
|
98
|
+
await request(app)
|
|
99
|
+
.get("/api/patients/?favbook=")
|
|
100
|
+
.set("Cookie", loginCookie)
|
|
101
|
+
.expect(succeedJsonWith((rows) => rows.length == 0));
|
|
102
|
+
});
|
|
103
|
+
|
|
87
104
|
it("should get books for public with search and one field", async () => {
|
|
88
105
|
const app = await getApp({ disableCsrf: true });
|
|
89
106
|
await request(app)
|
package/tests/clientjs.test.js
CHANGED
|
@@ -34,8 +34,18 @@ test("updateQueryStringParameter", () => {
|
|
|
34
34
|
expect(removeQueryStringParameter("/foo?name=Bar&age=45", "age")).toBe(
|
|
35
35
|
"/foo?name=Bar"
|
|
36
36
|
);
|
|
37
|
+
expect(
|
|
38
|
+
updateQueryStringParameter("/foo", "publisher.publisher->name", "AK")
|
|
39
|
+
).toBe("/foo?publisher.publisher->name=AK");
|
|
40
|
+
expect(
|
|
41
|
+
updateQueryStringParameter(
|
|
42
|
+
"/foo?publisher.publisher->name=AB",
|
|
43
|
+
"publisher.publisher->name",
|
|
44
|
+
"AK"
|
|
45
|
+
)
|
|
46
|
+
).toBe("/foo?publisher.publisher->name=AK");
|
|
37
47
|
});
|
|
38
|
-
|
|
48
|
+
//publisher.publisher->name
|
|
39
49
|
test("updateQueryStringParameter hash", () => {
|
|
40
50
|
expect(updateQueryStringParameter("/foo#baz", "age", 43)).toBe(
|
|
41
51
|
"/foo?age=43#baz"
|