@saltcorn/server 0.8.0-beta.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/auth/index.js CHANGED
@@ -1,20 +1,20 @@
1
- // for the jsdoc documentation
2
- /**
3
- *
4
- * @category server
5
- * @module auth/index
6
- * @subcategory auth
7
- */
8
-
9
- /**
10
- * All files in the auth module.
11
- * @namespace auth_overview
12
- * @property {module:auth/admin} admin
13
- * @property {module:auth/resetpw} resetpw
14
- * @property {module:auth/roleadmin} roleadmin
15
- * @property {module:auth/routes} routes
16
- * @property {module:auth/testhelp} testhelp
17
- *
18
- * @category server
19
- * @subcategory auth
20
- */
1
+ // for the jsdoc documentation
2
+ /**
3
+ *
4
+ * @category server
5
+ * @module auth/index
6
+ * @subcategory auth
7
+ */
8
+
9
+ /**
10
+ * All files in the auth module.
11
+ * @namespace auth_overview
12
+ * @property {module:auth/admin} admin
13
+ * @property {module:auth/resetpw} resetpw
14
+ * @property {module:auth/roleadmin} roleadmin
15
+ * @property {module:auth/routes} routes
16
+ * @property {module:auth/testhelp} testhelp
17
+ *
18
+ * @category server
19
+ * @subcategory auth
20
+ */
package/auth/roleadmin.js CHANGED
@@ -17,15 +17,8 @@ const {
17
17
  } = require("@saltcorn/markup");
18
18
  const { isAdmin, error_catcher, csrfField } = require("../routes/utils");
19
19
  const { getState } = require("@saltcorn/data/db/state");
20
- const {
21
- text,
22
- form,
23
- option,
24
- select,
25
- } = require("@saltcorn/markup/tags");
26
- const {
27
- send_users_page,
28
- } = require("../markup/admin");
20
+ const { text, form, option, select } = require("@saltcorn/markup/tags");
21
+ const { send_users_page } = require("../markup/admin");
29
22
 
30
23
  /**
31
24
  * @type {object}
package/auth/routes.js CHANGED
@@ -52,6 +52,9 @@ const rateLimit = require("express-rate-limit");
52
52
  const moment = require("moment");
53
53
  const View = require("@saltcorn/data/models/view");
54
54
  const Table = require("@saltcorn/data/models/table");
55
+ const {
56
+ getForm,
57
+ } = require("@saltcorn/data/base-plugin/viewtemplates/viewable_fields");
55
58
  const { InvalidConfiguration } = require("@saltcorn/data/utils");
56
59
  const Trigger = require("@saltcorn/data/models/trigger");
57
60
  const { restore_backup } = require("../markup/admin.js");
@@ -75,6 +78,7 @@ const router = new Router();
75
78
  module.exports = router;
76
79
 
77
80
  /**
81
+ * Login Form
78
82
  * @param {object} req
79
83
  * @param {boolean} isCreating
80
84
  * @returns {Form}
@@ -100,7 +104,8 @@ const loginForm = (req, isCreating) => {
100
104
  input_type: "email",
101
105
  },
102
106
  sublabel: user_sublabel || undefined,
103
- validator: (s) => s.length < 128,
107
+ // fixed correct size of email is 254 https://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
108
+ validator: (s) => s.length < 255,
104
109
  }),
105
110
  new Field({
106
111
  label: req.__("Password"),
@@ -117,6 +122,7 @@ const loginForm = (req, isCreating) => {
117
122
  };
118
123
 
119
124
  /**
125
+ * Forgot Form
120
126
  * @param {object} req
121
127
  * @returns {Form}
122
128
  */
@@ -133,7 +139,8 @@ const forgotForm = (req) =>
133
139
  attributes: {
134
140
  input_type: "email",
135
141
  },
136
- validator: (s) => s.length < 128,
142
+ // fixed correct size of email is 254 https://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
143
+ validator: (s) => s.length < 255,
137
144
  }),
138
145
  ],
139
146
  action: "/auth/forgot",
@@ -141,6 +148,7 @@ const forgotForm = (req) =>
141
148
  });
142
149
 
143
150
  /**
151
+ * Reset Form
144
152
  * @param {object} body
145
153
  * @param {object} req
146
154
  * @returns {Form}
@@ -172,6 +180,7 @@ const resetForm = (body, req) => {
172
180
  };
173
181
 
174
182
  /**
183
+ * get Auth Links
175
184
  * @param {string} current
176
185
  * @param {boolean} noMethods
177
186
  * @returns {object}
@@ -198,7 +207,13 @@ const getAuthLinks = (current, noMethods) => {
198
207
  });
199
208
  return links;
200
209
  };
201
-
210
+ /**
211
+ * Login with jwt
212
+ * @param {*} email
213
+ * @param {*} password
214
+ * @param {*} saltcornApp
215
+ * @param {*} res
216
+ */
202
217
  const loginWithJwt = async (email, password, saltcornApp, res) => {
203
218
  const loginFn = async () => {
204
219
  const publicUserLink = getState().getConfig("public_user_link");
@@ -263,6 +278,7 @@ const loginWithJwt = async (email, password, saltcornApp, res) => {
263
278
  };
264
279
 
265
280
  /**
281
+ * GET /auth/login
266
282
  * @name get/login
267
283
  * @function
268
284
  * @memberof module:auth/routes~routesRouter
@@ -290,6 +306,7 @@ router.get(
290
306
  );
291
307
 
292
308
  /**
309
+ * GET /auth/logout
293
310
  * @name get/logout
294
311
  * @function
295
312
  * @memberof module:auth/routes~routesRouter
@@ -433,6 +450,23 @@ router.post(
433
450
  })
434
451
  );
435
452
 
453
+ const default_signup_form = async (req) => {
454
+ const form = loginForm(req, true);
455
+ const new_user_form = getState().getConfig("new_user_form", "");
456
+ if (!new_user_form) {
457
+ const userTable = await Table.findOne({ name: "users" });
458
+ const userFields = await userTable.getFields();
459
+
460
+ for (const f of userFields) {
461
+ if (f.required && !f.calculated && !["id", "email"].includes(f.name))
462
+ form.fields.push(f);
463
+ }
464
+ }
465
+ form.action = "/auth/signup";
466
+ form.submitLabel = req.__("Sign up");
467
+ return form;
468
+ };
469
+
436
470
  /**
437
471
  * @name get/signup
438
472
  * @function
@@ -447,19 +481,7 @@ router.get(
447
481
  return;
448
482
  }
449
483
  const defaultSignup = async () => {
450
- const form = loginForm(req, true);
451
- const new_user_form = getState().getConfig("new_user_form", "");
452
- if (!new_user_form) {
453
- const userTable = await Table.findOne({ name: "users" });
454
- const userFields = await userTable.getFields();
455
-
456
- for (const f of userFields) {
457
- if (f.required && !f.calculated && !["id", "email"].includes(f.name))
458
- form.fields.push(f);
459
- }
460
- }
461
- form.action = "/auth/signup";
462
- form.submitLabel = req.__("Sign up");
484
+ const form = await default_signup_form(req);
463
485
  res.sendAuthWrap(req.__(`Sign up`), form, getAuthLinks("signup"));
464
486
  };
465
487
  const signup_form_name = getState().getConfig("signup_form", "");
@@ -503,11 +525,11 @@ router.get(
503
525
  form,
504
526
  {},
505
527
  restore +
506
- script(
507
- domReady(
508
- `$('form.create-first-user button[type=submit]').click(function(){press_store_button(this)})`
528
+ script(
529
+ domReady(
530
+ `$('form.create-first-user button[type=submit]').click(function(){press_store_button(this)})`
531
+ )
509
532
  )
510
- )
511
533
  );
512
534
  } else {
513
535
  req.flash("danger", req.__("Users already present"));
@@ -536,7 +558,7 @@ router.post(
536
558
  );
537
559
  if (err) req.flash("error", err);
538
560
  else req.flash("success", req.__("Successfully restored backup"));
539
- fs.unlink(newPath, function () { });
561
+ fs.unlink(newPath, function () {});
540
562
  res.redirect(`/auth/login`);
541
563
  } else {
542
564
  req.flash("danger", req.__("Users already present"));
@@ -568,18 +590,15 @@ router.post(
568
590
  } else {
569
591
  const { email, password } = form.values;
570
592
  const u = await User.create({ email, password, role_id: 1 });
571
- req.login(
572
- u.session_object,
573
- function (err) {
574
- if (!err) {
575
- Trigger.emitEvent("Login", null, u);
576
- res.redirect("/");
577
- } else {
578
- req.flash("danger", err);
579
- res.redirect("/auth/signup");
580
- }
593
+ req.login(u.session_object, function (err) {
594
+ if (!err) {
595
+ Trigger.emitEvent("Login", null, u);
596
+ res.redirect("/");
597
+ } else {
598
+ req.flash("danger", err);
599
+ res.redirect("/auth/signup");
581
600
  }
582
- );
601
+ });
583
602
  }
584
603
  } else {
585
604
  req.flash("danger", req.__("Users already present"));
@@ -673,21 +692,18 @@ const getNewUserForm = async (new_user_view_name, req, askEmail) => {
673
692
  * @returns {void}
674
693
  */
675
694
  const signup_login_with_user = (u, req, res) =>
676
- req.login(
677
- u.session_object,
678
- function (err) {
679
- if (!err) {
680
- Trigger.emitEvent("Login", null, u);
681
- if (getState().verifier) res.redirect("/auth/verification-flow");
682
- else if (getState().get2FApolicy(u) === "Mandatory")
683
- res.redirect("/auth/twofa/setup/totp");
684
- else res.redirect("/");
685
- } else {
686
- req.flash("danger", err);
687
- res.redirect("/auth/signup");
688
- }
695
+ req.login(u.session_object, function (err) {
696
+ if (!err) {
697
+ Trigger.emitEvent("Login", null, u);
698
+ if (getState().verifier) res.redirect("/auth/verification-flow");
699
+ else if (getState().get2FApolicy(u) === "Mandatory")
700
+ res.redirect("/auth/twofa/setup/totp");
701
+ else res.redirect("/");
702
+ } else {
703
+ req.flash("danger", err);
704
+ res.redirect("/auth/signup");
689
705
  }
690
- );
706
+ });
691
707
 
692
708
  /**
693
709
  * @name get/signup_final_ext
@@ -717,6 +733,7 @@ router.get(
717
733
  */
718
734
  router.post(
719
735
  "/signup_final_ext",
736
+ setTenant,
720
737
  error_catcher(async (req, res) => {
721
738
  const new_user_form = getState().getConfig("new_user_form");
722
739
  if (!req.user || req.user.id || !new_user_form) {
@@ -727,7 +744,7 @@ router.post(
727
744
  const form = await getNewUserForm(new_user_form, req, !req.user.email);
728
745
  form.action = "/auth/signup_final_ext";
729
746
 
730
- form.validate(req.body);
747
+ await form.asyncValidate(req.body);
731
748
  if (form.hasErrors) {
732
749
  res.sendAuthWrap(new_user_form, form, getAuthLinks("signup", true));
733
750
  return;
@@ -769,6 +786,7 @@ router.post(
769
786
  */
770
787
  router.post(
771
788
  "/signup_final",
789
+ setTenant,
772
790
  error_catcher(async (req, res) => {
773
791
  if (getState().getConfig("allow_signup")) {
774
792
  const new_user_form = getState().getConfig("new_user_form");
@@ -787,7 +805,7 @@ router.post(
787
805
  });
788
806
  }
789
807
  }
790
- form.validate(req.body);
808
+ await form.asyncValidate(req.body);
791
809
  if (form.hasErrors) {
792
810
  res.sendAuthWrap(new_user_form, form, getAuthLinks("signup", true));
793
811
  } else if (form.values.email && !check_email_mask(form.values.email)) {
@@ -830,6 +848,7 @@ router.post(
830
848
  */
831
849
  router.post(
832
850
  "/signup",
851
+ setTenant,
833
852
  error_catcher(async (req, res) => {
834
853
  if (!getState().getConfig("allow_signup")) {
835
854
  req.flash("danger", req.__("Signups not enabled"));
@@ -884,16 +903,33 @@ router.post(
884
903
 
885
904
  const signup_form_name = getState().getConfig("signup_form", "");
886
905
  if (signup_form_name) {
887
- const signup_form = await View.findOne({ name: signup_form_name });
888
- if (signup_form) {
889
- const userObject = {};
890
- signup_form.configuration.columns.forEach((col) => {
891
- if (col.type === "Field") {
892
- if (col.field_name === "passwordRepeat")
893
- userObject[col.field_name] = req.body[col.field_name] || "";
894
- else userObject[col.field_name] = req.body[col.field_name];
895
- }
906
+ const signup_view = await View.findOne({ name: signup_form_name });
907
+ if (signup_view) {
908
+ const signup_form = await getForm(
909
+ Table.findOne({ name: "users" }),
910
+ signup_form_name,
911
+ signup_view.configuration.columns,
912
+ signup_view.configuration.layout,
913
+ undefined,
914
+ req,
915
+ false
916
+ );
917
+ await signup_form.asyncValidate(req.body);
918
+ if (signup_form.hasErrors) {
919
+ signup_form.action = "/auth/signup";
920
+ res.sendAuthWrap(
921
+ req.__(`Sign up`),
922
+ signup_form,
923
+ getAuthLinks("signup")
924
+ );
925
+ return;
926
+ }
927
+ //ensure pw repeat is set if used.
928
+ signup_form.fields.forEach((f) => {
929
+ if (f.name === "passwordRepeat")
930
+ signup_form.values[f.name] = signup_form.values[f.name] || "";
896
931
  });
932
+ const userObject = signup_form.values;
897
933
  const { email, password, passwordRepeat } = userObject;
898
934
  if (await unsuitableEmailPassword(email, password, passwordRepeat))
899
935
  return;
@@ -914,8 +950,8 @@ router.post(
914
950
  }
915
951
  }
916
952
 
917
- const form = loginForm(req, true);
918
- form.validate(req.body);
953
+ const form = await default_signup_form(req);
954
+ await form.asyncValidate(req.body);
919
955
 
920
956
  if (form.hasErrors) {
921
957
  form.action = "/auth/signup";
@@ -930,7 +966,7 @@ router.post(
930
966
  form.values.password = password;
931
967
  res.sendAuthWrap(new_user_form, form, getAuthLinks("signup", true));
932
968
  } else {
933
- const u = await User.create({ email, password });
969
+ const u = await User.create(form.values);
934
970
  await send_verification_email(u, req);
935
971
  if (req.smr)
936
972
  await loginWithJwt(
@@ -959,7 +995,7 @@ function handler(req, res) {
959
995
  req.flash(
960
996
  "error",
961
997
  "You've made too many failed attempts in a short period of time, please try again " +
962
- moment(req.rateLimit.resetTime).fromNow()
998
+ moment(req.rateLimit.resetTime).fromNow()
963
999
  );
964
1000
  res.redirect("/auth/login"); // brute force protection triggered, send them back to the login page
965
1001
  }
@@ -992,6 +1028,7 @@ const userLimiter = rateLimit({
992
1028
  });
993
1029
 
994
1030
  /**
1031
+ * POST /auth/login
995
1032
  * @name post/login
996
1033
  * @function
997
1034
  * @memberof module:auth/routes~routesRouter
@@ -1183,7 +1220,10 @@ const setLanguageForm = (req, user) =>
1183
1220
  option(
1184
1221
  {
1185
1222
  value: locale,
1186
- ...(((user && user.language === locale) || (user && !user.language && req.getLocale() === locale)) && { selected: true }),
1223
+ ...(((user && user.language === locale) ||
1224
+ (user && !user.language && req.getLocale() === locale)) && {
1225
+ selected: true,
1226
+ }),
1187
1227
  },
1188
1228
  language
1189
1229
  )
@@ -1223,7 +1263,7 @@ const userSettings = async ({ req, res, pwform, user }) => {
1223
1263
  div(
1224
1264
  user.api_token
1225
1265
  ? span({ class: "me-1" }, req.__("API token for this user: ")) +
1226
- code(user.api_token)
1266
+ code(user.api_token)
1227
1267
  : req.__("No API token issued")
1228
1268
  ),
1229
1269
  // button for reset or generate api token
@@ -1237,16 +1277,16 @@ const userSettings = async ({ req, res, pwform, user }) => {
1237
1277
  ),
1238
1278
  // button for remove api token
1239
1279
  user.api_token &&
1240
- div(
1241
- { class: "mt-4 ms-2 d-inline-block" },
1242
- post_btn(
1243
- `/auth/remove-api-token`,
1244
- // TBD localization
1245
- user.api_token ? req.__("Remove") : req.__("Generate"),
1246
- req.csrfToken(),
1247
- { req: req, confirm: true }
1248
- )
1249
- ),
1280
+ div(
1281
+ { class: "mt-4 ms-2 d-inline-block" },
1282
+ post_btn(
1283
+ `/auth/remove-api-token`,
1284
+ // TBD localization
1285
+ user.api_token ? req.__("Remove") : req.__("Generate"),
1286
+ req.csrfToken(),
1287
+ { req: req, confirm: true }
1288
+ )
1289
+ ),
1250
1290
  ],
1251
1291
  };
1252
1292
  return {
@@ -1257,13 +1297,13 @@ const userSettings = async ({ req, res, pwform, user }) => {
1257
1297
  },
1258
1298
  ...(usersets
1259
1299
  ? [
1260
- {
1261
- type: "card",
1262
- class: "mt-0",
1263
- title: userSetsName,
1264
- contents: usersets,
1265
- },
1266
- ]
1300
+ {
1301
+ type: "card",
1302
+ class: "mt-0",
1303
+ title: userSetsName,
1304
+ contents: usersets,
1305
+ },
1306
+ ]
1267
1307
  : []),
1268
1308
  {
1269
1309
  type: "card",
@@ -1286,35 +1326,35 @@ const userSettings = async ({ req, res, pwform, user }) => {
1286
1326
  },
1287
1327
  ...(show2FAPolicy
1288
1328
  ? [
1289
- {
1290
- type: "card",
1291
- title: req.__("Two-factor authentication"),
1292
- contents: [
1293
- div(
1294
- user._attributes.totp_enabled
1295
- ? req.__("Two-factor authentication is enabled")
1296
- : req.__("Two-factor authentication is disabled")
1297
- ),
1298
- div(
1299
- user._attributes.totp_enabled
1300
- ? a(
1301
- {
1302
- href: "/auth/twofa/disable/totp",
1303
- class: "btn btn-danger mt-2",
1304
- },
1305
- req.__("Disable TWA")
1306
- )
1307
- : a(
1308
- {
1309
- href: "/auth/twofa/setup/totp",
1310
- class: "btn btn-primary mt-2",
1311
- },
1312
- req.__("Enable TWA")
1313
- )
1314
- ),
1315
- ],
1316
- },
1317
- ]
1329
+ {
1330
+ type: "card",
1331
+ title: req.__("Two-factor authentication"),
1332
+ contents: [
1333
+ div(
1334
+ user._attributes.totp_enabled
1335
+ ? req.__("Two-factor authentication is enabled")
1336
+ : req.__("Two-factor authentication is disabled")
1337
+ ),
1338
+ div(
1339
+ user._attributes.totp_enabled
1340
+ ? a(
1341
+ {
1342
+ href: "/auth/twofa/disable/totp",
1343
+ class: "btn btn-danger mt-2",
1344
+ },
1345
+ req.__("Disable TWA")
1346
+ )
1347
+ : a(
1348
+ {
1349
+ href: "/auth/twofa/setup/totp",
1350
+ class: "btn btn-primary mt-2",
1351
+ },
1352
+ req.__("Enable TWA")
1353
+ )
1354
+ ),
1355
+ ],
1356
+ },
1357
+ ]
1318
1358
  : []),
1319
1359
  ...(apikeycard ? [apikeycard] : []),
1320
1360
  ],
@@ -1371,18 +1411,15 @@ router.post(
1371
1411
  const newlang = available_languages[req.body.locale];
1372
1412
  if (newlang && u) {
1373
1413
  await u.set_language(req.body.locale);
1374
- req.login(
1375
- u.session_object,
1376
- function (err) {
1377
- if (!err) {
1378
- req.flash("success", req.__("Language changed to %s", newlang));
1379
- res.redirect("/auth/settings");
1380
- } else {
1381
- req.flash("danger", err);
1382
- res.redirect("/auth/settings");
1383
- }
1414
+ req.login(u.session_object, function (err) {
1415
+ if (!err) {
1416
+ req.flash("success", req.__("Language changed to %s", newlang));
1417
+ res.redirect("/auth/settings");
1418
+ } else {
1419
+ req.flash("danger", err);
1420
+ res.redirect("/auth/settings");
1384
1421
  }
1385
- );
1422
+ });
1386
1423
  } else {
1387
1424
  req.flash("danger", req.__("Language not found"));
1388
1425
  res.redirect("/auth/settings");
@@ -1479,19 +1516,16 @@ router.post(
1479
1516
  const u = await User.findForSession({ id: req.user.id });
1480
1517
  await u.update({ email: form.values.email });
1481
1518
  u.email = form.values.email;
1482
- req.login(
1483
- u.session_object,
1484
- function (err) {
1485
- if (!err) {
1486
- Trigger.emitEvent("Login", null, u);
1487
- req.flash("success", req.__("Welcome, %s!", u.email));
1488
- res.redirect("/");
1489
- } else {
1490
- req.flash("danger", err);
1491
- res.redirect("/");
1492
- }
1519
+ req.login(u.session_object, function (err) {
1520
+ if (!err) {
1521
+ Trigger.emitEvent("Login", null, u);
1522
+ req.flash("success", req.__("Welcome, %s!", u.email));
1523
+ res.redirect("/");
1524
+ } else {
1525
+ req.flash("danger", err);
1526
+ res.redirect("/");
1493
1527
  }
1494
- );
1528
+ });
1495
1529
  })
1496
1530
  );
1497
1531
 
@@ -1608,8 +1642,9 @@ router.get(
1608
1642
  // generate QR code for scanning into Google Authenticator
1609
1643
  // reference: https://code.google.com/p/google-authenticator/wiki/KeyUriFormat
1610
1644
  const site_name = getState().getConfig("site_name");
1611
- const otpUrl = `otpauth://totp/${user.email
1612
- }?secret=${encodedKey}&period=30&issuer=${encodeURIComponent(site_name)}`;
1645
+ const otpUrl = `otpauth://totp/${
1646
+ user.email
1647
+ }?secret=${encodedKey}&period=30&issuer=${encodeURIComponent(site_name)}`;
1613
1648
  const image = await qrcode.toDataURL(otpUrl);
1614
1649
  res.sendWrap(req.__("Setup two-factor authentication"), {
1615
1650
  type: "card",
@@ -1677,7 +1712,9 @@ router.post(
1677
1712
  res.redirect("/auth/settings");
1678
1713
  })
1679
1714
  );
1680
-
1715
+ /**
1716
+ * GET /twofa/disable/totp
1717
+ */
1681
1718
  router.get(
1682
1719
  "/twofa/disable/totp",
1683
1720
  loggedIn,
@@ -1692,7 +1729,10 @@ router.get(
1692
1729
  });
1693
1730
  })
1694
1731
  );
1695
-
1732
+ /**
1733
+ * POST /twofa/disable/totp
1734
+ * Disable TWA
1735
+ */
1696
1736
  router.post(
1697
1737
  "/twofa/disable/totp",
1698
1738
  loggedIn,
@@ -1726,6 +1766,12 @@ router.post(
1726
1766
  res.redirect("/auth/settings");
1727
1767
  })
1728
1768
  );
1769
+ /**
1770
+ * totpForm (TWA)
1771
+ * @param {*} req
1772
+ * @param {*} action
1773
+ * @returns
1774
+ */
1729
1775
  const totpForm = (req, action) =>
1730
1776
  new Form({
1731
1777
  action: action || "/auth/twofa/setup/totp",
@@ -1738,7 +1784,11 @@ const totpForm = (req, action) =>
1738
1784
  },
1739
1785
  ],
1740
1786
  });
1741
-
1787
+ /**
1788
+ * Random key generation for totp (TWA)
1789
+ * @param {*} len
1790
+ * @returns
1791
+ */
1742
1792
  const randomKey = function (len) {
1743
1793
  function getRandomInt(min, max) {
1744
1794
  return Math.floor(Math.random() * (max - min + 1)) + min;
@@ -1753,7 +1803,9 @@ const randomKey = function (len) {
1753
1803
 
1754
1804
  return buf.join("");
1755
1805
  };
1756
-
1806
+ /**
1807
+ * GET /twofa/login/totp
1808
+ */
1757
1809
  router.get(
1758
1810
  "/twofa/login/totp",
1759
1811
  error_catcher(async (req, res) => {
@@ -1772,7 +1824,9 @@ router.get(
1772
1824
  res.sendAuthWrap(req.__(`Two-factor authentication`), form, {});
1773
1825
  })
1774
1826
  );
1775
-
1827
+ /**
1828
+ * POST /twofa/login/totp
1829
+ */
1776
1830
  router.post(
1777
1831
  "/twofa/login/totp",
1778
1832
  passport.authenticate("totp", {