@saltcorn/server 0.7.4-beta.3 → 0.8.0-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/app.js +43 -22
- package/auth/admin.js +173 -74
- package/auth/routes.js +67 -28
- package/locales/en.json +54 -2
- package/locales/es.json +134 -134
- package/locales/ru.json +32 -5
- package/markup/admin.js +40 -38
- package/markup/forms.js +4 -3
- package/package.json +8 -7
- package/public/diagram_utils.js +530 -0
- package/public/gridedit.js +4 -1
- package/public/jquery-menu-editor.min.js +112 -112
- package/public/saltcorn-common.js +114 -26
- package/public/saltcorn.css +27 -10
- package/public/saltcorn.js +223 -76
- package/restart_watcher.js +1 -0
- package/routes/actions.js +20 -6
- package/routes/admin.js +243 -82
- package/routes/api.js +19 -2
- package/routes/common_lists.js +137 -134
- package/routes/diagram.js +362 -35
- package/routes/fields.js +4 -1
- package/routes/files.js +137 -101
- package/routes/homepage.js +2 -2
- package/routes/infoarch.js +2 -2
- package/routes/list.js +4 -4
- package/routes/page.js +16 -3
- package/routes/pageedit.js +22 -14
- package/routes/scapi.js +1 -1
- package/routes/search.js +1 -1
- package/routes/tables.js +4 -5
- package/routes/tag_entries.js +31 -10
- package/routes/tags.js +36 -32
- package/routes/tenant.js +98 -36
- package/routes/utils.js +72 -20
- package/routes/view.js +0 -1
- package/routes/viewedit.js +55 -22
- package/serve.js +5 -0
- package/tests/admin.test.js +2 -0
- package/tests/auth.test.js +20 -0
- package/tests/files.test.js +11 -20
- package/tests/tenant.test.js +4 -2
package/routes/viewedit.js
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
post_dropdown_item,
|
|
17
17
|
renderBuilder,
|
|
18
18
|
settingsDropdown,
|
|
19
|
+
alert,
|
|
19
20
|
} = require("@saltcorn/markup");
|
|
20
21
|
const {
|
|
21
22
|
//span,
|
|
@@ -26,11 +27,13 @@ const {
|
|
|
26
27
|
a,
|
|
27
28
|
div,
|
|
28
29
|
//button,
|
|
30
|
+
script,
|
|
29
31
|
text,
|
|
32
|
+
domReady,
|
|
30
33
|
} = require("@saltcorn/markup/tags");
|
|
31
34
|
|
|
32
35
|
const { getState } = require("@saltcorn/data/db/state");
|
|
33
|
-
const { isAdmin, error_catcher } = require("./utils.js");
|
|
36
|
+
const { isAdmin, error_catcher, addOnDoneRedirect } = require("./utils.js");
|
|
34
37
|
const { setTableRefs, viewsList } = require("./common_lists");
|
|
35
38
|
const Form = require("@saltcorn/data/models/form");
|
|
36
39
|
const Field = require("@saltcorn/data/models/field");
|
|
@@ -39,7 +42,6 @@ const View = require("@saltcorn/data/models/view");
|
|
|
39
42
|
const Workflow = require("@saltcorn/data/models/workflow");
|
|
40
43
|
const User = require("@saltcorn/data/models/user");
|
|
41
44
|
const Page = require("@saltcorn/data/models/page");
|
|
42
|
-
const Tag = require("@saltcorn/data/models/tag");
|
|
43
45
|
const db = require("@saltcorn/data/db");
|
|
44
46
|
|
|
45
47
|
const { add_to_menu } = require("@saltcorn/admin-models/models/pack");
|
|
@@ -76,6 +78,26 @@ router.get(
|
|
|
76
78
|
|
|
77
79
|
const viewMarkup = await viewsList(views, req);
|
|
78
80
|
const tables = await Table.find();
|
|
81
|
+
const viewAccessWarning = (view) => {
|
|
82
|
+
const table = tables.find((t) => t.name === view.table);
|
|
83
|
+
if (!table) return false;
|
|
84
|
+
if (table.ownership_field_id || table.ownership_formula) return false;
|
|
85
|
+
|
|
86
|
+
return table.min_role_read < view.min_role;
|
|
87
|
+
};
|
|
88
|
+
const hasAccessWarning = views.filter(viewAccessWarning);
|
|
89
|
+
const accessWarning =
|
|
90
|
+
hasAccessWarning.length > 0
|
|
91
|
+
? alert(
|
|
92
|
+
"danger",
|
|
93
|
+
`<p>You have views with a role to access lower than the table role to read,
|
|
94
|
+
with no table ownership. In the next version of Saltcorn, this may cause a
|
|
95
|
+
denial of access. Users will need to have table read access to any data displayed.</p>
|
|
96
|
+
Views potentially affected: ${hasAccessWarning
|
|
97
|
+
.map((v) => v.name)
|
|
98
|
+
.join(", ")}`
|
|
99
|
+
)
|
|
100
|
+
: "";
|
|
79
101
|
res.sendWrap(req.__(`Views`), {
|
|
80
102
|
above: [
|
|
81
103
|
{
|
|
@@ -87,17 +109,18 @@ router.get(
|
|
|
87
109
|
class: "mt-0",
|
|
88
110
|
title: req.__("Your views"),
|
|
89
111
|
contents: [
|
|
112
|
+
accessWarning,
|
|
90
113
|
viewMarkup,
|
|
91
114
|
tables.length > 0
|
|
92
115
|
? a(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
116
|
+
{ href: `/viewedit/new`, class: "btn btn-primary" },
|
|
117
|
+
req.__("Create view")
|
|
118
|
+
)
|
|
96
119
|
: p(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
req.__(
|
|
121
|
+
"You must create at least one table before you can create views."
|
|
122
|
+
)
|
|
123
|
+
),
|
|
101
124
|
],
|
|
102
125
|
},
|
|
103
126
|
],
|
|
@@ -129,7 +152,7 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
|
|
|
129
152
|
.map(([k, v]) => k);
|
|
130
153
|
const slugOptions = await Table.allSlugOptions();
|
|
131
154
|
return new Form({
|
|
132
|
-
action: "/viewedit/save",
|
|
155
|
+
action: addOnDoneRedirect("/viewedit/save", req),
|
|
133
156
|
submitLabel: req.__("Configure") + " »",
|
|
134
157
|
blurb: req.__("First, please give some basic information about the view."),
|
|
135
158
|
fields: [
|
|
@@ -337,7 +360,6 @@ router.post(
|
|
|
337
360
|
const pages = await Page.find();
|
|
338
361
|
const form = await viewForm(req, tableOptions, roles, pages);
|
|
339
362
|
const result = form.validate(req.body);
|
|
340
|
-
|
|
341
363
|
const sendForm = (form) => {
|
|
342
364
|
res.sendWrap(req.__(`Edit view`), {
|
|
343
365
|
above: [
|
|
@@ -397,7 +419,12 @@ router.post(
|
|
|
397
419
|
else v.configuration = {};
|
|
398
420
|
await View.create(v);
|
|
399
421
|
}
|
|
400
|
-
res.redirect(
|
|
422
|
+
res.redirect(
|
|
423
|
+
addOnDoneRedirect(
|
|
424
|
+
`/viewedit/config/${encodeURIComponent(v.name)}`,
|
|
425
|
+
req
|
|
426
|
+
)
|
|
427
|
+
);
|
|
401
428
|
}
|
|
402
429
|
} else {
|
|
403
430
|
sendForm(form);
|
|
@@ -414,7 +441,7 @@ router.post(
|
|
|
414
441
|
* @returns {void}
|
|
415
442
|
*/
|
|
416
443
|
const respondWorkflow = (view, wf, wfres, req, res) => {
|
|
417
|
-
const wrap = (contents, noCard) => ({
|
|
444
|
+
const wrap = (contents, noCard, previewURL) => ({
|
|
418
445
|
above: [
|
|
419
446
|
{
|
|
420
447
|
type: "breadcrumbs",
|
|
@@ -430,6 +457,12 @@ const respondWorkflow = (view, wf, wfres, req, res) => {
|
|
|
430
457
|
title: wfres.title,
|
|
431
458
|
contents,
|
|
432
459
|
},
|
|
460
|
+
...previewURL ? [{
|
|
461
|
+
type: "card",
|
|
462
|
+
title: req.__("Preview"),
|
|
463
|
+
contents: div({ id: "viewcfg-preview", "data-preview-url": previewURL },
|
|
464
|
+
script(domReady(`updateViewPreview()`))),
|
|
465
|
+
}] : []
|
|
433
466
|
],
|
|
434
467
|
});
|
|
435
468
|
if (wfres.flash) req.flash(wfres.flash[0], wfres.flash[1]);
|
|
@@ -452,7 +485,7 @@ const respondWorkflow = (view, wf, wfres, req, res) => {
|
|
|
452
485
|
},
|
|
453
486
|
],
|
|
454
487
|
},
|
|
455
|
-
wrap(renderForm(wfres.renderForm, req.csrfToken()))
|
|
488
|
+
wrap(renderForm(wfres.renderForm, req.csrfToken()), false, wfres.previewURL)
|
|
456
489
|
);
|
|
457
490
|
else if (wfres.renderBuilder) {
|
|
458
491
|
wfres.renderBuilder.options.view_id = view.id;
|
|
@@ -666,14 +699,14 @@ router.post(
|
|
|
666
699
|
const view = await View.findOne({ id });
|
|
667
700
|
const roles = await User.get_roles();
|
|
668
701
|
const roleRow = roles.find((r) => r.id === +role);
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
req.__(`Minimum role
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
res.
|
|
702
|
+
const message =
|
|
703
|
+
roleRow && view
|
|
704
|
+
? req.__(`Minimum role for %s updated to %s`, view.name, roleRow.role)
|
|
705
|
+
: req.__(`Minimum role updated`);
|
|
706
|
+
if (!req.xhr) {
|
|
707
|
+
req.flash("success", message);
|
|
708
|
+
res.redirect("/viewedit");
|
|
709
|
+
} else res.json({ okay: true, responseText: message });
|
|
677
710
|
})
|
|
678
711
|
);
|
|
679
712
|
|
package/serve.js
CHANGED
|
@@ -99,11 +99,16 @@ const workerDispatchMsg = ({ tenant, ...msg }) => {
|
|
|
99
99
|
db.runWithTenant(tenant, () => workerDispatchMsg(msg));
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
|
+
|
|
102
103
|
if (msg.refresh_plugin_cfg) {
|
|
103
104
|
Plugin.findOne({ name: msg.refresh_plugin_cfg }).then((plugin) => {
|
|
104
105
|
if (plugin) loadPlugin(plugin);
|
|
105
106
|
});
|
|
106
107
|
}
|
|
108
|
+
if (!getState()) {
|
|
109
|
+
console.error("no State for tenant", tenant)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
107
112
|
if (msg.refresh) getState()[`refresh_${msg.refresh}`](true);
|
|
108
113
|
if (msg.createTenant) {
|
|
109
114
|
const tenant_template = getState().getConfig("tenant_template");
|
package/tests/admin.test.js
CHANGED
|
@@ -68,9 +68,11 @@ describe("admin page", () => {
|
|
|
68
68
|
.expect(toInclude("Site identity settings"));
|
|
69
69
|
});
|
|
70
70
|
adminPageContains([
|
|
71
|
+
["/admin", "Site identity"],
|
|
71
72
|
["/admin/backup", "Download a backup"],
|
|
72
73
|
["/admin/email", "Email settings"],
|
|
73
74
|
["/admin/system", "Restart server"],
|
|
75
|
+
["/admin/dev", "Development"],
|
|
74
76
|
]);
|
|
75
77
|
adminPageContains([
|
|
76
78
|
["/useradmin", "Create user"],
|
package/tests/auth.test.js
CHANGED
|
@@ -18,6 +18,7 @@ const { getState } = require("@saltcorn/data/db/state");
|
|
|
18
18
|
const { get_reset_link, generate_email } = require("../auth/resetpw");
|
|
19
19
|
const i18n = require("i18n");
|
|
20
20
|
const path = require("path");
|
|
21
|
+
const fs = require("fs")
|
|
21
22
|
|
|
22
23
|
afterAll(db.close);
|
|
23
24
|
beforeAll(async () => {
|
|
@@ -602,3 +603,22 @@ describe("signup with custom login form", () => {
|
|
|
602
603
|
expect(userrow.height).toBe(15);
|
|
603
604
|
});
|
|
604
605
|
});
|
|
606
|
+
|
|
607
|
+
describe("Locale files", () => {
|
|
608
|
+
it("should be valid JSON", async () => {
|
|
609
|
+
|
|
610
|
+
const localeFiles =
|
|
611
|
+
await fs.promises.readdir(path.join(__dirname, "..", "/locales"));
|
|
612
|
+
expect(localeFiles.length).toBeGreaterThan(3)
|
|
613
|
+
expect(localeFiles).toContain("en.json")
|
|
614
|
+
for (const fnm of localeFiles) {
|
|
615
|
+
const conts = await fs.promises.readFile(
|
|
616
|
+
path.join(__dirname, "..", "/locales", fnm)
|
|
617
|
+
)
|
|
618
|
+
expect(conts.length).toBeGreaterThan(1)
|
|
619
|
+
|
|
620
|
+
const j = JSON.parse(conts)
|
|
621
|
+
expect(Object.keys(j).length).toBeGreaterThan(1)
|
|
622
|
+
}
|
|
623
|
+
})
|
|
624
|
+
})
|
package/tests/files.test.js
CHANGED
|
@@ -39,22 +39,13 @@ describe("files admin", () => {
|
|
|
39
39
|
await request(app)
|
|
40
40
|
.get("/files")
|
|
41
41
|
.set("Cookie", loginCookie)
|
|
42
|
-
.expect(toInclude("
|
|
42
|
+
.expect(toInclude("Upload file"));
|
|
43
43
|
});
|
|
44
44
|
it("download file", async () => {
|
|
45
45
|
const app = await getApp({ disableCsrf: true });
|
|
46
46
|
const loginCookie = await getStaffLoginCookie();
|
|
47
47
|
await request(app)
|
|
48
|
-
.get("/files/download/
|
|
49
|
-
.set("Cookie", loginCookie)
|
|
50
|
-
.expect(toSucceed());
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("serve file", async () => {
|
|
54
|
-
const app = await getApp({ disableCsrf: true });
|
|
55
|
-
const loginCookie = await getStaffLoginCookie();
|
|
56
|
-
await request(app)
|
|
57
|
-
.get("/files/serve/2")
|
|
48
|
+
.get("/files/download/rick.png")
|
|
58
49
|
.set("Cookie", loginCookie)
|
|
59
50
|
.expect(toSucceed());
|
|
60
51
|
});
|
|
@@ -71,38 +62,38 @@ describe("files admin", () => {
|
|
|
71
62
|
const app = await getApp({ disableCsrf: true });
|
|
72
63
|
const loginCookie = await getStaffLoginCookie();
|
|
73
64
|
await request(app)
|
|
74
|
-
.get("/files/serve/
|
|
65
|
+
.get("/files/serve/missingfile.foo")
|
|
75
66
|
.set("Cookie", loginCookie)
|
|
76
67
|
.expect(404);
|
|
77
68
|
});
|
|
78
69
|
it("not serve file to public", async () => {
|
|
79
70
|
const app = await getApp({ disableCsrf: true });
|
|
80
|
-
await request(app).get("/files/serve/
|
|
71
|
+
await request(app).get("/files/serve/rick.png").expect(404);
|
|
81
72
|
});
|
|
82
73
|
it("not download file to public", async () => {
|
|
83
74
|
const app = await getApp({ disableCsrf: true });
|
|
84
|
-
await request(app).get("/files/download/
|
|
75
|
+
await request(app).get("/files/download/rick.png").expect(404);
|
|
85
76
|
});
|
|
86
77
|
it("set file min role", async () => {
|
|
87
78
|
const app = await getApp({ disableCsrf: true });
|
|
88
79
|
const loginCookie = await getAdminLoginCookie();
|
|
89
80
|
await request(app)
|
|
90
|
-
.post("/files/setrole/
|
|
81
|
+
.post("/files/setrole/rick.png")
|
|
91
82
|
.set("Cookie", loginCookie)
|
|
92
83
|
.send("role=10")
|
|
93
|
-
.expect(toRedirect("/files"));
|
|
84
|
+
.expect(toRedirect("/files?dir=."));
|
|
94
85
|
});
|
|
95
86
|
it("serve file to public after role change", async () => {
|
|
96
87
|
const app = await getApp({ disableCsrf: true });
|
|
97
|
-
await request(app).get("/files/serve/
|
|
88
|
+
await request(app).get("/files/serve/rick.png").expect(toSucceed());
|
|
98
89
|
});
|
|
99
90
|
it("delete file", async () => {
|
|
100
91
|
const app = await getApp({ disableCsrf: true });
|
|
101
92
|
const loginCookie = await getAdminLoginCookie();
|
|
102
93
|
await request(app)
|
|
103
|
-
.post("/files/delete/
|
|
94
|
+
.post("/files/delete/rick.png")
|
|
104
95
|
.set("Cookie", loginCookie)
|
|
105
|
-
.expect(toRedirect("/files"));
|
|
96
|
+
.expect(toRedirect("/files?dir=."));
|
|
106
97
|
});
|
|
107
98
|
it("upload file", async () => {
|
|
108
99
|
const app = await getApp({ disableCsrf: true });
|
|
@@ -112,7 +103,7 @@ describe("files admin", () => {
|
|
|
112
103
|
.set("Cookie", loginCookie)
|
|
113
104
|
.attach("file", Buffer.from("helloiamasmallfile", "utf-8"))
|
|
114
105
|
|
|
115
|
-
.expect(toRedirect("/files"));
|
|
106
|
+
.expect(toRedirect("/files?dir=."));
|
|
116
107
|
});
|
|
117
108
|
});
|
|
118
109
|
describe("files edit", () => {
|
package/tests/tenant.test.js
CHANGED
|
@@ -5,14 +5,16 @@ const getApp = require("../app");
|
|
|
5
5
|
const {
|
|
6
6
|
toRedirect,
|
|
7
7
|
getAdminLoginCookie,
|
|
8
|
-
getStaffLoginCookie,
|
|
8
|
+
//getStaffLoginCookie,
|
|
9
9
|
itShouldRedirectUnauthToLogin,
|
|
10
10
|
toInclude,
|
|
11
|
-
toNotInclude,
|
|
11
|
+
//toNotInclude,
|
|
12
12
|
} = require("../auth/testhelp");
|
|
13
13
|
const { getState } = require("@saltcorn/data/db/state");
|
|
14
14
|
|
|
15
15
|
afterAll(db.close);
|
|
16
|
+
jest.setTimeout(10000);
|
|
17
|
+
|
|
16
18
|
|
|
17
19
|
beforeAll(async () => {
|
|
18
20
|
if (!db.isSQLite) {
|