@saltcorn/server 0.6.3-beta.2 → 0.6.3-beta.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 +860 -850
- package/package.json +10 -6
- package/public/gridedit.js +6 -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 +2 -2
- package/routes/page.js +2 -2
- package/routes/scapi.js +6 -5
- package/routes/utils.js +7 -1
- package/routes/view.js +3 -4
- package/serve.js +0 -3
- 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
|
+
);
|