@saltcorn/server 0.6.3-beta.0 → 0.6.3
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 +12 -2
- package/auth/roleadmin.js +50 -0
- package/auth/routes.js +228 -2
- package/locales/en.json +15 -1
- package/package.json +11 -6
- package/public/gridedit.js +6 -2
- package/public/saltcorn.js +34 -2
- package/restart_watcher.js +3 -0
- package/routes/admin.js +1 -0
- package/routes/api.js +18 -15
- package/routes/delete.js +1 -1
- package/routes/edit.js +1 -1
- package/routes/fields.js +11 -1
- package/routes/files.js +3 -3
- package/routes/homepage.js +23 -22
- package/routes/index.js +37 -36
- package/routes/list.js +4 -0
- package/routes/page.js +2 -2
- package/routes/pageedit.js +6 -3
- package/routes/scapi.js +6 -5
- package/routes/utils.js +7 -1
- package/routes/view.js +17 -11
- package/routes/viewedit.js +33 -7
- package/serve.js +0 -3
- package/tests/view.test.js +38 -0
- package/wrapper.js +1 -1
package/app.js
CHANGED
|
@@ -38,7 +38,7 @@ const { h1 } = require("@saltcorn/markup/tags");
|
|
|
38
38
|
const is = require("contractis/is");
|
|
39
39
|
const Trigger = require("@saltcorn/data/models/trigger");
|
|
40
40
|
const s3storage = require("./s3storage");
|
|
41
|
-
|
|
41
|
+
const TotpStrategy = require("passport-totp").Strategy;
|
|
42
42
|
const locales = Object.keys(available_languages);
|
|
43
43
|
// i18n configuration
|
|
44
44
|
const i18n = new I18n({
|
|
@@ -148,7 +148,9 @@ const getApp = async (opts = {}) => {
|
|
|
148
148
|
req.flash("danger", req.__("Incorrect user or password"))
|
|
149
149
|
);
|
|
150
150
|
const mu = await User.authenticate(userobj);
|
|
151
|
-
if (mu
|
|
151
|
+
if (mu && mu._attributes.totp_enabled)
|
|
152
|
+
return done(null, { pending_user: mu.session_object });
|
|
153
|
+
else if (mu) return done(null, mu.session_object);
|
|
152
154
|
else {
|
|
153
155
|
const { password, ...nopw } = userobj;
|
|
154
156
|
Trigger.emitEvent("LoginFailed", null, null, nopw);
|
|
@@ -188,6 +190,14 @@ const getApp = async (opts = {}) => {
|
|
|
188
190
|
}
|
|
189
191
|
})
|
|
190
192
|
);
|
|
193
|
+
passport.use(
|
|
194
|
+
new TotpStrategy(function (user, done) {
|
|
195
|
+
// setup function, supply key and period to done callback
|
|
196
|
+
User.findOne({ id: user.pending_user.id }).then((u) => {
|
|
197
|
+
return done(null, u._attributes.totp_key, 30);
|
|
198
|
+
});
|
|
199
|
+
})
|
|
200
|
+
);
|
|
191
201
|
passport.serializeUser(function (user, done) {
|
|
192
202
|
done(null, user);
|
|
193
203
|
});
|
package/auth/roleadmin.js
CHANGED
|
@@ -85,6 +85,28 @@ const editRoleLayoutForm = (role, layouts, layout_by_role, req) =>
|
|
|
85
85
|
)
|
|
86
86
|
);
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* @param {Role} role
|
|
90
|
+
* @param {Layout[]} layouts
|
|
91
|
+
* @param {*} layout_by_role
|
|
92
|
+
* @param {object} req
|
|
93
|
+
* @returns {Form}
|
|
94
|
+
*/
|
|
95
|
+
const editRole2FAPolicyForm = (role, twofa_policy_by_role, req) =>
|
|
96
|
+
form(
|
|
97
|
+
{
|
|
98
|
+
action: `/roleadmin/setrole2fapolicy/${role.id}`,
|
|
99
|
+
method: "post",
|
|
100
|
+
},
|
|
101
|
+
csrfField(req),
|
|
102
|
+
select(
|
|
103
|
+
{ name: "policy", onchange: "form.submit()" },
|
|
104
|
+
["Optional", "Disabled", "Mandatory"].map((p) =>
|
|
105
|
+
option({ selected: twofa_policy_by_role[role.id] === p }, p)
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
);
|
|
109
|
+
|
|
88
110
|
/**
|
|
89
111
|
* @param {object} req
|
|
90
112
|
* @returns {Form}
|
|
@@ -125,6 +147,7 @@ router.get(
|
|
|
125
147
|
(l) => l !== "emergency"
|
|
126
148
|
);
|
|
127
149
|
const layout_by_role = getState().getConfig("layout_by_role");
|
|
150
|
+
const twofa_policy_by_role = getState().getConfig("twofa_policy_by_role");
|
|
128
151
|
send_users_page({
|
|
129
152
|
res,
|
|
130
153
|
req,
|
|
@@ -142,6 +165,13 @@ router.get(
|
|
|
142
165
|
key: (role) =>
|
|
143
166
|
editRoleLayoutForm(role, layouts, layout_by_role, req),
|
|
144
167
|
},
|
|
168
|
+
{
|
|
169
|
+
label: req.__("2FA policy"),
|
|
170
|
+
key: (role) =>
|
|
171
|
+
role.id === 10
|
|
172
|
+
? ""
|
|
173
|
+
: editRole2FAPolicyForm(role, twofa_policy_by_role, req),
|
|
174
|
+
},
|
|
145
175
|
{
|
|
146
176
|
label: req.__("Delete"),
|
|
147
177
|
key: (r) =>
|
|
@@ -240,6 +270,26 @@ router.post(
|
|
|
240
270
|
})
|
|
241
271
|
);
|
|
242
272
|
|
|
273
|
+
/**
|
|
274
|
+
* @name post/setrolelayout/:id
|
|
275
|
+
* @function
|
|
276
|
+
* @memberof module:auth/roleadmin~roleadminRouter
|
|
277
|
+
*/
|
|
278
|
+
router.post(
|
|
279
|
+
"/setrole2fapolicy/:id",
|
|
280
|
+
isAdmin,
|
|
281
|
+
error_catcher(async (req, res) => {
|
|
282
|
+
const { id } = req.params;
|
|
283
|
+
const twofa_policy_by_role = getState().getConfigCopy(
|
|
284
|
+
"twofa_policy_by_role"
|
|
285
|
+
);
|
|
286
|
+
twofa_policy_by_role[+id] = req.body.policy;
|
|
287
|
+
await getState().setConfig("twofa_policy_by_role", twofa_policy_by_role);
|
|
288
|
+
req.flash("success", req.__(`Saved 2FA policy for role`));
|
|
289
|
+
|
|
290
|
+
res.redirect(`/roleadmin`);
|
|
291
|
+
})
|
|
292
|
+
);
|
|
243
293
|
const unDeletableRoles = [1, 8, 10];
|
|
244
294
|
/**
|
|
245
295
|
* @name post/delete/:id
|
package/auth/routes.js
CHANGED
|
@@ -24,12 +24,14 @@ const { renderForm, post_btn } = require("@saltcorn/markup");
|
|
|
24
24
|
const passport = require("passport");
|
|
25
25
|
const {
|
|
26
26
|
a,
|
|
27
|
+
img,
|
|
27
28
|
text,
|
|
28
29
|
table,
|
|
29
30
|
tbody,
|
|
30
31
|
th,
|
|
31
32
|
td,
|
|
32
33
|
tr,
|
|
34
|
+
h4,
|
|
33
35
|
form,
|
|
34
36
|
select,
|
|
35
37
|
option,
|
|
@@ -37,6 +39,8 @@ const {
|
|
|
37
39
|
i,
|
|
38
40
|
div,
|
|
39
41
|
code,
|
|
42
|
+
pre,
|
|
43
|
+
p,
|
|
40
44
|
} = require("@saltcorn/markup/tags");
|
|
41
45
|
const {
|
|
42
46
|
available_languages,
|
|
@@ -52,7 +56,9 @@ const { restore_backup } = require("../markup/admin.js");
|
|
|
52
56
|
const { restore } = require("@saltcorn/data/models/backup");
|
|
53
57
|
const load_plugins = require("../load_plugins");
|
|
54
58
|
const fs = require("fs");
|
|
55
|
-
|
|
59
|
+
const base32 = require("thirty-two");
|
|
60
|
+
const qrcode = require("qrcode");
|
|
61
|
+
const totp = require("notp").totp;
|
|
56
62
|
/**
|
|
57
63
|
* @type {object}
|
|
58
64
|
* @const
|
|
@@ -599,6 +605,8 @@ const signup_login_with_user = (u, req, res) =>
|
|
|
599
605
|
if (!err) {
|
|
600
606
|
Trigger.emitEvent("Login", null, u);
|
|
601
607
|
if (getState().verifier) res.redirect("/auth/verification-flow");
|
|
608
|
+
else if (getState().get2FApolicy(u) === "Mandatory")
|
|
609
|
+
res.redirect("/auth/twofa/setup/totp");
|
|
602
610
|
else res.redirect("/");
|
|
603
611
|
} else {
|
|
604
612
|
req.flash("danger", err);
|
|
@@ -920,6 +928,11 @@ router.post(
|
|
|
920
928
|
error_catcher(async (req, res) => {
|
|
921
929
|
ipLimiter.resetKey(req.ip);
|
|
922
930
|
userLimiter.resetKey(userIdKey(req.body));
|
|
931
|
+
if (req.user.pending_user) {
|
|
932
|
+
res.redirect("/auth/twofa/login/totp");
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
|
|
923
936
|
if (req.session.cookie)
|
|
924
937
|
if (req.body.remember) {
|
|
925
938
|
const setDur = +getState().getConfig("cookie_duration_remember", 0);
|
|
@@ -932,7 +945,9 @@ router.post(
|
|
|
932
945
|
}
|
|
933
946
|
Trigger.emitEvent("Login", null, req.user);
|
|
934
947
|
req.flash("success", req.__("Welcome, %s!", req.user.email));
|
|
935
|
-
|
|
948
|
+
if (getState().get2FApolicy(req.user) === "Mandatory") {
|
|
949
|
+
res.redirect("/auth/twofa/setup/totp");
|
|
950
|
+
} else res.redirect("/");
|
|
936
951
|
})
|
|
937
952
|
);
|
|
938
953
|
|
|
@@ -1095,6 +1110,9 @@ const userSettings = async ({ req, res, pwform, user }) => {
|
|
|
1095
1110
|
}
|
|
1096
1111
|
let apikeycard;
|
|
1097
1112
|
const min_role_apikeygen = +getState().getConfig("min_role_apikeygen", 1);
|
|
1113
|
+
const twoFaPolicy = getState().get2FApolicy(user);
|
|
1114
|
+
const show2FAPolicy =
|
|
1115
|
+
twoFaPolicy !== "Disabled" || user._attributes.totp_enabled;
|
|
1098
1116
|
if (user.role_id <= min_role_apikeygen)
|
|
1099
1117
|
apikeycard = {
|
|
1100
1118
|
type: "card",
|
|
@@ -1163,6 +1181,40 @@ const userSettings = async ({ req, res, pwform, user }) => {
|
|
|
1163
1181
|
title: req.__("Change password"),
|
|
1164
1182
|
contents: renderForm(pwform, req.csrfToken()),
|
|
1165
1183
|
},
|
|
1184
|
+
...(show2FAPolicy
|
|
1185
|
+
? [
|
|
1186
|
+
{
|
|
1187
|
+
type: "card",
|
|
1188
|
+
title: req.__("Two-factor authentication"),
|
|
1189
|
+
contents: [
|
|
1190
|
+
div(
|
|
1191
|
+
user._attributes.totp_enabled
|
|
1192
|
+
? req.__("Two-factor authentication is enabled")
|
|
1193
|
+
: req.__("Two-factor authentication is disabled")
|
|
1194
|
+
),
|
|
1195
|
+
div(
|
|
1196
|
+
user._attributes.totp_enabled
|
|
1197
|
+
? post_btn(
|
|
1198
|
+
"/auth/twofa/disable/totp",
|
|
1199
|
+
"Disable",
|
|
1200
|
+
req.csrfToken(),
|
|
1201
|
+
{
|
|
1202
|
+
btnClass: "btn-danger mt-2",
|
|
1203
|
+
req,
|
|
1204
|
+
}
|
|
1205
|
+
)
|
|
1206
|
+
: a(
|
|
1207
|
+
{
|
|
1208
|
+
href: "/auth/twofa/setup/totp",
|
|
1209
|
+
class: "btn btn-primary mt-2",
|
|
1210
|
+
},
|
|
1211
|
+
"Enable"
|
|
1212
|
+
)
|
|
1213
|
+
),
|
|
1214
|
+
],
|
|
1215
|
+
},
|
|
1216
|
+
]
|
|
1217
|
+
: []),
|
|
1166
1218
|
...(apikeycard ? [apikeycard] : []),
|
|
1167
1219
|
],
|
|
1168
1220
|
};
|
|
@@ -1253,6 +1305,12 @@ router.get(
|
|
|
1253
1305
|
loggedIn,
|
|
1254
1306
|
error_catcher(async (req, res) => {
|
|
1255
1307
|
const user = await User.findOne({ id: req.user.id });
|
|
1308
|
+
if (!user) {
|
|
1309
|
+
req.logout();
|
|
1310
|
+
req.flash("danger", req.__("Must be logged in first"));
|
|
1311
|
+
res.redirect("/auth/login");
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1256
1314
|
res.sendWrap(
|
|
1257
1315
|
req.__("User settings"),
|
|
1258
1316
|
await userSettings({ req, res, pwform: changPwForm(req), user })
|
|
@@ -1436,3 +1494,171 @@ router.all(
|
|
|
1436
1494
|
res.redirect(wfres.redirect || "/");
|
|
1437
1495
|
})
|
|
1438
1496
|
);
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* @name get/settings
|
|
1500
|
+
* @function
|
|
1501
|
+
* @memberof module:auth/routes~routesRouter
|
|
1502
|
+
*/
|
|
1503
|
+
router.get(
|
|
1504
|
+
"/twofa/setup/totp",
|
|
1505
|
+
loggedIn,
|
|
1506
|
+
error_catcher(async (req, res) => {
|
|
1507
|
+
const user = await User.findOne({ id: req.user.id });
|
|
1508
|
+
let key;
|
|
1509
|
+
if (user._attributes.totp_key) key = user._attributes.totp_key;
|
|
1510
|
+
else {
|
|
1511
|
+
key = randomKey(10);
|
|
1512
|
+
user._attributes.totp_key = key;
|
|
1513
|
+
await user.update({ _attributes: user._attributes });
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
const encodedKey = base32.encode(key);
|
|
1517
|
+
|
|
1518
|
+
// generate QR code for scanning into Google Authenticator
|
|
1519
|
+
// reference: https://code.google.com/p/google-authenticator/wiki/KeyUriFormat
|
|
1520
|
+
const site_name = getState().getConfig("site_name");
|
|
1521
|
+
const otpUrl = `otpauth://totp/${
|
|
1522
|
+
user.email
|
|
1523
|
+
}?secret=${encodedKey}&period=30&issuer=${encodeURIComponent(site_name)}`;
|
|
1524
|
+
const image = await qrcode.toDataURL(otpUrl);
|
|
1525
|
+
res.sendWrap(req.__("Setup two-factor authentication"), {
|
|
1526
|
+
type: "card",
|
|
1527
|
+
title: req.__(
|
|
1528
|
+
"Setup two-factor authentication with Time-based One-Time Password (TOTP)"
|
|
1529
|
+
),
|
|
1530
|
+
contents: [
|
|
1531
|
+
h4(req.__("1. Scan this QR code in your Authenticator app")),
|
|
1532
|
+
img({ src: image }),
|
|
1533
|
+
p("Or enter this code:"),
|
|
1534
|
+
code(pre(encodedKey.toString())),
|
|
1535
|
+
h4(
|
|
1536
|
+
req.__(
|
|
1537
|
+
"2. Enter the six-digit code generated in your Authenticator app"
|
|
1538
|
+
)
|
|
1539
|
+
),
|
|
1540
|
+
renderForm(totpForm(req), req.csrfToken()),
|
|
1541
|
+
],
|
|
1542
|
+
});
|
|
1543
|
+
})
|
|
1544
|
+
);
|
|
1545
|
+
|
|
1546
|
+
router.post(
|
|
1547
|
+
"/twofa/setup/totp",
|
|
1548
|
+
loggedIn,
|
|
1549
|
+
error_catcher(async (req, res) => {
|
|
1550
|
+
const user = await User.findOne({ id: req.user.id });
|
|
1551
|
+
|
|
1552
|
+
if (!user._attributes.totp_key) {
|
|
1553
|
+
//key not set
|
|
1554
|
+
req.flash("danger", req.__("2FA TOTP Key not set"));
|
|
1555
|
+
res.redirect("/auth/twofa/setup/totp");
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
const form = totpForm(req);
|
|
1560
|
+
form.validate(req.body);
|
|
1561
|
+
if (form.hasErrors) {
|
|
1562
|
+
req.flash("danger", req.__("Error processing form"));
|
|
1563
|
+
res.redirect("/auth/twofa/setup/totp");
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
const code = `${form.values.totpCode}`;
|
|
1567
|
+
const rv = totp.verify(code, user._attributes.totp_key, {
|
|
1568
|
+
time: 30,
|
|
1569
|
+
});
|
|
1570
|
+
if (!rv) {
|
|
1571
|
+
req.flash("danger", req.__("Could not verify code"));
|
|
1572
|
+
res.redirect("/auth/twofa/setup/totp");
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
user._attributes.totp_enabled = true;
|
|
1576
|
+
await user.update({ _attributes: user._attributes });
|
|
1577
|
+
req.flash(
|
|
1578
|
+
"success",
|
|
1579
|
+
req.__(
|
|
1580
|
+
"Two-factor authentication with Time-based One-Time Password enabled"
|
|
1581
|
+
)
|
|
1582
|
+
);
|
|
1583
|
+
|
|
1584
|
+
res.redirect("/auth/settings");
|
|
1585
|
+
})
|
|
1586
|
+
);
|
|
1587
|
+
|
|
1588
|
+
router.post(
|
|
1589
|
+
"/twofa/disable/totp",
|
|
1590
|
+
loggedIn,
|
|
1591
|
+
error_catcher(async (req, res) => {
|
|
1592
|
+
const user = await User.findOne({ id: req.user.id });
|
|
1593
|
+
user._attributes.totp_enabled = false;
|
|
1594
|
+
delete user._attributes.totp_key;
|
|
1595
|
+
await user.update({ _attributes: user._attributes });
|
|
1596
|
+
req.flash(
|
|
1597
|
+
"success",
|
|
1598
|
+
req.__(
|
|
1599
|
+
"Two-factor authentication with Time-based One-Time Password disabled"
|
|
1600
|
+
)
|
|
1601
|
+
);
|
|
1602
|
+
res.redirect("/auth/settings");
|
|
1603
|
+
})
|
|
1604
|
+
);
|
|
1605
|
+
const totpForm = (req) =>
|
|
1606
|
+
new Form({
|
|
1607
|
+
action: "/auth/twofa/setup/totp",
|
|
1608
|
+
fields: [
|
|
1609
|
+
{
|
|
1610
|
+
name: "totpCode",
|
|
1611
|
+
label: req.__("Code"),
|
|
1612
|
+
type: "Integer",
|
|
1613
|
+
required: true,
|
|
1614
|
+
},
|
|
1615
|
+
],
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
const randomKey = function (len) {
|
|
1619
|
+
function getRandomInt(min, max) {
|
|
1620
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
1621
|
+
}
|
|
1622
|
+
var buf = [],
|
|
1623
|
+
chars = "abcdefghijklmnopqrstuvwxyz0123456789",
|
|
1624
|
+
charlen = chars.length;
|
|
1625
|
+
|
|
1626
|
+
for (var i = 0; i < len; ++i) {
|
|
1627
|
+
buf.push(chars[getRandomInt(0, charlen - 1)]);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
return buf.join("");
|
|
1631
|
+
};
|
|
1632
|
+
|
|
1633
|
+
router.get(
|
|
1634
|
+
"/twofa/login/totp",
|
|
1635
|
+
error_catcher(async (req, res) => {
|
|
1636
|
+
const form = new Form({
|
|
1637
|
+
action: "/auth/twofa/login/totp",
|
|
1638
|
+
submitLabel: "Verify",
|
|
1639
|
+
fields: [
|
|
1640
|
+
{
|
|
1641
|
+
name: "code",
|
|
1642
|
+
label: req.__("Code"),
|
|
1643
|
+
type: "Integer",
|
|
1644
|
+
required: true,
|
|
1645
|
+
},
|
|
1646
|
+
],
|
|
1647
|
+
});
|
|
1648
|
+
res.sendAuthWrap(req.__(`Two-factor authentication`), form, {});
|
|
1649
|
+
})
|
|
1650
|
+
);
|
|
1651
|
+
|
|
1652
|
+
router.post(
|
|
1653
|
+
"/twofa/login/totp",
|
|
1654
|
+
passport.authenticate("totp", {
|
|
1655
|
+
failureRedirect: "/auth/twofa/login/totp",
|
|
1656
|
+
failureFlash: true,
|
|
1657
|
+
}),
|
|
1658
|
+
error_catcher(async (req, res) => {
|
|
1659
|
+
const user = await User.findOne({ id: req.user.pending_user.id });
|
|
1660
|
+
user.relogin(req);
|
|
1661
|
+
Trigger.emitEvent("Login", null, user);
|
|
1662
|
+
res.redirect("/");
|
|
1663
|
+
})
|
|
1664
|
+
);
|
package/locales/en.json
CHANGED
|
@@ -845,5 +845,19 @@
|
|
|
845
845
|
"User should have this role or higher to generate API keys in their user settings": "User should have this role or higher to generate API keys in their user settings",
|
|
846
846
|
"API token removed": "API token removed",
|
|
847
847
|
"Row inclusion formula": "Row inclusion formula",
|
|
848
|
-
"Only include rows where this formula is true": "Only include rows where this formula is true"
|
|
848
|
+
"Only include rows where this formula is true": "Only include rows where this formula is true",
|
|
849
|
+
"Slug": "Slug",
|
|
850
|
+
"Field that can be used for a prettier URL structure": "Field that can be used for a prettier URL structure",
|
|
851
|
+
"Setup two-factor authentication": "Setup two-factor authentication",
|
|
852
|
+
"Setup two-factor authentication with Time-based One-Time Password (TOTP)": "Setup two-factor authentication with Time-based One-Time Password (TOTP)",
|
|
853
|
+
"1. Scan this QR code in your Authenticator app": "1. Scan this QR code in your Authenticator app",
|
|
854
|
+
"2. Enter the six-digit code generated in your Authenticator app": "2. Enter the six-digit code generated in your Authenticator app",
|
|
855
|
+
"Code": "Code",
|
|
856
|
+
"Two-factor authentication with Time-based One-Time Password enabled": "Two-factor authentication with Time-based One-Time Password enabled",
|
|
857
|
+
"Two-factor authentication": "Two-factor authentication",
|
|
858
|
+
"Two-factor authentication is enabled": "Two-factor authentication is enabled",
|
|
859
|
+
"Two-factor authentication with Time-based One-Time Password disabled": "Two-factor authentication with Time-based One-Time Password disabled",
|
|
860
|
+
"Two-factor authentication is disabled": "Two-factor authentication is disabled",
|
|
861
|
+
"Auto save": "Auto save",
|
|
862
|
+
"Save any changes immediately": "Save any changes immediately"
|
|
849
863
|
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "0.6.3
|
|
3
|
+
"version": "0.6.3",
|
|
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.6.3
|
|
10
|
-
"@saltcorn/builder": "0.6.3
|
|
11
|
-
"@saltcorn/data": "0.6.3
|
|
9
|
+
"@saltcorn/base-plugin": "0.6.3",
|
|
10
|
+
"@saltcorn/builder": "0.6.3",
|
|
11
|
+
"@saltcorn/data": "0.6.3",
|
|
12
12
|
"greenlock-express": "^4.0.3",
|
|
13
|
-
"@saltcorn/markup": "0.6.3
|
|
14
|
-
"@saltcorn/sbadmin2": "0.6.3
|
|
13
|
+
"@saltcorn/markup": "0.6.3",
|
|
14
|
+
"@saltcorn/sbadmin2": "0.6.3",
|
|
15
15
|
"@socket.io/cluster-adapter": "^0.1.0",
|
|
16
16
|
"@socket.io/sticky": "^1.0.1",
|
|
17
17
|
"connect-flash": "^0.1.1",
|
|
@@ -34,13 +34,17 @@
|
|
|
34
34
|
"moment": "^2.27.0",
|
|
35
35
|
"node-fetch": "2.6.2",
|
|
36
36
|
"node-watch": "^0.7.2",
|
|
37
|
+
"notp": "2.0.3",
|
|
37
38
|
"passport": "^0.4.1",
|
|
38
39
|
"passport-custom": "^1.1.1",
|
|
39
40
|
"passport-http-bearer": "^1.0.1",
|
|
41
|
+
"passport-totp": "0.0.2",
|
|
40
42
|
"pg": "^8.2.1",
|
|
41
43
|
"pluralize": "^8.0.0",
|
|
44
|
+
"qrcode": "1.5.0",
|
|
42
45
|
"socket.io": "4.2.0",
|
|
43
46
|
"tmp-promise": "^3.0.2",
|
|
47
|
+
"thirty-two": "1.0.2",
|
|
44
48
|
"multer-s3": "^2.10.0",
|
|
45
49
|
"multer": "^1.4.3",
|
|
46
50
|
"aws-sdk": "^2.1037.0",
|
|
@@ -75,6 +79,7 @@
|
|
|
75
79
|
"@saltcorn/sqlite/(.*)": "@saltcorn/sqlite/dist/$1",
|
|
76
80
|
"@saltcorn/db-common/(.*)": "@saltcorn/db-common/dist/$1",
|
|
77
81
|
"@saltcorn/data/(.*)": "@saltcorn/data/dist/$1",
|
|
82
|
+
"@saltcorn/types/(.*)": "@saltcorn/types/dist/$1",
|
|
78
83
|
"@saltcorn/markup$": "@saltcorn/markup/dist",
|
|
79
84
|
"@saltcorn/markup/(.*)": "@saltcorn/markup/dist/$1"
|
|
80
85
|
}
|
package/public/gridedit.js
CHANGED
|
@@ -179,7 +179,9 @@ DateField.prototype = new jsGrid.Field({
|
|
|
179
179
|
setTimeout(function () {
|
|
180
180
|
flatpickr(insertPicker, {
|
|
181
181
|
enableTime: true,
|
|
182
|
-
dateFormat: "
|
|
182
|
+
dateFormat: "Z",
|
|
183
|
+
altInput: true,
|
|
184
|
+
altFormat: "Y-m-d h:i K",
|
|
183
185
|
});
|
|
184
186
|
});
|
|
185
187
|
return insertPicker;
|
|
@@ -192,7 +194,9 @@ DateField.prototype = new jsGrid.Field({
|
|
|
192
194
|
setTimeout(function () {
|
|
193
195
|
flatpickr(editPicker, {
|
|
194
196
|
enableTime: true,
|
|
195
|
-
dateFormat: "
|
|
197
|
+
dateFormat: "Z",
|
|
198
|
+
altInput: true,
|
|
199
|
+
altFormat: "Y-m-d h:i K",
|
|
196
200
|
});
|
|
197
201
|
});
|
|
198
202
|
return editPicker;
|
package/public/saltcorn.js
CHANGED
|
@@ -30,9 +30,14 @@ function add_repeater(nm) {
|
|
|
30
30
|
function apply_showif() {
|
|
31
31
|
$("[data-show-if]").each(function (ix, element) {
|
|
32
32
|
var e = $(element);
|
|
33
|
-
var to_show = new Function(
|
|
33
|
+
var to_show = new Function(
|
|
34
|
+
"e",
|
|
35
|
+
"return " + decodeURIComponent(e.attr("data-show-if"))
|
|
36
|
+
);
|
|
34
37
|
if (to_show(e))
|
|
35
|
-
e.show()
|
|
38
|
+
e.show()
|
|
39
|
+
.find("input, textarea, button, select")
|
|
40
|
+
.prop("disabled", e.attr("data-disabled") || false);
|
|
36
41
|
else
|
|
37
42
|
e.hide().find("input, textarea, button, select").prop("disabled", true);
|
|
38
43
|
});
|
|
@@ -508,6 +513,33 @@ function ajax_modal(url, opts = {}) {
|
|
|
508
513
|
},
|
|
509
514
|
});
|
|
510
515
|
}
|
|
516
|
+
|
|
517
|
+
function saveAndContinue(e) {
|
|
518
|
+
var form = $(e).closest("form");
|
|
519
|
+
var url = form.attr("action");
|
|
520
|
+
var form_data = form.serialize();
|
|
521
|
+
$.ajax(url, {
|
|
522
|
+
type: "POST",
|
|
523
|
+
headers: {
|
|
524
|
+
"CSRF-Token": _sc_globalCsrf,
|
|
525
|
+
},
|
|
526
|
+
data: form_data,
|
|
527
|
+
success: function (res) {
|
|
528
|
+
if (res.id && form.find("input[name=id")) {
|
|
529
|
+
form.append(
|
|
530
|
+
`<input type="hidden" class="form-control " name="id" value="${res.id}">`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
error: function (request) {
|
|
535
|
+
$("#page-inner-content").html(request.responseText);
|
|
536
|
+
initialize_page();
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
|
|
511
543
|
function ajaxSubmitForm(e) {
|
|
512
544
|
var form = $(e).closest("form");
|
|
513
545
|
var url = form.attr("action");
|
package/restart_watcher.js
CHANGED
|
@@ -120,6 +120,9 @@ const listenForChanges = (projectDirs, pluginDirs) => {
|
|
|
120
120
|
(event, file) => {
|
|
121
121
|
console.log("'%s' changed \n re-starting now", file);
|
|
122
122
|
closeWatchers();
|
|
123
|
+
spawnSync("npm", ["run", "tsc"], {
|
|
124
|
+
stdio: "inherit",
|
|
125
|
+
});
|
|
123
126
|
process.exit();
|
|
124
127
|
}
|
|
125
128
|
)
|
package/routes/admin.js
CHANGED
package/routes/api.js
CHANGED
|
@@ -65,11 +65,12 @@ const limitFields = (fields) => (r) => {
|
|
|
65
65
|
* @returns {boolean}
|
|
66
66
|
*/
|
|
67
67
|
function accessAllowedRead(req, user, table) {
|
|
68
|
-
const role =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
const role =
|
|
69
|
+
req.user && req.user.id
|
|
70
|
+
? req.user.role_id
|
|
71
|
+
: user && user.role_id
|
|
72
|
+
? user.role_id
|
|
73
|
+
: 10;
|
|
73
74
|
|
|
74
75
|
return role <= table.min_role_read;
|
|
75
76
|
}
|
|
@@ -82,11 +83,12 @@ function accessAllowedRead(req, user, table) {
|
|
|
82
83
|
* @returns {boolean}
|
|
83
84
|
*/
|
|
84
85
|
function accessAllowedWrite(req, user, table) {
|
|
85
|
-
const role =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
const role =
|
|
87
|
+
req.user && req.user.id
|
|
88
|
+
? req.user.role_id
|
|
89
|
+
: user && user.role_id
|
|
90
|
+
? user.role_id
|
|
91
|
+
: 10;
|
|
90
92
|
|
|
91
93
|
return role <= table.min_role_write;
|
|
92
94
|
}
|
|
@@ -98,11 +100,12 @@ function accessAllowedWrite(req, user, table) {
|
|
|
98
100
|
* @returns {boolean}
|
|
99
101
|
*/
|
|
100
102
|
function accessAllowed(req, user, trigger) {
|
|
101
|
-
const role =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
const role =
|
|
104
|
+
req.user && req.user.id
|
|
105
|
+
? req.user.role_id
|
|
106
|
+
: user && user.role_id
|
|
107
|
+
? user.role_id
|
|
108
|
+
: 10;
|
|
106
109
|
|
|
107
110
|
return role <= trigger.min_role;
|
|
108
111
|
}
|
package/routes/delete.js
CHANGED
|
@@ -33,7 +33,7 @@ router.post(
|
|
|
33
33
|
const { name, id } = req.params;
|
|
34
34
|
const { redirect } = req.query;
|
|
35
35
|
const table = await Table.findOne({ name });
|
|
36
|
-
const role = req.
|
|
36
|
+
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
37
37
|
try {
|
|
38
38
|
if (role <= table.min_role_write) await table.deleteRows({ id });
|
|
39
39
|
else if (table.ownership_field_id && req.user) {
|
package/routes/edit.js
CHANGED
|
@@ -37,7 +37,7 @@ router.post(
|
|
|
37
37
|
const { name, id, field_name } = req.params;
|
|
38
38
|
const { redirect } = req.query;
|
|
39
39
|
const table = await Table.findOne({ name });
|
|
40
|
-
const role = req.
|
|
40
|
+
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
41
41
|
if (role <= table.min_role_write) await table.toggleBool(+id, field_name);
|
|
42
42
|
else
|
|
43
43
|
req.flash(
|
package/routes/fields.js
CHANGED
|
@@ -26,6 +26,7 @@ const { isAdmin, error_catcher } = require("./utils.js");
|
|
|
26
26
|
const expressionBlurb = require("../markup/expression_blurb");
|
|
27
27
|
const { readState } = require("@saltcorn/data/plugin-helper");
|
|
28
28
|
const { wizardCardTitle } = require("../markup/forms.js");
|
|
29
|
+
const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* @type {object}
|
|
@@ -161,8 +162,9 @@ const translateAttributes = (attrs, req) =>
|
|
|
161
162
|
* @returns {object}
|
|
162
163
|
*/
|
|
163
164
|
const translateAttribute = (attr, req) => {
|
|
164
|
-
|
|
165
|
+
let res = { ...attr };
|
|
165
166
|
if (res.sublabel) res.sublabel = req.__(res.sublabel);
|
|
167
|
+
if (res.isRepeat) res = new FieldRepeat(res);
|
|
166
168
|
return res;
|
|
167
169
|
};
|
|
168
170
|
|
|
@@ -660,7 +662,15 @@ router.post(
|
|
|
660
662
|
if (fieldName.includes(".")) {
|
|
661
663
|
const [refNm, targetNm] = fieldName.split(".");
|
|
662
664
|
const ref = fields.find((f) => f.name === refNm);
|
|
665
|
+
if (!ref) {
|
|
666
|
+
res.send("");
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
663
669
|
const reftable = await Table.findOne({ name: ref.reftable_name });
|
|
670
|
+
if (!reftable) {
|
|
671
|
+
res.send("");
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
664
674
|
const reffields = await reftable.getFields();
|
|
665
675
|
field = reffields.find((f) => f.name === targetNm);
|
|
666
676
|
row = await reftable.getRow({});
|
package/routes/files.js
CHANGED
|
@@ -136,7 +136,7 @@ router.get(
|
|
|
136
136
|
router.get(
|
|
137
137
|
"/download/:id",
|
|
138
138
|
error_catcher(async (req, res) => {
|
|
139
|
-
const role = req.
|
|
139
|
+
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
140
140
|
const user_id = req.user && req.user.id;
|
|
141
141
|
const { id } = req.params;
|
|
142
142
|
const file = await File.findOne({ id });
|
|
@@ -160,7 +160,7 @@ router.get(
|
|
|
160
160
|
router.get(
|
|
161
161
|
"/serve/:id",
|
|
162
162
|
error_catcher(async (req, res) => {
|
|
163
|
-
const role = req.
|
|
163
|
+
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
164
164
|
const user_id = req.user && req.user.id;
|
|
165
165
|
const { id } = req.params;
|
|
166
166
|
let file;
|
|
@@ -243,7 +243,7 @@ router.post(
|
|
|
243
243
|
error_catcher(async (req, res) => {
|
|
244
244
|
let jsonResp = {};
|
|
245
245
|
const min_role_upload = getState().getConfig("min_role_upload", 1);
|
|
246
|
-
const role = req.
|
|
246
|
+
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
247
247
|
if (role > +min_role_upload) {
|
|
248
248
|
if (!req.xhr) req.flash("warning", req.__("Not authorized"));
|
|
249
249
|
else jsonResp = { error: "Not authorized" };
|
package/routes/homepage.js
CHANGED
|
@@ -398,7 +398,7 @@ const welcome_page = async (req) => {
|
|
|
398
398
|
* @returns {Promise<void>}
|
|
399
399
|
*/
|
|
400
400
|
const no_views_logged_in = async (req, res) => {
|
|
401
|
-
const role = req.
|
|
401
|
+
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
402
402
|
if (role > 1 || req.user.tenant !== db.getTenantSchema())
|
|
403
403
|
res.sendWrap(req.__("Hello"), req.__("Welcome to Saltcorn!"));
|
|
404
404
|
else {
|
|
@@ -455,25 +455,26 @@ const get_config_response = async (role_id, res, req) => {
|
|
|
455
455
|
}
|
|
456
456
|
};
|
|
457
457
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
458
|
+
module.exports =
|
|
459
|
+
/**
|
|
460
|
+
* Function assigned to 'module.exports'.
|
|
461
|
+
* @param {object} req
|
|
462
|
+
* @param {object} res
|
|
463
|
+
* @returns {Promise<void>}
|
|
464
|
+
*/
|
|
465
|
+
async (req, res) => {
|
|
466
|
+
const isAuth = req.user && req.user.id;
|
|
467
|
+
const role_id = req.user ? req.user.role_id : 10;
|
|
468
|
+
const cfgResp = await get_config_response(role_id, res, req);
|
|
469
|
+
if (cfgResp) return;
|
|
469
470
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
};
|
|
471
|
+
if (!isAuth) {
|
|
472
|
+
const hasUsers = await User.nonEmpty();
|
|
473
|
+
if (!hasUsers) {
|
|
474
|
+
res.redirect("/auth/create_first_user");
|
|
475
|
+
return;
|
|
476
|
+
} else res.redirect("/auth/login");
|
|
477
|
+
} else {
|
|
478
|
+
await no_views_logged_in(req, res);
|
|
479
|
+
}
|
|
480
|
+
};
|
package/routes/index.js
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* @property {module:routes/utils} utils
|
|
37
37
|
* @property {module:routes/view} view
|
|
38
38
|
* @property {module:routes/viewedit} viewedit
|
|
39
|
-
*
|
|
39
|
+
*
|
|
40
40
|
* @category server
|
|
41
41
|
* @subcategory routes
|
|
42
42
|
*/
|
|
@@ -71,38 +71,39 @@ const useradmin = require("../auth/admin");
|
|
|
71
71
|
const roleadmin = require("../auth/roleadmin");
|
|
72
72
|
const scapi = require("./scapi");
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
app
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
74
|
+
module.exports =
|
|
75
|
+
/**
|
|
76
|
+
* Function assigned to 'module.exports'
|
|
77
|
+
* @returns {void}
|
|
78
|
+
*/
|
|
79
|
+
(app) => {
|
|
80
|
+
app.use("/table", table);
|
|
81
|
+
app.use("/field", field);
|
|
82
|
+
app.use("/files", files);
|
|
83
|
+
app.use("/list", list);
|
|
84
|
+
app.use("/edit", edit);
|
|
85
|
+
app.use("/config", config);
|
|
86
|
+
app.use("/plugins", plugins);
|
|
87
|
+
app.use("/packs", packs);
|
|
88
|
+
app.use("/menu", menu);
|
|
89
|
+
app.use("/view", view);
|
|
90
|
+
app.use("/crashlog", crashlog);
|
|
91
|
+
app.use("/events", events);
|
|
92
|
+
app.use("/page", page);
|
|
93
|
+
app.use("/settings", settings);
|
|
94
|
+
app.use("/pageedit", pageedit);
|
|
95
|
+
app.use("/actions", actions);
|
|
96
|
+
app.use("/eventlog", eventlog);
|
|
97
|
+
app.use("/library", library);
|
|
98
|
+
app.use("/site-structure", infoarch);
|
|
99
|
+
app.use("/search", search);
|
|
100
|
+
app.use("/admin", admin);
|
|
101
|
+
app.use("/tenant", tenant);
|
|
102
|
+
app.use("/api", api);
|
|
103
|
+
app.use("/viewedit", viewedit);
|
|
104
|
+
app.use("/delete", del);
|
|
105
|
+
app.use("/auth", auth);
|
|
106
|
+
app.use("/useradmin", useradmin);
|
|
107
|
+
app.use("/roleadmin", roleadmin);
|
|
108
|
+
app.use("/scapi", scapi);
|
|
109
|
+
};
|
package/routes/list.js
CHANGED
package/routes/page.js
CHANGED
|
@@ -37,7 +37,7 @@ router.get(
|
|
|
37
37
|
error_catcher(async (req, res) => {
|
|
38
38
|
const { pagename } = req.params;
|
|
39
39
|
|
|
40
|
-
const role = req.
|
|
40
|
+
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
41
41
|
const db_page = await Page.findOne({ name: pagename });
|
|
42
42
|
if (db_page && role <= db_page.min_role) {
|
|
43
43
|
const contents = await db_page.run(req.query, { res, req });
|
|
@@ -73,7 +73,7 @@ router.post(
|
|
|
73
73
|
"/:pagename/action/:rndid",
|
|
74
74
|
error_catcher(async (req, res) => {
|
|
75
75
|
const { pagename, rndid } = req.params;
|
|
76
|
-
const role = req.
|
|
76
|
+
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
77
77
|
const db_page = await Page.findOne({ name: pagename });
|
|
78
78
|
if (db_page && role <= db_page.min_role) {
|
|
79
79
|
let col;
|
package/routes/pageedit.js
CHANGED
|
@@ -157,9 +157,12 @@ const pageBuilderData = async (req, context) => {
|
|
|
157
157
|
const images = await File.find({ mime_super: "image" });
|
|
158
158
|
const roles = await User.get_roles();
|
|
159
159
|
const stateActions = getState().actions;
|
|
160
|
-
const actions =
|
|
161
|
-
|
|
162
|
-
.
|
|
160
|
+
const actions = [
|
|
161
|
+
"GoBack",
|
|
162
|
+
...Object.entries(stateActions)
|
|
163
|
+
.filter(([k, v]) => !v.requireRow && !v.disableInBuilder)
|
|
164
|
+
.map(([k, v]) => k),
|
|
165
|
+
];
|
|
163
166
|
const triggers = await Trigger.find({
|
|
164
167
|
when_trigger: { or: ["API call", "Never"] },
|
|
165
168
|
});
|
package/routes/scapi.js
CHANGED
|
@@ -44,11 +44,12 @@ module.exports = router;
|
|
|
44
44
|
* @returns {boolean}
|
|
45
45
|
*/
|
|
46
46
|
function accessAllowedRead(req, user) {
|
|
47
|
-
const role =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
const role =
|
|
48
|
+
req.user && req.user.id
|
|
49
|
+
? req.user.role_id
|
|
50
|
+
: user && user.role_id
|
|
51
|
+
? user.role_id
|
|
52
|
+
: 10;
|
|
52
53
|
|
|
53
54
|
if (role === 1) return true;
|
|
54
55
|
return false;
|
package/routes/utils.js
CHANGED
|
@@ -49,7 +49,13 @@ function isAdmin(req, res, next) {
|
|
|
49
49
|
next();
|
|
50
50
|
} else {
|
|
51
51
|
req.flash("danger", req.__("Must be admin"));
|
|
52
|
-
res.redirect(
|
|
52
|
+
res.redirect(
|
|
53
|
+
req.user && req.user.pending_user
|
|
54
|
+
? "/auth/twofa/login/totp"
|
|
55
|
+
: req.user
|
|
56
|
+
? "/"
|
|
57
|
+
: "/auth/login"
|
|
58
|
+
);
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
|
package/routes/view.js
CHANGED
|
@@ -37,27 +37,28 @@ module.exports = router;
|
|
|
37
37
|
* @function
|
|
38
38
|
*/
|
|
39
39
|
router.get(
|
|
40
|
-
"/:viewname",
|
|
40
|
+
["/:viewname", "/:viewname/*"],
|
|
41
41
|
error_catcher(async (req, res) => {
|
|
42
42
|
const { viewname } = req.params;
|
|
43
|
-
|
|
43
|
+
const query = { ...req.query };
|
|
44
44
|
const view = await View.findOne({ name: viewname });
|
|
45
|
-
const role = req.
|
|
46
|
-
|
|
45
|
+
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
47
46
|
if (!view) {
|
|
48
47
|
req.flash("danger", req.__(`No such view: %s`, text(viewname)));
|
|
49
48
|
res.redirect("/");
|
|
50
49
|
return;
|
|
51
50
|
}
|
|
51
|
+
|
|
52
|
+
view.rewrite_query_from_slug(query, req.params);
|
|
52
53
|
if (
|
|
53
54
|
role > view.min_role &&
|
|
54
|
-
!(await view.authorise_get({ query
|
|
55
|
+
!(await view.authorise_get({ query, req, ...view }))
|
|
55
56
|
) {
|
|
56
57
|
req.flash("danger", req.__("Not authorized"));
|
|
57
58
|
res.redirect("/");
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
60
|
-
const contents = await view.run_possibly_on_page(
|
|
61
|
+
const contents = await view.run_possibly_on_page(query, req, res);
|
|
61
62
|
const title = scan_for_page_title(contents, view.name);
|
|
62
63
|
res.sendWrap(
|
|
63
64
|
title,
|
|
@@ -120,7 +121,7 @@ router.post(
|
|
|
120
121
|
"/:viewname/:route",
|
|
121
122
|
error_catcher(async (req, res) => {
|
|
122
123
|
const { viewname, route } = req.params;
|
|
123
|
-
const role = req.
|
|
124
|
+
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
124
125
|
|
|
125
126
|
const view = await View.findOne({ name: viewname });
|
|
126
127
|
if (!view) {
|
|
@@ -142,16 +143,21 @@ router.post(
|
|
|
142
143
|
* @function
|
|
143
144
|
*/
|
|
144
145
|
router.post(
|
|
145
|
-
"/:viewname",
|
|
146
|
+
["/:viewname", "/:viewname/*"],
|
|
146
147
|
error_catcher(async (req, res) => {
|
|
147
148
|
const { viewname } = req.params;
|
|
148
|
-
const role = req.
|
|
149
|
+
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
150
|
+
const query = { ...req.query };
|
|
149
151
|
|
|
150
152
|
const view = await View.findOne({ name: viewname });
|
|
151
153
|
if (!view) {
|
|
152
154
|
req.flash("danger", req.__(`No such view: %s`, text(viewname)));
|
|
153
155
|
res.redirect("/");
|
|
154
|
-
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
view.rewrite_query_from_slug(query, req.params);
|
|
159
|
+
|
|
160
|
+
if (
|
|
155
161
|
role > view.min_role &&
|
|
156
162
|
!(await view.authorise_post({ body: req.body, req, ...view }))
|
|
157
163
|
) {
|
|
@@ -164,7 +170,7 @@ router.post(
|
|
|
164
170
|
} does not supply a POST handler`
|
|
165
171
|
);
|
|
166
172
|
} else {
|
|
167
|
-
await view.runPost(
|
|
173
|
+
await view.runPost(query, req.body, { res, req });
|
|
168
174
|
}
|
|
169
175
|
})
|
|
170
176
|
);
|
package/routes/viewedit.js
CHANGED
|
@@ -232,12 +232,13 @@ const mapObjectValues = (o, f) =>
|
|
|
232
232
|
* @param {object} values
|
|
233
233
|
* @returns {Form}
|
|
234
234
|
*/
|
|
235
|
-
const viewForm = (req, tableOptions, roles, pages, values) => {
|
|
235
|
+
const viewForm = async (req, tableOptions, roles, pages, values) => {
|
|
236
236
|
const isEdit =
|
|
237
237
|
values && values.id && !getState().getConfig("development_mode", false);
|
|
238
238
|
const hasTable = Object.entries(getState().viewtemplates)
|
|
239
239
|
.filter(([k, v]) => !v.tableless)
|
|
240
240
|
.map(([k, v]) => k);
|
|
241
|
+
const slugOptions = await Table.allSlugOptions();
|
|
241
242
|
return new Form({
|
|
242
243
|
action: "/viewedit/save",
|
|
243
244
|
submitLabel: req.__("Configure") + " »",
|
|
@@ -302,6 +303,19 @@ const viewForm = (req, tableOptions, roles, pages, values) => {
|
|
|
302
303
|
...pages.map((p) => ({ value: p.name, label: p.name })),
|
|
303
304
|
],
|
|
304
305
|
}),
|
|
306
|
+
new Field({
|
|
307
|
+
name: "slug",
|
|
308
|
+
label: req.__("Slug"),
|
|
309
|
+
sublabel: req.__("Field that can be used for a prettier URL structure"),
|
|
310
|
+
type: "String",
|
|
311
|
+
attributes: {
|
|
312
|
+
calcOptions: [
|
|
313
|
+
"table_name",
|
|
314
|
+
mapObjectValues(slugOptions, (lvs) => lvs.map((lv) => lv.label)),
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
showIf: { viewtemplate: hasTable },
|
|
318
|
+
}),
|
|
305
319
|
...(isEdit
|
|
306
320
|
? [
|
|
307
321
|
new Field({
|
|
@@ -342,10 +356,15 @@ router.get(
|
|
|
342
356
|
(t) => t.id === viewrow.table_id || t.name === viewrow.exttable_name
|
|
343
357
|
);
|
|
344
358
|
viewrow.table_name = currentTable && currentTable.name;
|
|
359
|
+
if (viewrow.slug && currentTable) {
|
|
360
|
+
const slugOptions = await currentTable.slug_options();
|
|
361
|
+
const slug = slugOptions.find((so) => so.label === viewrow.slug.label);
|
|
362
|
+
if (slug) viewrow.slug = slug.label;
|
|
363
|
+
}
|
|
345
364
|
const tableOptions = tables.map((t) => t.name);
|
|
346
365
|
const roles = await User.get_roles();
|
|
347
366
|
const pages = await Page.find();
|
|
348
|
-
const form = viewForm(req, tableOptions, roles, pages, viewrow);
|
|
367
|
+
const form = await viewForm(req, tableOptions, roles, pages, viewrow);
|
|
349
368
|
form.hidden("id");
|
|
350
369
|
res.sendWrap(req.__(`Edit view`), {
|
|
351
370
|
above: [
|
|
@@ -380,7 +399,7 @@ router.get(
|
|
|
380
399
|
const tableOptions = tables.map((t) => t.name);
|
|
381
400
|
const roles = await User.get_roles();
|
|
382
401
|
const pages = await Page.find();
|
|
383
|
-
const form = viewForm(req, tableOptions, roles, pages);
|
|
402
|
+
const form = await viewForm(req, tableOptions, roles, pages);
|
|
384
403
|
if (req.query && req.query.table) {
|
|
385
404
|
form.values.table_name = req.query.table;
|
|
386
405
|
}
|
|
@@ -417,7 +436,7 @@ router.post(
|
|
|
417
436
|
const tableOptions = tables.map((t) => t.name);
|
|
418
437
|
const roles = await User.get_roles();
|
|
419
438
|
const pages = await Page.find();
|
|
420
|
-
const form = viewForm(req, tableOptions, roles, pages);
|
|
439
|
+
const form = await viewForm(req, tableOptions, roles, pages);
|
|
421
440
|
const result = form.validate(req.body);
|
|
422
441
|
|
|
423
442
|
const sendForm = (form) => {
|
|
@@ -458,11 +477,18 @@ router.post(
|
|
|
458
477
|
const v = result.success;
|
|
459
478
|
if (v.table_name) {
|
|
460
479
|
const table = await Table.findOne({ name: v.table_name });
|
|
461
|
-
if (table && table.id)
|
|
462
|
-
|
|
480
|
+
if (table && table.id) {
|
|
481
|
+
v.table_id = table.id;
|
|
482
|
+
} else if (table && table.external) v.exttable_name = v.table_name;
|
|
463
483
|
}
|
|
484
|
+
if (v.table_id) {
|
|
485
|
+
const table = await Table.findOne({ id: v.table_id });
|
|
486
|
+
const slugOptions = await table.slug_options();
|
|
487
|
+
const slug = slugOptions.find((so) => so.label === v.slug);
|
|
488
|
+
v.slug = slug || null;
|
|
489
|
+
}
|
|
490
|
+
const table = await Table.findOne({ name: v.table_name });
|
|
464
491
|
delete v.table_name;
|
|
465
|
-
|
|
466
492
|
if (req.body.id) {
|
|
467
493
|
await View.update(v, +req.body.id);
|
|
468
494
|
} else {
|
package/serve.js
CHANGED
|
@@ -163,9 +163,6 @@ module.exports =
|
|
|
163
163
|
...appargs
|
|
164
164
|
} = {}) => {
|
|
165
165
|
if (dev && cluster.isMaster) {
|
|
166
|
-
spawnSync("npm", ["run", "tsc"], {
|
|
167
|
-
stdio: "inherit",
|
|
168
|
-
});
|
|
169
166
|
listenForChanges(getRelevantPackages(), await getPluginDirectories());
|
|
170
167
|
}
|
|
171
168
|
const useNCpus = process.env.SALTCORN_NWORKERS
|
package/tests/view.test.js
CHANGED
|
@@ -137,3 +137,41 @@ describe("render view on page", () => {
|
|
|
137
137
|
.expect(toNotInclude("Herman Melville"));
|
|
138
138
|
});
|
|
139
139
|
});
|
|
140
|
+
|
|
141
|
+
describe("render view with slug", () => {
|
|
142
|
+
it("should show with id slug in list", async () => {
|
|
143
|
+
const view = await View.findOne({ name: "authorshow" });
|
|
144
|
+
const table = await Table.findOne({ name: "books" });
|
|
145
|
+
const slugOpts = await table.slug_options();
|
|
146
|
+
const slugOpt = slugOpts.find((so) => so.label === "/:id");
|
|
147
|
+
expect(!!slugOpt).toBe(true);
|
|
148
|
+
View.update({ default_render_page: null, slug: slugOpt }, view.id);
|
|
149
|
+
const app = await getApp({ disableCsrf: true });
|
|
150
|
+
await request(app)
|
|
151
|
+
.get("/view/authorlist")
|
|
152
|
+
.expect(toInclude(`/view/authorshow/1`));
|
|
153
|
+
await request(app)
|
|
154
|
+
.get("/view/authorshow/1")
|
|
155
|
+
.expect(toInclude(`Herman Melville`));
|
|
156
|
+
});
|
|
157
|
+
it("should show with name slug in list", async () => {
|
|
158
|
+
const view = await View.findOne({ name: "authorshow" });
|
|
159
|
+
const table0 = await Table.findOne({ name: "books" });
|
|
160
|
+
const fields = await table0.getFields();
|
|
161
|
+
const field = fields.find((f) => f.name === "author");
|
|
162
|
+
await field.update({ is_unique: true });
|
|
163
|
+
const table = await Table.findOne({ name: "books" });
|
|
164
|
+
|
|
165
|
+
const slugOpts = await table.slug_options();
|
|
166
|
+
const slugOpt = slugOpts.find((so) => so.label === "/slugify-author");
|
|
167
|
+
expect(!!slugOpt).toBe(true);
|
|
168
|
+
View.update({ default_render_page: null, slug: slugOpt }, view.id);
|
|
169
|
+
const app = await getApp({ disableCsrf: true });
|
|
170
|
+
await request(app)
|
|
171
|
+
.get("/view/authorlist")
|
|
172
|
+
.expect(toInclude(`/view/authorshow/herman-melville`));
|
|
173
|
+
await request(app)
|
|
174
|
+
.get("/view/authorshow/herman-melville")
|
|
175
|
+
.expect(toInclude(`Herman Melville`));
|
|
176
|
+
});
|
|
177
|
+
});
|
package/wrapper.js
CHANGED