@saltcorn/server 1.1.0-beta.0 → 1.1.0-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/auth/roleadmin.js +10 -2
- package/locales/en.json +5 -1
- package/package.json +9 -9
- package/public/codemirror.css +33 -0
- package/public/gridedit.js +1 -1
- package/public/saltcorn-common.js +5 -4
- package/public/saltcorn.css +1 -4
- package/routes/admin.js +116 -5
- package/routes/eventlog.js +5 -1
- package/routes/homepage.js +13 -3
- package/routes/list.js +9 -1
- package/routes/page.js +1 -0
- package/routes/pageedit.js +16 -2
- package/routes/tenant.js +81 -2
- package/routes/utils.js +15 -0
- package/tests/plugin_install.test.js +7 -7
- package/tests/plugins.test.js +4 -4
- package/wrapper.js +3 -1
package/auth/roleadmin.js
CHANGED
|
@@ -56,7 +56,11 @@ const editRoleLayoutForm = (role, layouts, layout_by_role, req) => {
|
|
|
56
56
|
},
|
|
57
57
|
csrfField(req),
|
|
58
58
|
select(
|
|
59
|
-
{
|
|
59
|
+
{
|
|
60
|
+
name: "layout",
|
|
61
|
+
onchange: "form.submit()",
|
|
62
|
+
class: "form-select form-select-sm w-unset d-inline",
|
|
63
|
+
},
|
|
60
64
|
layouts.map((layout, ix) =>
|
|
61
65
|
option(
|
|
62
66
|
{
|
|
@@ -88,7 +92,11 @@ const editRole2FAPolicyForm = (role, twofa_policy_by_role, req) =>
|
|
|
88
92
|
},
|
|
89
93
|
csrfField(req),
|
|
90
94
|
select(
|
|
91
|
-
{
|
|
95
|
+
{
|
|
96
|
+
name: "policy",
|
|
97
|
+
onchange: "form.submit()",
|
|
98
|
+
class: "form-select form-select-sm w-unset d-inline",
|
|
99
|
+
},
|
|
92
100
|
["Optional", "Disabled", "Mandatory"].map((p) =>
|
|
93
101
|
option({ selected: twofa_policy_by_role[role.id] === p }, p)
|
|
94
102
|
)
|
package/locales/en.json
CHANGED
|
@@ -1490,5 +1490,9 @@
|
|
|
1490
1490
|
"Login and signup views should be accessible by public users": "Login and signup views should be accessible by public users",
|
|
1491
1491
|
"Shared: %s": "Shared: %s",
|
|
1492
1492
|
"Sharing not enabled": "Sharing not enabled",
|
|
1493
|
-
"You must be logged in to share": "You must be logged in to share"
|
|
1493
|
+
"You must be logged in to share": "You must be logged in to share",
|
|
1494
|
+
"Fluid layout": "Fluid layout",
|
|
1495
|
+
"Request fluid layout from theme for a wider display for this page": "Request fluid layout from theme for a wider display for this page",
|
|
1496
|
+
"Location of view to create new row": "Location of view to create new row",
|
|
1497
|
+
"Default locale": "Default locale"
|
|
1494
1498
|
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "1.1.0-beta.
|
|
3
|
+
"version": "1.1.0-beta.10",
|
|
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
9
|
"@aws-sdk/client-s3": "^3.451.0",
|
|
10
|
-
"@saltcorn/base-plugin": "1.1.0-beta.
|
|
11
|
-
"@saltcorn/builder": "1.1.0-beta.
|
|
12
|
-
"@saltcorn/data": "1.1.0-beta.
|
|
13
|
-
"@saltcorn/admin-models": "1.1.0-beta.
|
|
14
|
-
"@saltcorn/filemanager": "1.1.0-beta.
|
|
15
|
-
"@saltcorn/markup": "1.1.0-beta.
|
|
16
|
-
"@saltcorn/plugins-loader": "1.1.0-beta.
|
|
17
|
-
"@saltcorn/sbadmin2": "1.1.0-beta.
|
|
10
|
+
"@saltcorn/base-plugin": "1.1.0-beta.10",
|
|
11
|
+
"@saltcorn/builder": "1.1.0-beta.10",
|
|
12
|
+
"@saltcorn/data": "1.1.0-beta.10",
|
|
13
|
+
"@saltcorn/admin-models": "1.1.0-beta.10",
|
|
14
|
+
"@saltcorn/filemanager": "1.1.0-beta.10",
|
|
15
|
+
"@saltcorn/markup": "1.1.0-beta.10",
|
|
16
|
+
"@saltcorn/plugins-loader": "1.1.0-beta.10",
|
|
17
|
+
"@saltcorn/sbadmin2": "1.1.0-beta.10",
|
|
18
18
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
19
19
|
"@socket.io/sticky": "^1.0.1",
|
|
20
20
|
"adm-zip": "0.5.10",
|
package/public/codemirror.css
CHANGED
|
@@ -348,3 +348,36 @@ div.CodeMirror-dragcursors {
|
|
|
348
348
|
|
|
349
349
|
/* Help users use markselection to safely style text background */
|
|
350
350
|
span.CodeMirror-selectedtext { background: none; }
|
|
351
|
+
|
|
352
|
+
/* Port of TextMate's Blackboard theme */
|
|
353
|
+
|
|
354
|
+
.cm-s-blackboard.CodeMirror { background: #0C1021; color: #F8F8F8; }
|
|
355
|
+
.cm-s-blackboard div.CodeMirror-selected { background: #253B76; }
|
|
356
|
+
.cm-s-blackboard .CodeMirror-line::selection, .cm-s-blackboard .CodeMirror-line > span::selection, .cm-s-blackboard .CodeMirror-line > span > span::selection { background: rgba(37, 59, 118, .99); }
|
|
357
|
+
.cm-s-blackboard .CodeMirror-line::-moz-selection, .cm-s-blackboard .CodeMirror-line > span::-moz-selection, .cm-s-blackboard .CodeMirror-line > span > span::-moz-selection { background: rgba(37, 59, 118, .99); }
|
|
358
|
+
.cm-s-blackboard .CodeMirror-gutters { background: #0C1021; border-right: 0; }
|
|
359
|
+
.cm-s-blackboard .CodeMirror-guttermarker { color: #FBDE2D; }
|
|
360
|
+
.cm-s-blackboard .CodeMirror-guttermarker-subtle { color: #888; }
|
|
361
|
+
.cm-s-blackboard .CodeMirror-linenumber { color: #888; }
|
|
362
|
+
.cm-s-blackboard .CodeMirror-cursor { border-left: 1px solid #A7A7A7; }
|
|
363
|
+
|
|
364
|
+
.cm-s-blackboard .cm-keyword { color: #FBDE2D; }
|
|
365
|
+
.cm-s-blackboard .cm-atom { color: #D8FA3C; }
|
|
366
|
+
.cm-s-blackboard .cm-number { color: #D8FA3C; }
|
|
367
|
+
.cm-s-blackboard .cm-def { color: #8DA6CE; }
|
|
368
|
+
.cm-s-blackboard .cm-variable { color: #FF6400; }
|
|
369
|
+
.cm-s-blackboard .cm-operator { color: #FBDE2D; }
|
|
370
|
+
.cm-s-blackboard .cm-comment { color: #AEAEAE; }
|
|
371
|
+
.cm-s-blackboard .cm-string { color: #61CE3C; }
|
|
372
|
+
.cm-s-blackboard .cm-string-2 { color: #61CE3C; }
|
|
373
|
+
.cm-s-blackboard .cm-meta { color: #D8FA3C; }
|
|
374
|
+
.cm-s-blackboard .cm-builtin { color: #8DA6CE; }
|
|
375
|
+
.cm-s-blackboard .cm-tag { color: #8DA6CE; }
|
|
376
|
+
.cm-s-blackboard .cm-attribute { color: #8DA6CE; }
|
|
377
|
+
.cm-s-blackboard .cm-header { color: #FF6400; }
|
|
378
|
+
.cm-s-blackboard .cm-hr { color: #AEAEAE; }
|
|
379
|
+
.cm-s-blackboard .cm-link { color: #8DA6CE; }
|
|
380
|
+
.cm-s-blackboard .cm-error { background: #9D1E15; color: #F8F8F8; }
|
|
381
|
+
|
|
382
|
+
.cm-s-blackboard .CodeMirror-activeline-background { background: #3C3636; }
|
|
383
|
+
.cm-s-blackboard .CodeMirror-matchingbracket { outline:1px solid grey;color:white !important; }
|
package/public/gridedit.js
CHANGED
|
@@ -28,7 +28,7 @@ function flatpickerEditor(cell, onRendered, success, cancel, editorParams) {
|
|
|
28
28
|
enableTime: !dayOnly,
|
|
29
29
|
dateFormat: dayOnly ? "Y-m-d" : "Z",
|
|
30
30
|
time_24hr: true,
|
|
31
|
-
locale: "en",
|
|
31
|
+
locale: window._sc_locale || "en",
|
|
32
32
|
defaultDate,
|
|
33
33
|
onClose: function (selectedDates, dateStr, instance) {
|
|
34
34
|
evt = window.event;
|
|
@@ -1064,11 +1064,12 @@ function initialize_page() {
|
|
|
1064
1064
|
codes.forEach((el) => {
|
|
1065
1065
|
//console.log($(el).attr("mode"), el);
|
|
1066
1066
|
if ($(el).hasClass("codemirror-enabled")) return;
|
|
1067
|
-
|
|
1068
|
-
const cm = CodeMirror.fromTextArea(el, {
|
|
1067
|
+
const cmOpts = {
|
|
1069
1068
|
lineNumbers: true,
|
|
1070
1069
|
mode: $(el).attr("mode"),
|
|
1071
|
-
}
|
|
1070
|
+
};
|
|
1071
|
+
if (_sc_lightmode === "dark") cmOpts.theme = "blackboard";
|
|
1072
|
+
const cm = CodeMirror.fromTextArea(el, cmOpts);
|
|
1072
1073
|
$(el).addClass("codemirror-enabled");
|
|
1073
1074
|
cm.on(
|
|
1074
1075
|
"change",
|
|
@@ -1550,7 +1551,7 @@ async function common_done(res, viewnameOrElem, isWeb = true) {
|
|
|
1550
1551
|
});
|
|
1551
1552
|
}
|
|
1552
1553
|
if (res.eval_js) await handle(res.eval_js, eval_it);
|
|
1553
|
-
|
|
1554
|
+
if (res.goto) {
|
|
1554
1555
|
if (!isWeb) {
|
|
1555
1556
|
const next = new URL(res.goto, "http://localhost");
|
|
1556
1557
|
const pathname = next.pathname;
|
package/public/saltcorn.css
CHANGED
|
@@ -85,18 +85,15 @@ div[data-inline-edit-dest-url]:hover .editicon {
|
|
|
85
85
|
border-left: none;
|
|
86
86
|
border-color: #95a5a6;
|
|
87
87
|
padding-left: 0.3rem;
|
|
88
|
-
background-color: #ffffff;
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
.search-bar input[type="search"] {
|
|
92
91
|
border-color: #95a5a6;
|
|
93
92
|
padding-left: 0.3rem;
|
|
94
|
-
background-color: #ffffff;
|
|
95
93
|
}
|
|
96
94
|
|
|
97
95
|
.search-bar button.search-bar {
|
|
98
96
|
border-color: #95a5a6;
|
|
99
|
-
background-color: #ffffff;
|
|
100
97
|
border-left: 1px solid #95a5a6 !important;
|
|
101
98
|
border-top: 1px solid #95a5a6 !important;
|
|
102
99
|
border-bottom: 1px solid #95a5a6 !important;
|
|
@@ -358,7 +355,7 @@ table.table-inner-grid td {
|
|
|
358
355
|
}
|
|
359
356
|
|
|
360
357
|
.w-unset {
|
|
361
|
-
width: unset;
|
|
358
|
+
width: unset !important;
|
|
362
359
|
}
|
|
363
360
|
|
|
364
361
|
.preview-text {
|
package/routes/admin.js
CHANGED
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
setTenant,
|
|
13
13
|
admin_config_route,
|
|
14
14
|
get_sys_info,
|
|
15
|
+
tenant_letsencrypt_name,
|
|
15
16
|
} = require("./utils.js");
|
|
16
17
|
const Table = require("@saltcorn/data/models/table");
|
|
17
18
|
const Plugin = require("@saltcorn/data/models/plugin");
|
|
@@ -90,7 +91,10 @@ const {
|
|
|
90
91
|
} = require("../markup/admin.js");
|
|
91
92
|
const packagejson = require("../package.json");
|
|
92
93
|
const Form = require("@saltcorn/data/models/form");
|
|
93
|
-
const {
|
|
94
|
+
const {
|
|
95
|
+
get_latest_npm_version,
|
|
96
|
+
isFixedConfig,
|
|
97
|
+
} = require("@saltcorn/data/models/config");
|
|
94
98
|
const { getMailTransport } = require("@saltcorn/data/models/email");
|
|
95
99
|
const {
|
|
96
100
|
getBaseDomain,
|
|
@@ -151,6 +155,7 @@ admin_config_route({
|
|
|
151
155
|
field_names: [
|
|
152
156
|
"site_name",
|
|
153
157
|
"timezone",
|
|
158
|
+
"default_locale",
|
|
154
159
|
"base_url",
|
|
155
160
|
...(getConfigFile() ? ["multitenancy_enabled"] : []),
|
|
156
161
|
{ section_header: "Logo image" },
|
|
@@ -530,6 +535,7 @@ router.get(
|
|
|
530
535
|
{},
|
|
531
536
|
{ orderBy: "created", orderDesc: true, fields: ["id", "created", "hash"] }
|
|
532
537
|
);
|
|
538
|
+
const locale = getState().getConfig("default_locale", "en");
|
|
533
539
|
send_admin_page({
|
|
534
540
|
res,
|
|
535
541
|
req,
|
|
@@ -551,9 +557,11 @@ router.get(
|
|
|
551
557
|
)}`,
|
|
552
558
|
target: "_blank",
|
|
553
559
|
},
|
|
554
|
-
`${localeDateTime(
|
|
555
|
-
snap.created
|
|
556
|
-
|
|
560
|
+
`${localeDateTime(
|
|
561
|
+
snap.created,
|
|
562
|
+
{},
|
|
563
|
+
locale
|
|
564
|
+
)} (${moment(snap.created).fromNow()})`
|
|
557
565
|
)
|
|
558
566
|
)
|
|
559
567
|
)
|
|
@@ -591,6 +599,7 @@ router.get(
|
|
|
591
599
|
error_catcher(async (req, res) => {
|
|
592
600
|
const { type, name } = req.params;
|
|
593
601
|
const snaps = await Snapshot.entity_history(type, name);
|
|
602
|
+
const locale = getState().getConfig("default_locale", "en");
|
|
594
603
|
res.set("Page-Title", `Restore ${text(name)}`);
|
|
595
604
|
res.send(
|
|
596
605
|
mkTable(
|
|
@@ -598,7 +607,9 @@ router.get(
|
|
|
598
607
|
{
|
|
599
608
|
label: req.__("When"),
|
|
600
609
|
key: (r) =>
|
|
601
|
-
`${localeDateTime(r.created)} (${moment(
|
|
610
|
+
`${localeDateTime(r.created, {}, locale)} (${moment(
|
|
611
|
+
r.created
|
|
612
|
+
).fromNow()})`,
|
|
602
613
|
},
|
|
603
614
|
|
|
604
615
|
{
|
|
@@ -976,6 +987,40 @@ router.post(
|
|
|
976
987
|
}
|
|
977
988
|
})
|
|
978
989
|
);
|
|
990
|
+
|
|
991
|
+
router.post(
|
|
992
|
+
"/save-config",
|
|
993
|
+
isAdmin,
|
|
994
|
+
error_catcher(async (req, res) => {
|
|
995
|
+
const state = getState();
|
|
996
|
+
|
|
997
|
+
//TODO check this is a config key
|
|
998
|
+
const validKeyName = (k) =>
|
|
999
|
+
k !== "_csrf" && k !== "constructor" && k !== "__proto__";
|
|
1000
|
+
|
|
1001
|
+
for (const [k, v] of Object.entries(req.body)) {
|
|
1002
|
+
if (!isFixedConfig(k) && typeof v !== "undefined" && validKeyName(k)) {
|
|
1003
|
+
//TODO read value from type
|
|
1004
|
+
await state.setConfig(k, v);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// checkboxes that are false are not sent in post body. Check here
|
|
1009
|
+
const { boolcheck } = req.query;
|
|
1010
|
+
const boolchecks =
|
|
1011
|
+
typeof boolcheck === "undefined"
|
|
1012
|
+
? []
|
|
1013
|
+
: Array.isArray(boolcheck)
|
|
1014
|
+
? boolcheck
|
|
1015
|
+
: [boolcheck];
|
|
1016
|
+
for (const k of boolchecks) {
|
|
1017
|
+
if (typeof req.body[k] === "undefined" && validKeyName(k))
|
|
1018
|
+
await state.setConfig(k, false);
|
|
1019
|
+
}
|
|
1020
|
+
res.json({ success: "ok" });
|
|
1021
|
+
})
|
|
1022
|
+
);
|
|
1023
|
+
|
|
979
1024
|
/**
|
|
980
1025
|
* Do Auto backup now
|
|
981
1026
|
*/
|
|
@@ -1689,6 +1734,63 @@ const clearAllForm = (req) =>
|
|
|
1689
1734
|
],
|
|
1690
1735
|
});
|
|
1691
1736
|
|
|
1737
|
+
router.post(
|
|
1738
|
+
"/acq-ssl-tenant/:subdomain",
|
|
1739
|
+
isAdmin,
|
|
1740
|
+
error_catcher(async (req, res) => {
|
|
1741
|
+
if (
|
|
1742
|
+
db.is_it_multi_tenant() &&
|
|
1743
|
+
db.getTenantSchema() === db.connectObj.default_schema
|
|
1744
|
+
) {
|
|
1745
|
+
const { subdomain } = req.params;
|
|
1746
|
+
|
|
1747
|
+
const domain = getBaseDomain();
|
|
1748
|
+
|
|
1749
|
+
let altname = await tenant_letsencrypt_name(subdomain);
|
|
1750
|
+
|
|
1751
|
+
if (!altname || !domain) {
|
|
1752
|
+
res.json({ error: "Set Base URL for both tenant and root first." });
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
try {
|
|
1757
|
+
const file_store = db.connectObj.file_store;
|
|
1758
|
+
const admin_users = await User.find({ role_id: 1 }, { orderBy: "id" });
|
|
1759
|
+
// greenlock logic
|
|
1760
|
+
const Greenlock = require("greenlock");
|
|
1761
|
+
const greenlock = Greenlock.create({
|
|
1762
|
+
packageRoot: path.resolve(__dirname, ".."),
|
|
1763
|
+
configDir: path.join(file_store, "greenlock.d"),
|
|
1764
|
+
maintainerEmail: admin_users[0].email,
|
|
1765
|
+
});
|
|
1766
|
+
|
|
1767
|
+
await greenlock.sites.add({
|
|
1768
|
+
subject: altname,
|
|
1769
|
+
altnames: [altname],
|
|
1770
|
+
});
|
|
1771
|
+
// letsencrypt
|
|
1772
|
+
const tenant_letsencrypt_sites = getState().getConfig(
|
|
1773
|
+
"tenant_letsencrypt_sites",
|
|
1774
|
+
[]
|
|
1775
|
+
);
|
|
1776
|
+
await getState().setConfig("tenant_letsencrypt_sites", [
|
|
1777
|
+
altname,
|
|
1778
|
+
...tenant_letsencrypt_sites,
|
|
1779
|
+
]);
|
|
1780
|
+
|
|
1781
|
+
res.json({
|
|
1782
|
+
success: true,
|
|
1783
|
+
notify: "Certificate added, please restart server",
|
|
1784
|
+
});
|
|
1785
|
+
} catch (e) {
|
|
1786
|
+
res.json({ error: e.message });
|
|
1787
|
+
}
|
|
1788
|
+
} else {
|
|
1789
|
+
res.json({ error: req.__("Not possible for tenant") });
|
|
1790
|
+
}
|
|
1791
|
+
})
|
|
1792
|
+
);
|
|
1793
|
+
|
|
1692
1794
|
/**
|
|
1693
1795
|
* Do Enable letsencrypt
|
|
1694
1796
|
* @name post/enable-letsencrypt
|
|
@@ -1749,6 +1851,15 @@ router.post(
|
|
|
1749
1851
|
});
|
|
1750
1852
|
// letsencrypt
|
|
1751
1853
|
await getState().setConfig("letsencrypt", true);
|
|
1854
|
+
const tenant_letsencrypt_sites = getState().getConfig(
|
|
1855
|
+
"tenant_letsencrypt_sites",
|
|
1856
|
+
[]
|
|
1857
|
+
);
|
|
1858
|
+
await getState().setConfig("tenant_letsencrypt_sites", [
|
|
1859
|
+
...altnames,
|
|
1860
|
+
...tenant_letsencrypt_sites,
|
|
1861
|
+
]);
|
|
1862
|
+
|
|
1752
1863
|
req.flash(
|
|
1753
1864
|
"success",
|
|
1754
1865
|
req.__(
|
package/routes/eventlog.js
CHANGED
|
@@ -424,6 +424,7 @@ router.get(
|
|
|
424
424
|
error_catcher(async (req, res) => {
|
|
425
425
|
const { id } = req.params;
|
|
426
426
|
const ev = await EventLog.findOneWithUser(id);
|
|
427
|
+
const locale = getState().getConfig("default_locale", "en");
|
|
427
428
|
send_events_page({
|
|
428
429
|
res,
|
|
429
430
|
req,
|
|
@@ -435,7 +436,10 @@ router.get(
|
|
|
435
436
|
table(
|
|
436
437
|
{ class: "table eventlog" },
|
|
437
438
|
tbody(
|
|
438
|
-
tr(
|
|
439
|
+
tr(
|
|
440
|
+
th(req.__("When")),
|
|
441
|
+
td(localeDateTime(ev.occur_at, {}, locale))
|
|
442
|
+
),
|
|
439
443
|
tr(th(req.__("Type")), td(ev.event_type)),
|
|
440
444
|
tr(th(req.__("Channel")), td(ev.channel)),
|
|
441
445
|
tr(th(req.__("User")), td(ev.email))
|
package/routes/homepage.js
CHANGED
|
@@ -549,7 +549,14 @@ const no_views_logged_in = async (req, res) => {
|
|
|
549
549
|
* @returns {Promise<boolean>}
|
|
550
550
|
*/
|
|
551
551
|
const get_config_response = async (role_id, res, req) => {
|
|
552
|
-
const wrap = async (
|
|
552
|
+
const wrap = async (
|
|
553
|
+
contents,
|
|
554
|
+
homeCfg,
|
|
555
|
+
title,
|
|
556
|
+
description,
|
|
557
|
+
no_menu,
|
|
558
|
+
requestFluidLayout
|
|
559
|
+
) => {
|
|
553
560
|
if (contents.html_file) await sendHtmlFile(req, res, contents.html_file);
|
|
554
561
|
else
|
|
555
562
|
res.sendWrap(
|
|
@@ -558,6 +565,7 @@ const get_config_response = async (role_id, res, req) => {
|
|
|
558
565
|
description: description || "",
|
|
559
566
|
bodyClass: "page_" + db.sqlsanitize(homeCfg),
|
|
560
567
|
no_menu,
|
|
568
|
+
requestFluidLayout,
|
|
561
569
|
},
|
|
562
570
|
contents
|
|
563
571
|
);
|
|
@@ -578,7 +586,8 @@ const get_config_response = async (role_id, res, req) => {
|
|
|
578
586
|
homeCfg,
|
|
579
587
|
db_page.title,
|
|
580
588
|
db_page.description,
|
|
581
|
-
db_page.attributes?.no_menu
|
|
589
|
+
db_page.attributes?.no_menu,
|
|
590
|
+
db_page.attributes?.request_fluid_layout
|
|
582
591
|
);
|
|
583
592
|
else {
|
|
584
593
|
const group = PageGroup.findOne({ name: homeCfg });
|
|
@@ -592,7 +601,8 @@ const get_config_response = async (role_id, res, req) => {
|
|
|
592
601
|
homeCfg,
|
|
593
602
|
eligible.title,
|
|
594
603
|
eligible.description,
|
|
595
|
-
eligible.attributes?.no_menu
|
|
604
|
+
eligible.attributes?.no_menu,
|
|
605
|
+
eligible.attributes?.request_fluid_layout
|
|
596
606
|
);
|
|
597
607
|
} else wrap(req.__("%s has no eligible page", group.name), homeCfg);
|
|
598
608
|
} else res.redirect(homeCfg);
|
package/routes/list.js
CHANGED
|
@@ -26,6 +26,7 @@ const {
|
|
|
26
26
|
const Table = require("@saltcorn/data/models/table");
|
|
27
27
|
const { isAdmin, error_catcher } = require("./utils");
|
|
28
28
|
const moment = require("moment");
|
|
29
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* @type {object}
|
|
@@ -270,6 +271,7 @@ router.get(
|
|
|
270
271
|
res.sendWrap(
|
|
271
272
|
{
|
|
272
273
|
title: req.__(`%s data table`, table.name),
|
|
274
|
+
requestFluidLayout: true,
|
|
273
275
|
headers: [
|
|
274
276
|
//jsgrid - grid editor external component
|
|
275
277
|
{
|
|
@@ -426,7 +428,13 @@ router.get(
|
|
|
426
428
|
),
|
|
427
429
|
div({ id: "jsGridNotify" }),
|
|
428
430
|
|
|
429
|
-
div({
|
|
431
|
+
div({
|
|
432
|
+
id: "jsGrid",
|
|
433
|
+
class:
|
|
434
|
+
getState().getLightDarkMode() === "dark"
|
|
435
|
+
? "table-dark"
|
|
436
|
+
: undefined,
|
|
437
|
+
})
|
|
430
438
|
),
|
|
431
439
|
},
|
|
432
440
|
],
|
package/routes/page.js
CHANGED
|
@@ -66,6 +66,7 @@ const runPage = async (page, req, res, tic) => {
|
|
|
66
66
|
description: page.description,
|
|
67
67
|
bodyClass: "page_" + db.sqlsanitize(page.name),
|
|
68
68
|
no_menu: page.attributes?.no_menu,
|
|
69
|
+
requestFluidLayout: page.attributes?.request_fluid_layout,
|
|
69
70
|
} || `${page.name} page`,
|
|
70
71
|
add_edit_bar({
|
|
71
72
|
role,
|
package/routes/pageedit.js
CHANGED
|
@@ -143,6 +143,12 @@ const pagePropertiesForm = async (req, isNew) => {
|
|
|
143
143
|
sublabel: req.__("Omit the menu from this page"),
|
|
144
144
|
type: "Bool",
|
|
145
145
|
},
|
|
146
|
+
{
|
|
147
|
+
name: "request_fluid_layout",
|
|
148
|
+
label: req.__("Fluid layout"),
|
|
149
|
+
sublabel: req.__("Request fluid layout from theme for a wider display for this page"),
|
|
150
|
+
type: "Bool",
|
|
151
|
+
},
|
|
146
152
|
],
|
|
147
153
|
});
|
|
148
154
|
return form;
|
|
@@ -429,6 +435,7 @@ router.get(
|
|
|
429
435
|
form.hidden("id");
|
|
430
436
|
form.values = page;
|
|
431
437
|
form.values.no_menu = page.attributes?.no_menu;
|
|
438
|
+
form.values.request_fluid_layout = page.attributes?.request_fluid_layout;
|
|
432
439
|
form.onChange = `saveAndContinue(this)`;
|
|
433
440
|
res.sendWrap(
|
|
434
441
|
req.__(`Page attributes`),
|
|
@@ -475,9 +482,16 @@ router.post(
|
|
|
475
482
|
wrap(renderForm(form, req.csrfToken()), false, req)
|
|
476
483
|
);
|
|
477
484
|
} else {
|
|
478
|
-
const {
|
|
485
|
+
const {
|
|
486
|
+
id,
|
|
487
|
+
columns,
|
|
488
|
+
no_menu,
|
|
489
|
+
request_fluid_layout,
|
|
490
|
+
html_file,
|
|
491
|
+
...pageRow
|
|
492
|
+
} = form.values;
|
|
479
493
|
pageRow.min_role = +pageRow.min_role;
|
|
480
|
-
pageRow.attributes = { no_menu };
|
|
494
|
+
pageRow.attributes = { no_menu, request_fluid_layout };
|
|
481
495
|
if (html_file) {
|
|
482
496
|
pageRow.layout = {
|
|
483
497
|
html_file: html_file,
|
package/routes/tenant.js
CHANGED
|
@@ -44,7 +44,12 @@ const {
|
|
|
44
44
|
const db = require("@saltcorn/data/db");
|
|
45
45
|
|
|
46
46
|
const { loadAllPlugins, loadAndSaveNewPlugin } = require("../load_plugins");
|
|
47
|
-
const {
|
|
47
|
+
const {
|
|
48
|
+
isAdmin,
|
|
49
|
+
error_catcher,
|
|
50
|
+
is_ip_address,
|
|
51
|
+
tenant_letsencrypt_name,
|
|
52
|
+
} = require("./utils.js");
|
|
48
53
|
const User = require("@saltcorn/data/models/user");
|
|
49
54
|
const File = require("@saltcorn/data/models/file");
|
|
50
55
|
const {
|
|
@@ -53,6 +58,7 @@ const {
|
|
|
53
58
|
save_config_from_form,
|
|
54
59
|
} = require("../markup/admin.js");
|
|
55
60
|
const { getConfig } = require("@saltcorn/data/models/config");
|
|
61
|
+
const path = require("path");
|
|
56
62
|
//const {quote} = require("@saltcorn/db-common");
|
|
57
63
|
// todo add button backup / restore for particular tenant (available in admin tenants screens)
|
|
58
64
|
//const {
|
|
@@ -313,7 +319,53 @@ router.post(
|
|
|
313
319
|
if (hasTemplate) {
|
|
314
320
|
new_url_create += "auth/create_first_user";
|
|
315
321
|
}
|
|
322
|
+
const letsencrypt = getState().getConfig("letsencrypt", false);
|
|
323
|
+
if (letsencrypt) {
|
|
324
|
+
let altname = await tenant_letsencrypt_name(subdomain);
|
|
325
|
+
const tenant_letsencrypt_sites = getState().getConfig(
|
|
326
|
+
"tenant_letsencrypt_sites",
|
|
327
|
+
[]
|
|
328
|
+
);
|
|
329
|
+
const has_cert = tenant_letsencrypt_sites.includes(altname);
|
|
330
|
+
if (!has_cert) {
|
|
331
|
+
const file_store = db.connectObj.file_store;
|
|
332
|
+
const admin_users = await User.find(
|
|
333
|
+
{ role_id: 1 },
|
|
334
|
+
{ orderBy: "id" }
|
|
335
|
+
);
|
|
336
|
+
// greenlock logic
|
|
337
|
+
const Greenlock = require("greenlock");
|
|
338
|
+
const greenlock = Greenlock.create({
|
|
339
|
+
packageRoot: path.resolve(__dirname, ".."),
|
|
340
|
+
configDir: path.join(file_store, "greenlock.d"),
|
|
341
|
+
maintainerEmail: admin_users[0].email,
|
|
342
|
+
});
|
|
316
343
|
|
|
344
|
+
await greenlock.sites.add({
|
|
345
|
+
subject: altname,
|
|
346
|
+
altnames: [altname],
|
|
347
|
+
});
|
|
348
|
+
// letsencrypt
|
|
349
|
+
const tenant_letsencrypt_sites = getState().getConfig(
|
|
350
|
+
"tenant_letsencrypt_sites",
|
|
351
|
+
[]
|
|
352
|
+
);
|
|
353
|
+
await getState().setConfig("tenant_letsencrypt_sites", [
|
|
354
|
+
altname,
|
|
355
|
+
...tenant_letsencrypt_sites,
|
|
356
|
+
]);
|
|
357
|
+
if (req.user?.role_id === 1) {
|
|
358
|
+
req.flash(
|
|
359
|
+
"success",
|
|
360
|
+
req.__(
|
|
361
|
+
"Tenant created. Certificate will be acquired on first visit."
|
|
362
|
+
)
|
|
363
|
+
);
|
|
364
|
+
res.redirect("/tenant/list");
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
317
369
|
res.sendWrap(
|
|
318
370
|
req.__("Create application"),
|
|
319
371
|
div(
|
|
@@ -369,6 +421,7 @@ router.get(
|
|
|
369
421
|
return;
|
|
370
422
|
}
|
|
371
423
|
const tens = await db.select("_sc_tenants");
|
|
424
|
+
const locale = getState().getConfig("default_locale", "en");
|
|
372
425
|
send_infoarch_page({
|
|
373
426
|
res,
|
|
374
427
|
req,
|
|
@@ -395,7 +448,8 @@ router.get(
|
|
|
395
448
|
},
|
|
396
449
|
{
|
|
397
450
|
label: req.__("Created"),
|
|
398
|
-
key: (r) =>
|
|
451
|
+
key: (r) =>
|
|
452
|
+
r.created ? localeDateTime(r.created, {}, locale) : "",
|
|
399
453
|
},
|
|
400
454
|
{
|
|
401
455
|
label: req.__("Information"),
|
|
@@ -612,8 +666,18 @@ router.get(
|
|
|
612
666
|
return;
|
|
613
667
|
}
|
|
614
668
|
const { subdomain } = req.params;
|
|
669
|
+
|
|
615
670
|
// get tenant info
|
|
616
671
|
const info = await get_tenant_info(subdomain);
|
|
672
|
+
const letsencrypt = getState().getConfig("letsencrypt", false);
|
|
673
|
+
|
|
674
|
+
let altname = await tenant_letsencrypt_name(subdomain);
|
|
675
|
+
const tenant_letsencrypt_sites = getState().getConfig(
|
|
676
|
+
"tenant_letsencrypt_sites",
|
|
677
|
+
[]
|
|
678
|
+
);
|
|
679
|
+
const has_cert = tenant_letsencrypt_sites.includes(altname);
|
|
680
|
+
|
|
617
681
|
// get list of files
|
|
618
682
|
let files;
|
|
619
683
|
await db.runWithTenant(subdomain, async () => {
|
|
@@ -632,6 +696,7 @@ router.get(
|
|
|
632
696
|
// TBD make more pretty view - in ideal with charts
|
|
633
697
|
contents: [
|
|
634
698
|
table(
|
|
699
|
+
{ class: "table table-sm" },
|
|
635
700
|
tr(
|
|
636
701
|
th(req.__("First user E-mail")),
|
|
637
702
|
td(
|
|
@@ -723,6 +788,20 @@ router.get(
|
|
|
723
788
|
submitLabel: req.__("Save"),
|
|
724
789
|
submitButtonClass: "btn-outline-primary",
|
|
725
790
|
onChange: "remove_outline(this)",
|
|
791
|
+
additionalButtons: [
|
|
792
|
+
...(letsencrypt && !has_cert
|
|
793
|
+
? [
|
|
794
|
+
{
|
|
795
|
+
label: req.__("Acquire LetsEncrypt certificate"),
|
|
796
|
+
id: "btnAcqCert",
|
|
797
|
+
class: "btn btn-secondary",
|
|
798
|
+
onclick: `press_store_button(this);ajax_post('/admin/acq-ssl-tenant/${encodeURIComponent(
|
|
799
|
+
subdomain
|
|
800
|
+
)}')`,
|
|
801
|
+
},
|
|
802
|
+
]
|
|
803
|
+
: []),
|
|
804
|
+
],
|
|
726
805
|
fields: [
|
|
727
806
|
{
|
|
728
807
|
name: "base_url",
|
package/routes/utils.js
CHANGED
|
@@ -33,6 +33,7 @@ const {
|
|
|
33
33
|
const path = require("path");
|
|
34
34
|
const { UAParser } = require("ua-parser-js");
|
|
35
35
|
const crypto = require("crypto");
|
|
36
|
+
const { domain_sanitize } = require("@saltcorn/admin-models/models/tenant");
|
|
36
37
|
|
|
37
38
|
const get_sys_info = async () => {
|
|
38
39
|
const disks = await si.fsSize();
|
|
@@ -372,6 +373,19 @@ const is_ip_address = (hostname) => {
|
|
|
372
373
|
return hostname.split(".").every((s) => +s >= 0 && +s <= 255);
|
|
373
374
|
};
|
|
374
375
|
|
|
376
|
+
const tenant_letsencrypt_name = async (subdomain) => {
|
|
377
|
+
const saneDomain = domain_sanitize(subdomain);
|
|
378
|
+
let altname;
|
|
379
|
+
await db.runWithTenant(saneDomain, async () => {
|
|
380
|
+
altname = getState()
|
|
381
|
+
.getConfig("base_url", "")
|
|
382
|
+
.replace("https://", "")
|
|
383
|
+
.replace("http://", "")
|
|
384
|
+
.replace("/", "");
|
|
385
|
+
});
|
|
386
|
+
return altname;
|
|
387
|
+
};
|
|
388
|
+
|
|
375
389
|
const admin_config_route = ({
|
|
376
390
|
router,
|
|
377
391
|
path,
|
|
@@ -587,4 +601,5 @@ module.exports = {
|
|
|
587
601
|
setRole,
|
|
588
602
|
getEligiblePage,
|
|
589
603
|
getRandomPage,
|
|
604
|
+
tenant_letsencrypt_name,
|
|
590
605
|
};
|
|
@@ -164,7 +164,7 @@ describe("Stable versioning install", () => {
|
|
|
164
164
|
expect(dbPlugin.version).toBe("0.1.0");
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
-
it("installs a fixed version", async () => {
|
|
167
|
+
it("installs and upgrades a fixed version", async () => {
|
|
168
168
|
const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
|
|
169
169
|
await loadAndSaveNewPlugin(
|
|
170
170
|
new Plugin({
|
|
@@ -188,7 +188,7 @@ describe("Stable versioning install", () => {
|
|
|
188
188
|
name: "@christianhugoch/empty_sc_test_plugin",
|
|
189
189
|
location: "@christianhugoch/empty_sc_test_plugin",
|
|
190
190
|
source: "npm",
|
|
191
|
-
version: "0.0
|
|
191
|
+
version: "0.2.0",
|
|
192
192
|
}),
|
|
193
193
|
true
|
|
194
194
|
);
|
|
@@ -196,7 +196,7 @@ describe("Stable versioning install", () => {
|
|
|
196
196
|
name: "@christianhugoch/empty_sc_test_plugin",
|
|
197
197
|
});
|
|
198
198
|
expect(dbPlugin).not.toBe(null);
|
|
199
|
-
expect(dbPlugin.version).toBe("0.0
|
|
199
|
+
expect(dbPlugin.version).toBe("0.1.0");
|
|
200
200
|
});
|
|
201
201
|
});
|
|
202
202
|
|
|
@@ -245,7 +245,7 @@ describe("Stable versioning upgrade", () => {
|
|
|
245
245
|
expect(newPlugin.version).toBe("0.0.3");
|
|
246
246
|
});
|
|
247
247
|
|
|
248
|
-
it("upgrades to latest with downgrade", async () => {
|
|
248
|
+
it("upgrades to latest with downgrade to supported", async () => {
|
|
249
249
|
const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
|
|
250
250
|
await loadAndSaveNewPlugin(
|
|
251
251
|
new Plugin({
|
|
@@ -313,7 +313,7 @@ describe("Stable versioning upgrade", () => {
|
|
|
313
313
|
expect(newPlugin.version).toBe("0.0.3");
|
|
314
314
|
});
|
|
315
315
|
|
|
316
|
-
it("upgrades to fixed version with downgrade", async () => {
|
|
316
|
+
it("upgrades to fixed version with downgrade to supported", async () => {
|
|
317
317
|
const loadAndSaveNewPlugin = load_plugins.loadAndSaveNewPlugin;
|
|
318
318
|
await loadAndSaveNewPlugin(
|
|
319
319
|
new Plugin({
|
|
@@ -336,7 +336,7 @@ describe("Stable versioning upgrade", () => {
|
|
|
336
336
|
name: "@christianhugoch/empty_sc_test_plugin",
|
|
337
337
|
location: "@christianhugoch/empty_sc_test_plugin",
|
|
338
338
|
source: "npm",
|
|
339
|
-
version: "0.0
|
|
339
|
+
version: "0.2.0",
|
|
340
340
|
}),
|
|
341
341
|
true
|
|
342
342
|
);
|
|
@@ -344,6 +344,6 @@ describe("Stable versioning upgrade", () => {
|
|
|
344
344
|
name: "@christianhugoch/empty_sc_test_plugin",
|
|
345
345
|
});
|
|
346
346
|
expect(newPlugin).not.toBe(null);
|
|
347
|
-
expect(newPlugin.version).toBe("0.0
|
|
347
|
+
expect(newPlugin.version).toBe("0.1.0");
|
|
348
348
|
});
|
|
349
349
|
});
|
package/tests/plugins.test.js
CHANGED
|
@@ -365,7 +365,7 @@ describe("Upgrade plugin to supported version", () => {
|
|
|
365
365
|
expect(upgradedPlugin.version).toBe("0.0.3");
|
|
366
366
|
});
|
|
367
367
|
|
|
368
|
-
it("upgrades to
|
|
368
|
+
it("upgrades to latest as fixed version", async () => {
|
|
369
369
|
const oldPlugin = await setupPluginVersion(
|
|
370
370
|
"@christianhugoch/empty_sc_test_plugin_two",
|
|
371
371
|
"0.0.1"
|
|
@@ -389,7 +389,7 @@ describe("Upgrade plugin to supported version", () => {
|
|
|
389
389
|
expect(upgradedPlugin.version).toBe("0.0.3");
|
|
390
390
|
});
|
|
391
391
|
|
|
392
|
-
it("upgrades with a downgrade of
|
|
392
|
+
it("upgrades with a downgrade of latest", async () => {
|
|
393
393
|
const oldPlugin = await setupPluginVersion(
|
|
394
394
|
"@christianhugoch/empty_sc_test_plugin",
|
|
395
395
|
"0.0.1"
|
|
@@ -407,7 +407,7 @@ describe("Upgrade plugin to supported version", () => {
|
|
|
407
407
|
expect(upgradedPlugin.version).toBe("0.1.0");
|
|
408
408
|
});
|
|
409
409
|
|
|
410
|
-
it("upgrades with a downgrade of
|
|
410
|
+
it("upgrades with a downgrade of latest as fixed version", async () => {
|
|
411
411
|
const oldPlugin = await setupPluginVersion(
|
|
412
412
|
"@christianhugoch/empty_sc_test_plugin",
|
|
413
413
|
"0.0.1"
|
|
@@ -421,7 +421,7 @@ describe("Upgrade plugin to supported version", () => {
|
|
|
421
421
|
"@christianhugoch/empty_sc_test_plugin"
|
|
422
422
|
)}`
|
|
423
423
|
)
|
|
424
|
-
.send("version=0.
|
|
424
|
+
.send("version=0.2.0")
|
|
425
425
|
.set("Cookie", loginCookie)
|
|
426
426
|
.expect(toRedirect("/plugins"));
|
|
427
427
|
const upgradedPlugin = await Plugin.findOne({
|
package/wrapper.js
CHANGED
|
@@ -193,7 +193,9 @@ const get_headers = (req, version_tag, description, extras = []) => {
|
|
|
193
193
|
state.logLevel
|
|
194
194
|
}, _sc_globalCsrf = "${req.csrfToken()}", _sc_version_tag = "${version_tag}"${
|
|
195
195
|
locale ? `, _sc_locale = "${locale}"` : ""
|
|
196
|
-
}
|
|
196
|
+
}, _sc_lightmode = ${JSON.stringify(
|
|
197
|
+
state.getLightDarkMode(req.user)
|
|
198
|
+
)};</script>`,
|
|
197
199
|
},
|
|
198
200
|
{ css: `/static_assets/${version_tag}/saltcorn.css` },
|
|
199
201
|
{ script: `/static_assets/${version_tag}/saltcorn-common.js` },
|