@saltcorn/server 0.9.6-beta.8 → 0.9.6
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 +9 -2
- package/auth/admin.js +51 -52
- package/auth/roleadmin.js +6 -2
- package/auth/routes.js +28 -10
- package/auth/testhelp.js +86 -0
- package/help/Field label.tmd +11 -0
- package/help/Field types.tmd +39 -0
- package/help/Inclusion Formula.tmd +38 -0
- package/help/Ownership field.tmd +76 -0
- package/help/Ownership formula.tmd +75 -0
- package/help/Table roles.tmd +20 -0
- package/help/User groups.tmd +35 -0
- package/help/User roles.tmd +30 -0
- package/load_plugins.js +28 -4
- package/locales/en.json +28 -1
- package/locales/it.json +3 -2
- package/markup/forms.js +5 -1
- package/package.json +9 -9
- package/public/log_viewer_utils.js +32 -0
- package/public/mermaid.min.js +705 -306
- package/public/saltcorn-builder.css +23 -0
- package/public/saltcorn-common.js +195 -71
- package/public/saltcorn.css +72 -0
- package/public/saltcorn.js +78 -0
- package/restart_watcher.js +1 -0
- package/routes/actions.js +27 -0
- package/routes/admin.js +180 -66
- package/routes/api.js +6 -0
- package/routes/common_lists.js +42 -32
- package/routes/fields.js +9 -1
- package/routes/homepage.js +2 -0
- package/routes/menu.js +69 -4
- package/routes/notifications.js +90 -10
- package/routes/pageedit.js +18 -13
- package/routes/plugins.js +5 -1
- package/routes/search.js +10 -4
- package/routes/tables.js +47 -27
- package/routes/tenant.js +4 -15
- package/routes/utils.js +20 -6
- package/routes/viewedit.js +11 -7
- package/serve.js +27 -5
- package/tests/edit.test.js +426 -0
- package/tests/fields.test.js +21 -0
- package/tests/filter.test.js +68 -0
- package/tests/page.test.js +2 -2
- package/tests/sync.test.js +59 -0
- package/wrapper.js +4 -1
package/routes/notifications.js
CHANGED
|
@@ -13,7 +13,7 @@ const { getState } = require("@saltcorn/data/db/state");
|
|
|
13
13
|
const Form = require("@saltcorn/data/models/form");
|
|
14
14
|
const File = require("@saltcorn/data/models/file");
|
|
15
15
|
const User = require("@saltcorn/data/models/user");
|
|
16
|
-
const { renderForm } = require("@saltcorn/markup");
|
|
16
|
+
const { renderForm, post_btn } = require("@saltcorn/markup");
|
|
17
17
|
|
|
18
18
|
const router = new Router();
|
|
19
19
|
module.exports = router;
|
|
@@ -31,22 +31,50 @@ router.get(
|
|
|
31
31
|
"/",
|
|
32
32
|
loggedIn,
|
|
33
33
|
error_catcher(async (req, res) => {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
const { after } = req.query;
|
|
35
|
+
const where = { user_id: req.user.id };
|
|
36
|
+
if (after) where.id = { lt: after };
|
|
37
|
+
const nots = await Notification.find(where, {
|
|
38
|
+
orderBy: "id",
|
|
39
|
+
orderDesc: true,
|
|
40
|
+
limit: 20,
|
|
41
|
+
});
|
|
38
42
|
await Notification.mark_as_read({
|
|
39
43
|
id: { in: nots.filter((n) => !n.read).map((n) => n.id) },
|
|
40
44
|
});
|
|
45
|
+
const form = notificationSettingsForm();
|
|
46
|
+
const user = await User.findOne({ id: req.user?.id });
|
|
47
|
+
form.values = { notify_email: user?._attributes?.notify_email };
|
|
41
48
|
const notifyCards = nots.length
|
|
42
49
|
? nots.map((not) => ({
|
|
43
50
|
type: "card",
|
|
44
51
|
class: [!not.read && "unread-notify"],
|
|
52
|
+
id: `notify-${not.id}`,
|
|
45
53
|
contents: [
|
|
46
54
|
div(
|
|
47
55
|
{ class: "d-flex" },
|
|
48
56
|
span({ class: "fw-bold" }, not.title),
|
|
49
|
-
span(
|
|
57
|
+
span(
|
|
58
|
+
{
|
|
59
|
+
class: "ms-2 text-muted",
|
|
60
|
+
title: not.created.toLocaleString(req.getLocale()),
|
|
61
|
+
},
|
|
62
|
+
moment(not.created).fromNow()
|
|
63
|
+
),
|
|
64
|
+
div(
|
|
65
|
+
{ class: "ms-auto" },
|
|
66
|
+
post_btn(
|
|
67
|
+
`/notifications/delete/${not.id}`,
|
|
68
|
+
"",
|
|
69
|
+
req.csrfToken(),
|
|
70
|
+
{
|
|
71
|
+
icon: "fas fa-times-circle",
|
|
72
|
+
klass: "btn-link text-muted text-decoration-none p-0",
|
|
73
|
+
ajax: true,
|
|
74
|
+
onClick: `$('#notify-${not.id}').remove()`,
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
)
|
|
50
78
|
),
|
|
51
79
|
not.body && p(not.body),
|
|
52
80
|
not.link && a({ href: not.link }, "Link"),
|
|
@@ -58,6 +86,35 @@ router.get(
|
|
|
58
86
|
contents: [h5(req.__("No notifications"))],
|
|
59
87
|
},
|
|
60
88
|
];
|
|
89
|
+
const pageLinks = div(
|
|
90
|
+
{ class: "d-flex mt-3 mb-3" },
|
|
91
|
+
nots.length == 20
|
|
92
|
+
? div(
|
|
93
|
+
after &&
|
|
94
|
+
a(
|
|
95
|
+
{ href: `/notifications`, class: "me-2" },
|
|
96
|
+
"← " + req.__("Newest")
|
|
97
|
+
),
|
|
98
|
+
a(
|
|
99
|
+
{ href: `/notifications?after=${nots[19].id}` },
|
|
100
|
+
req.__("Older") + " →"
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
: div(),
|
|
104
|
+
nots.length > 0 &&
|
|
105
|
+
div(
|
|
106
|
+
{ class: "ms-auto" },
|
|
107
|
+
post_btn(
|
|
108
|
+
`/notifications/delete/read`,
|
|
109
|
+
req.__("Delete all read"),
|
|
110
|
+
req.csrfToken(),
|
|
111
|
+
{
|
|
112
|
+
icon: "fas fa-trash",
|
|
113
|
+
klass: "btn-sm btn-danger",
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
);
|
|
61
118
|
res.sendWrap(req.__("Notifications"), {
|
|
62
119
|
above: [
|
|
63
120
|
{
|
|
@@ -72,10 +129,10 @@ router.get(
|
|
|
72
129
|
type: "card",
|
|
73
130
|
contents: [
|
|
74
131
|
req.__("Receive notifications by:"),
|
|
75
|
-
renderForm(
|
|
132
|
+
renderForm(form, req.csrfToken()),
|
|
76
133
|
],
|
|
77
134
|
},
|
|
78
|
-
{ above: notifyCards },
|
|
135
|
+
{ above: [...notifyCards, pageLinks] },
|
|
79
136
|
],
|
|
80
137
|
},
|
|
81
138
|
],
|
|
@@ -109,9 +166,27 @@ router.post(
|
|
|
109
166
|
})
|
|
110
167
|
);
|
|
111
168
|
|
|
169
|
+
router.post(
|
|
170
|
+
"/delete/:idlike",
|
|
171
|
+
loggedIn,
|
|
172
|
+
error_catcher(async (req, res) => {
|
|
173
|
+
const { idlike } = req.params;
|
|
174
|
+
if (idlike == "read") {
|
|
175
|
+
await Notification.deleteRead(req.user.id);
|
|
176
|
+
} else {
|
|
177
|
+
const id = +idlike;
|
|
178
|
+
const notif = await Notification.findOne({ id });
|
|
179
|
+
if (notif?.user_id == req.user?.id) await notif.delete();
|
|
180
|
+
}
|
|
181
|
+
if (req.xhr) res.json({ success: "ok" });
|
|
182
|
+
else res.redirect("/notifications");
|
|
183
|
+
})
|
|
184
|
+
);
|
|
185
|
+
|
|
112
186
|
router.get(
|
|
113
|
-
"/manifest.json",
|
|
187
|
+
"/manifest.json:opt_cache_bust?",
|
|
114
188
|
error_catcher(async (req, res) => {
|
|
189
|
+
const { pretty } = req.query;
|
|
115
190
|
const state = getState();
|
|
116
191
|
const manifest = {
|
|
117
192
|
name: state.getConfig("site_name"),
|
|
@@ -142,6 +217,11 @@ router.get(
|
|
|
142
217
|
"red"
|
|
143
218
|
);
|
|
144
219
|
}
|
|
145
|
-
res.json(manifest);
|
|
220
|
+
if (!pretty) res.json(manifest);
|
|
221
|
+
else {
|
|
222
|
+
const prettyJson = JSON.stringify(manifest, null, 2);
|
|
223
|
+
res.setHeader("Content-Type", "application/json");
|
|
224
|
+
res.send(prettyJson);
|
|
225
|
+
}
|
|
146
226
|
})
|
|
147
227
|
);
|
package/routes/pageedit.js
CHANGED
|
@@ -94,7 +94,7 @@ const pagePropertiesForm = async (req, isNew) => {
|
|
|
94
94
|
if (groups.includes(s) && isNew)
|
|
95
95
|
return req.__("A page group with this name already exists");
|
|
96
96
|
},
|
|
97
|
-
sublabel: req.__("A short name that will be in
|
|
97
|
+
sublabel: req.__("A short name that will be in the page URL"),
|
|
98
98
|
type: "String",
|
|
99
99
|
attributes: { autofocus: true },
|
|
100
100
|
}),
|
|
@@ -107,13 +107,15 @@ const pagePropertiesForm = async (req, isNew) => {
|
|
|
107
107
|
new Field({
|
|
108
108
|
label: req.__("Description"),
|
|
109
109
|
name: "description",
|
|
110
|
-
sublabel: req.__(
|
|
110
|
+
sublabel: req.__(
|
|
111
|
+
"A longer description that is not visible but appears in the page header and is indexed by search engines"
|
|
112
|
+
),
|
|
111
113
|
input_type: "text",
|
|
112
114
|
}),
|
|
113
115
|
{
|
|
114
116
|
name: "min_role",
|
|
115
117
|
label: req.__("Minimum role"),
|
|
116
|
-
sublabel: req.__("
|
|
118
|
+
sublabel: req.__("User role required to access page"),
|
|
117
119
|
input_type: "select",
|
|
118
120
|
options: roles.map((r) => ({ value: r.id, label: r.role })),
|
|
119
121
|
},
|
|
@@ -336,6 +338,15 @@ router.get(
|
|
|
336
338
|
)
|
|
337
339
|
),
|
|
338
340
|
},
|
|
341
|
+
{
|
|
342
|
+
type: "card",
|
|
343
|
+
title: req.__("Root pages"),
|
|
344
|
+
titleAjaxIndicator: true,
|
|
345
|
+
contents: renderForm(
|
|
346
|
+
getRootPageForm(pages, pageGroups, roles, req),
|
|
347
|
+
req.csrfToken()
|
|
348
|
+
),
|
|
349
|
+
},
|
|
339
350
|
{
|
|
340
351
|
type: "card",
|
|
341
352
|
title: req.__("Your page groups"),
|
|
@@ -357,15 +368,6 @@ router.get(
|
|
|
357
368
|
)
|
|
358
369
|
),
|
|
359
370
|
},
|
|
360
|
-
{
|
|
361
|
-
type: "card",
|
|
362
|
-
title: req.__("Root pages"),
|
|
363
|
-
titleAjaxIndicator: true,
|
|
364
|
-
contents: renderForm(
|
|
365
|
-
getRootPageForm(pages, pageGroups, roles, req),
|
|
366
|
-
req.csrfToken()
|
|
367
|
-
),
|
|
368
|
-
},
|
|
369
371
|
],
|
|
370
372
|
});
|
|
371
373
|
})
|
|
@@ -392,6 +394,7 @@ const wrap = (contents, noCard, req, page) => ({
|
|
|
392
394
|
{
|
|
393
395
|
type: noCard ? "container" : "card",
|
|
394
396
|
title: page ? page.name : req.__("New"),
|
|
397
|
+
titleAjaxIndicator: true,
|
|
395
398
|
contents,
|
|
396
399
|
},
|
|
397
400
|
],
|
|
@@ -418,6 +421,7 @@ router.get(
|
|
|
418
421
|
form.hidden("id");
|
|
419
422
|
form.values = page;
|
|
420
423
|
form.values.no_menu = page.attributes?.no_menu;
|
|
424
|
+
form.onChange = `saveAndContinue(this)`;
|
|
421
425
|
res.sendWrap(
|
|
422
426
|
req.__(`Page attributes`),
|
|
423
427
|
wrap(renderForm(form, req.csrfToken()), false, req, page)
|
|
@@ -477,7 +481,8 @@ router.post(
|
|
|
477
481
|
pageRow.layout = {};
|
|
478
482
|
}
|
|
479
483
|
await Page.update(+id, pageRow);
|
|
480
|
-
res.
|
|
484
|
+
if (req.xhr) res.json({ success: "ok" });
|
|
485
|
+
else res.redirect(`/pageedit/`);
|
|
481
486
|
} else {
|
|
482
487
|
if (!pageRow.layout) pageRow.layout = {};
|
|
483
488
|
if (!pageRow.fixed_states) pageRow.fixed_states = {};
|
package/routes/plugins.js
CHANGED
|
@@ -865,9 +865,13 @@ router.get(
|
|
|
865
865
|
if (!module) {
|
|
866
866
|
module = getState().plugins[getState().plugin_module_names[plugin.name]];
|
|
867
867
|
}
|
|
868
|
+
const userLayout =
|
|
869
|
+
user._attributes?.layout?.plugin === plugin.name
|
|
870
|
+
? user._attributes.layout.config || {}
|
|
871
|
+
: {};
|
|
868
872
|
const form = await module.user_config_form({
|
|
869
873
|
...(plugin.configuration || {}),
|
|
870
|
-
...
|
|
874
|
+
...userLayout,
|
|
871
875
|
});
|
|
872
876
|
form.action = `/plugins/user_configure/${encodeURIComponent(plugin.name)}`;
|
|
873
877
|
form.onChange = `applyViewConfig(this, '/plugins/user_saveconfig/${encodeURIComponent(
|
package/routes/search.js
CHANGED
|
@@ -202,7 +202,8 @@ const runSearch = async ({ q, _page, table }, req, res) => {
|
|
|
202
202
|
let tablesWithResults = [];
|
|
203
203
|
let tablesConfigured = 0;
|
|
204
204
|
for (const [tableName, viewName] of Object.entries(cfg)) {
|
|
205
|
-
if (!viewName || viewName === "")
|
|
205
|
+
if (!viewName || viewName === "" || viewName === "search_table_description")
|
|
206
|
+
continue;
|
|
206
207
|
tablesConfigured += 1;
|
|
207
208
|
if (table && tableName !== table) continue;
|
|
208
209
|
let sectionHeader = tableName;
|
|
@@ -232,7 +233,7 @@ const runSearch = async ({ q, _page, table }, req, res) => {
|
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
if (vresps.length > 0) {
|
|
235
|
-
tablesWithResults.push(tableName);
|
|
236
|
+
tablesWithResults.push({ tableName, label: sectionHeader });
|
|
236
237
|
resp.push({
|
|
237
238
|
type: "card",
|
|
238
239
|
title: span({ id: tableName }, sectionHeader),
|
|
@@ -273,8 +274,13 @@ const runSearch = async ({ q, _page, table }, req, res) => {
|
|
|
273
274
|
req.__("Show only matches in table:"),
|
|
274
275
|
" ",
|
|
275
276
|
tablesWithResults
|
|
276
|
-
.map((
|
|
277
|
-
a(
|
|
277
|
+
.map(({ tableName, label }) =>
|
|
278
|
+
a(
|
|
279
|
+
{
|
|
280
|
+
href: `javascript:set_state_field('table', '${tableName}')`,
|
|
281
|
+
},
|
|
282
|
+
label
|
|
283
|
+
)
|
|
278
284
|
)
|
|
279
285
|
.join(" | ")
|
|
280
286
|
)
|
package/routes/tables.js
CHANGED
|
@@ -93,14 +93,45 @@ const tableForm = async (table, req) => {
|
|
|
93
93
|
noSubmitButton: true,
|
|
94
94
|
onChange: "saveAndContinue(this)",
|
|
95
95
|
fields: [
|
|
96
|
+
{
|
|
97
|
+
label: req.__("Minimum role to read"),
|
|
98
|
+
sublabel: req.__(
|
|
99
|
+
"User must have this role or higher to read rows from the table, unless they are the owner"
|
|
100
|
+
),
|
|
101
|
+
help: {
|
|
102
|
+
topic: "Table roles",
|
|
103
|
+
context: {},
|
|
104
|
+
},
|
|
105
|
+
name: "min_role_read",
|
|
106
|
+
input_type: "select",
|
|
107
|
+
options: roleOptions,
|
|
108
|
+
attributes: { asideNext: !table.external && !table.provider_name },
|
|
109
|
+
},
|
|
96
110
|
...(!table.external && !table.provider_name
|
|
97
111
|
? [
|
|
112
|
+
{
|
|
113
|
+
label: req.__("Minimum role to write"),
|
|
114
|
+
name: "min_role_write",
|
|
115
|
+
input_type: "select",
|
|
116
|
+
help: {
|
|
117
|
+
topic: "Table roles",
|
|
118
|
+
context: {},
|
|
119
|
+
},
|
|
120
|
+
sublabel: req.__(
|
|
121
|
+
"User must have this role or higher to edit or create new rows in the table, unless they are the owner"
|
|
122
|
+
),
|
|
123
|
+
options: roleOptions,
|
|
124
|
+
},
|
|
98
125
|
{
|
|
99
126
|
label: req.__("Ownership field"),
|
|
100
127
|
name: "ownership_field_id",
|
|
101
128
|
sublabel: req.__(
|
|
102
129
|
"The user referred to in this field will be the owner of the row"
|
|
103
130
|
),
|
|
131
|
+
help: {
|
|
132
|
+
topic: "Ownership field",
|
|
133
|
+
context: {},
|
|
134
|
+
},
|
|
104
135
|
input_type: "select",
|
|
105
136
|
options: [
|
|
106
137
|
{ value: "", label: req.__("None") },
|
|
@@ -114,6 +145,10 @@ const tableForm = async (table, req) => {
|
|
|
114
145
|
validator: expressionValidator,
|
|
115
146
|
type: "String",
|
|
116
147
|
class: "validate-expression",
|
|
148
|
+
help: {
|
|
149
|
+
topic: "Ownership formula",
|
|
150
|
+
context: {},
|
|
151
|
+
},
|
|
117
152
|
sublabel:
|
|
118
153
|
req.__("User is treated as owner if true. In scope: ") +
|
|
119
154
|
["user", ...fields.map((f) => f.name)]
|
|
@@ -126,6 +161,10 @@ const tableForm = async (table, req) => {
|
|
|
126
161
|
sublabel: req.__(
|
|
127
162
|
"Add relations to this table in dropdown options for ownership field"
|
|
128
163
|
),
|
|
164
|
+
help: {
|
|
165
|
+
topic: "User groups",
|
|
166
|
+
context: {},
|
|
167
|
+
},
|
|
129
168
|
name: "is_user_group",
|
|
130
169
|
type: "Bool",
|
|
131
170
|
},
|
|
@@ -141,28 +180,9 @@ const tableForm = async (table, req) => {
|
|
|
141
180
|
),
|
|
142
181
|
//options: roleOptions,
|
|
143
182
|
},
|
|
144
|
-
{
|
|
145
|
-
label: req.__("Minimum role to read"),
|
|
146
|
-
sublabel: req.__(
|
|
147
|
-
"User must have this role or higher to read rows from the table, unless they are the owner"
|
|
148
|
-
),
|
|
149
|
-
name: "min_role_read",
|
|
150
|
-
input_type: "select",
|
|
151
|
-
options: roleOptions,
|
|
152
|
-
attributes: { asideNext: !table.external && !table.provider_name },
|
|
153
|
-
},
|
|
154
183
|
...(table.external || table.provider_name
|
|
155
184
|
? []
|
|
156
185
|
: [
|
|
157
|
-
{
|
|
158
|
-
label: req.__("Minimum role to write"),
|
|
159
|
-
name: "min_role_write",
|
|
160
|
-
input_type: "select",
|
|
161
|
-
sublabel: req.__(
|
|
162
|
-
"User must have this role or higher to edit or create new rows in the table, unless they are the owner"
|
|
163
|
-
),
|
|
164
|
-
options: roleOptions,
|
|
165
|
-
},
|
|
166
186
|
{
|
|
167
187
|
label: req.__("Version history"),
|
|
168
188
|
sublabel: req.__("Track table data changes over time"),
|
|
@@ -754,6 +774,14 @@ router.get(
|
|
|
754
774
|
r.typename +
|
|
755
775
|
span({ class: "badge bg-danger ms-1" }, "Unknown type"),
|
|
756
776
|
},
|
|
777
|
+
...(table.external
|
|
778
|
+
? []
|
|
779
|
+
: [
|
|
780
|
+
{
|
|
781
|
+
label: req.__("Edit"),
|
|
782
|
+
key: (r) => link(`/field/${r.id}`, req.__("Edit")),
|
|
783
|
+
},
|
|
784
|
+
]),
|
|
757
785
|
{
|
|
758
786
|
label: "",
|
|
759
787
|
key: (r) => typeBadges(r, req),
|
|
@@ -763,14 +791,6 @@ router.get(
|
|
|
763
791
|
key: (r) => attribBadges(r),
|
|
764
792
|
},
|
|
765
793
|
{ label: req.__("Variable name"), key: (t) => code(t.name) },
|
|
766
|
-
...(table.external
|
|
767
|
-
? []
|
|
768
|
-
: [
|
|
769
|
-
{
|
|
770
|
-
label: req.__("Edit"),
|
|
771
|
-
key: (r) => link(`/field/${r.id}`, req.__("Edit")),
|
|
772
|
-
},
|
|
773
|
-
]),
|
|
774
794
|
...(table.external || db.isSQLite
|
|
775
795
|
? []
|
|
776
796
|
: [
|
package/routes/tenant.js
CHANGED
|
@@ -44,7 +44,7 @@ const {
|
|
|
44
44
|
const db = require("@saltcorn/data/db");
|
|
45
45
|
|
|
46
46
|
const { loadAllPlugins, loadAndSaveNewPlugin } = require("../load_plugins");
|
|
47
|
-
const { isAdmin, error_catcher } = require("./utils.js");
|
|
47
|
+
const { isAdmin, error_catcher, is_ip_address } = require("./utils.js");
|
|
48
48
|
const User = require("@saltcorn/data/models/user");
|
|
49
49
|
const File = require("@saltcorn/data/models/file");
|
|
50
50
|
const {
|
|
@@ -117,22 +117,10 @@ const create_tenant_allowed = (req) => {
|
|
|
117
117
|
return user_role <= required_role;
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
-
/**
|
|
121
|
-
* Check that String is IPv4 address
|
|
122
|
-
* @param {string} hostname
|
|
123
|
-
* @returns {boolean|string[]}
|
|
124
|
-
*/
|
|
125
|
-
// TBD not sure that false is correct return if type of is not string
|
|
126
|
-
// TBD Add IPv6 support
|
|
127
|
-
const is_ip_address = (hostname) => {
|
|
128
|
-
if (typeof hostname !== "string") return false;
|
|
129
|
-
return hostname.split(".").every((s) => +s >= 0 && +s <= 255);
|
|
130
|
-
};
|
|
131
|
-
|
|
132
120
|
const get_cfg_tenant_base_url = (req) =>
|
|
133
121
|
remove_leading_chars(
|
|
134
122
|
".",
|
|
135
|
-
getRootState().getConfig("tenant_baseurl", req.hostname)
|
|
123
|
+
getRootState().getConfig("tenant_baseurl", req.hostname) || req.hostname
|
|
136
124
|
)
|
|
137
125
|
.replace("http://", "")
|
|
138
126
|
.replace("https://", "");
|
|
@@ -268,7 +256,8 @@ router.post(
|
|
|
268
256
|
return;
|
|
269
257
|
}
|
|
270
258
|
// declare ui form
|
|
271
|
-
const
|
|
259
|
+
const base_url = get_cfg_tenant_base_url(req);
|
|
260
|
+
const form = tenant_form(req, base_url);
|
|
272
261
|
// validate ui form
|
|
273
262
|
const valres = form.validate(req.body);
|
|
274
263
|
if (valres.errors)
|
package/routes/utils.js
CHANGED
|
@@ -77,11 +77,9 @@ function loggedIn(req, res, next) {
|
|
|
77
77
|
* @returns {void}
|
|
78
78
|
*/
|
|
79
79
|
function isAdmin(req, res, next) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
req.user.tenant === db.getTenantSchema()
|
|
84
|
-
) {
|
|
80
|
+
const cur_tenant = db.getTenantSchema();
|
|
81
|
+
//console.log({ cur_tenant, user: req.user });
|
|
82
|
+
if (req.user && req.user.role_id === 1 && req.user.tenant === cur_tenant) {
|
|
85
83
|
next();
|
|
86
84
|
} else {
|
|
87
85
|
req.flash("danger", req.__("Must be admin"));
|
|
@@ -157,6 +155,8 @@ const get_tenant_from_req = (req, hostPartsOffset) => {
|
|
|
157
155
|
if (req.subdomains && req.subdomains.length == 0)
|
|
158
156
|
return db.connectObj.default_schema;
|
|
159
157
|
if (!req.subdomains && req.headers.host) {
|
|
158
|
+
if (is_ip_address(req.headers.host.split(":")[0]))
|
|
159
|
+
return db.connectObj.default_schema;
|
|
160
160
|
const parts = req.headers.host.split(".");
|
|
161
161
|
if (parts.length < (!hostPartsOffset ? 3 : 3 - hostPartsOffset))
|
|
162
162
|
return db.connectObj.default_schema;
|
|
@@ -299,7 +299,7 @@ const getGitRevision = () => db.connectObj.git_commit;
|
|
|
299
299
|
* Gets session store
|
|
300
300
|
* @returns {session|cookieSession}
|
|
301
301
|
*/
|
|
302
|
-
const getSessionStore = () => {
|
|
302
|
+
const getSessionStore = (pruneInterval) => {
|
|
303
303
|
/*if (getState().getConfig("cookie_sessions", false)) {
|
|
304
304
|
return cookieSession({
|
|
305
305
|
keys: [db.connectObj.session_secret || is.str.generate()],
|
|
@@ -322,6 +322,7 @@ const getSessionStore = () => {
|
|
|
322
322
|
schemaName: db.connectObj.default_schema,
|
|
323
323
|
pool: db.pool,
|
|
324
324
|
tableName: "_sc_session",
|
|
325
|
+
pruneSessionInterval: pruneInterval > 0 ? pruneInterval : false,
|
|
325
326
|
}),
|
|
326
327
|
secret: db.connectObj.session_secret || is.str.generate(),
|
|
327
328
|
resave: false,
|
|
@@ -350,6 +351,18 @@ const is_relative_url = (url) => {
|
|
|
350
351
|
return typeof url === "string" && !url.includes(":/") && !url.includes("//");
|
|
351
352
|
};
|
|
352
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Check that String is IPv4 address
|
|
356
|
+
* @param {string} hostname
|
|
357
|
+
* @returns {boolean|string[]}
|
|
358
|
+
*/
|
|
359
|
+
// TBD not sure that false is correct return if type of is not string
|
|
360
|
+
// TBD Add IPv6 support
|
|
361
|
+
const is_ip_address = (hostname) => {
|
|
362
|
+
if (typeof hostname !== "string") return false;
|
|
363
|
+
return hostname.split(".").every((s) => +s >= 0 && +s <= 255);
|
|
364
|
+
};
|
|
365
|
+
|
|
353
366
|
const admin_config_route = ({
|
|
354
367
|
router,
|
|
355
368
|
path,
|
|
@@ -558,6 +571,7 @@ module.exports = {
|
|
|
558
571
|
get_tenant_from_req,
|
|
559
572
|
addOnDoneRedirect,
|
|
560
573
|
is_relative_url,
|
|
574
|
+
is_ip_address,
|
|
561
575
|
get_sys_info,
|
|
562
576
|
admin_config_route,
|
|
563
577
|
sendHtmlFile,
|
package/routes/viewedit.js
CHANGED
|
@@ -371,6 +371,7 @@ router.get(
|
|
|
371
371
|
const form = await viewForm(req, tableOptions, roles, pages, viewrow);
|
|
372
372
|
const inbound_connected = await viewrow.inbound_connected_objects();
|
|
373
373
|
form.hidden("id");
|
|
374
|
+
form.onChange = `saveAndContinue(this)`;
|
|
374
375
|
res.sendWrap(req.__(`Edit view`), {
|
|
375
376
|
above: [
|
|
376
377
|
{
|
|
@@ -383,6 +384,7 @@ router.get(
|
|
|
383
384
|
{
|
|
384
385
|
type: "card",
|
|
385
386
|
class: "mt-0",
|
|
387
|
+
titleAjaxIndicator: true,
|
|
386
388
|
title: req.__(
|
|
387
389
|
`%s view - %s on %s`,
|
|
388
390
|
viewname,
|
|
@@ -541,12 +543,14 @@ router.post(
|
|
|
541
543
|
//console.log(v);
|
|
542
544
|
await View.create(v);
|
|
543
545
|
}
|
|
544
|
-
res.
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
546
|
+
if (req.xhr) res.json({ success: "ok" });
|
|
547
|
+
else
|
|
548
|
+
res.redirect(
|
|
549
|
+
addOnDoneRedirect(
|
|
550
|
+
`/viewedit/config/${encodeURIComponent(v.name)}`,
|
|
551
|
+
req
|
|
552
|
+
)
|
|
553
|
+
);
|
|
550
554
|
}
|
|
551
555
|
} else {
|
|
552
556
|
sendForm(form);
|
|
@@ -902,7 +906,7 @@ router.post(
|
|
|
902
906
|
? `/${req.query.on_done_redirect}`
|
|
903
907
|
: "/viewedit";
|
|
904
908
|
res.redirect(redirectTarget);
|
|
905
|
-
} else res.json({
|
|
909
|
+
} else res.json({ success: "ok" });
|
|
906
910
|
})
|
|
907
911
|
);
|
|
908
912
|
|
package/serve.js
CHANGED
|
@@ -235,6 +235,10 @@ module.exports =
|
|
|
235
235
|
: defaultNCPUs;
|
|
236
236
|
|
|
237
237
|
const letsEncrypt = await getConfig("letsencrypt", false);
|
|
238
|
+
const pruneSessionInterval = +(await getConfig(
|
|
239
|
+
"prune_session_interval",
|
|
240
|
+
900
|
|
241
|
+
));
|
|
238
242
|
const masterState = {
|
|
239
243
|
started: false,
|
|
240
244
|
listeningTo: new Set([]),
|
|
@@ -287,7 +291,11 @@ module.exports =
|
|
|
287
291
|
})
|
|
288
292
|
.ready((glx) => {
|
|
289
293
|
const httpsServer = glx.httpsServer();
|
|
290
|
-
setupSocket(
|
|
294
|
+
setupSocket(
|
|
295
|
+
appargs?.subdomainOffset,
|
|
296
|
+
pruneSessionInterval,
|
|
297
|
+
httpsServer
|
|
298
|
+
);
|
|
291
299
|
httpsServer.setTimeout(timeout * 1000);
|
|
292
300
|
process.on("message", workerDispatchMsg);
|
|
293
301
|
glx.serveApp(app);
|
|
@@ -344,6 +352,10 @@ const nonGreenlockWorkerSetup = async (appargs, port) => {
|
|
|
344
352
|
const cert = getState().getConfig("custom_ssl_certificate", "");
|
|
345
353
|
const key = getState().getConfig("custom_ssl_private_key", "");
|
|
346
354
|
const timeout = +getState().getConfig("timeout", 120);
|
|
355
|
+
const pruneSessionInterval = +(await getState().getConfig(
|
|
356
|
+
"prune_session_interval",
|
|
357
|
+
900
|
|
358
|
+
));
|
|
347
359
|
// Server with http on port 80 / https on 443
|
|
348
360
|
// todo resolve hardcode
|
|
349
361
|
if (port === 80 && cert && key) {
|
|
@@ -354,7 +366,12 @@ const nonGreenlockWorkerSetup = async (appargs, port) => {
|
|
|
354
366
|
// todo timeout to config
|
|
355
367
|
httpServer.setTimeout(timeout * 1000);
|
|
356
368
|
httpsServer.setTimeout(timeout * 1000);
|
|
357
|
-
setupSocket(
|
|
369
|
+
setupSocket(
|
|
370
|
+
appargs?.subdomainOffset,
|
|
371
|
+
pruneSessionInterval,
|
|
372
|
+
httpServer,
|
|
373
|
+
httpsServer
|
|
374
|
+
);
|
|
358
375
|
httpServer.listen(port, () => {
|
|
359
376
|
console.log("HTTP Server running on port 80");
|
|
360
377
|
});
|
|
@@ -367,7 +384,7 @@ const nonGreenlockWorkerSetup = async (appargs, port) => {
|
|
|
367
384
|
// server with http only
|
|
368
385
|
const http = require("http");
|
|
369
386
|
const httpServer = http.createServer(app);
|
|
370
|
-
setupSocket(appargs?.subdomainOffset, httpServer);
|
|
387
|
+
setupSocket(appargs?.subdomainOffset, pruneSessionInterval, httpServer);
|
|
371
388
|
|
|
372
389
|
// todo timeout to config
|
|
373
390
|
// todo refer in doc to httpserver doc
|
|
@@ -384,7 +401,7 @@ const nonGreenlockWorkerSetup = async (appargs, port) => {
|
|
|
384
401
|
*
|
|
385
402
|
* @param {...*} servers
|
|
386
403
|
*/
|
|
387
|
-
const setupSocket = (subdomainOffset, ...servers) => {
|
|
404
|
+
const setupSocket = (subdomainOffset, pruneSessionInterval, ...servers) => {
|
|
388
405
|
// https://socket.io/docs/v4/middlewares/
|
|
389
406
|
const wrap = (middleware) => (socket, next) =>
|
|
390
407
|
middleware(socket.request, {}, next);
|
|
@@ -395,7 +412,7 @@ const setupSocket = (subdomainOffset, ...servers) => {
|
|
|
395
412
|
}
|
|
396
413
|
|
|
397
414
|
const passportInit = passport.initialize();
|
|
398
|
-
const sessionStore = getSessionStore();
|
|
415
|
+
const sessionStore = getSessionStore(pruneSessionInterval);
|
|
399
416
|
const setupNamespace = (namespace) => {
|
|
400
417
|
//io.of(namespace).use(wrap(setTenant));
|
|
401
418
|
io.of(namespace).use(wrap(sessionStore));
|
|
@@ -452,6 +469,11 @@ const setupSocket = (subdomainOffset, ...servers) => {
|
|
|
452
469
|
socketIds.push(socket.id);
|
|
453
470
|
await getState().setConfig("joined_log_socket_ids", [...socketIds]);
|
|
454
471
|
callback({ status: "ok" });
|
|
472
|
+
setTimeout(() => {
|
|
473
|
+
io.of("/")
|
|
474
|
+
.to(`_logs_${tenant}_`)
|
|
475
|
+
.emit("test_conn_msg", { text: "test message" });
|
|
476
|
+
}, 1000);
|
|
455
477
|
}
|
|
456
478
|
} catch (err) {
|
|
457
479
|
getState().log(1, `Socket join_logs stream: ${err.stack}`);
|