@saltcorn/server 0.7.4 → 0.8.0-beta.1

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.
Files changed (50) hide show
  1. package/app.js +18 -11
  2. package/auth/admin.js +370 -120
  3. package/auth/roleadmin.js +5 -23
  4. package/auth/routes.js +40 -15
  5. package/locales/de.json +1049 -273
  6. package/locales/en.json +58 -3
  7. package/locales/es.json +134 -134
  8. package/locales/it.json +6 -1
  9. package/locales/ru.json +44 -7
  10. package/markup/admin.js +46 -42
  11. package/markup/forms.js +4 -3
  12. package/package.json +8 -7
  13. package/public/blockly.js +19 -31
  14. package/public/diagram_utils.js +530 -0
  15. package/public/gridedit.js +4 -1
  16. package/public/jquery-menu-editor.min.js +112 -112
  17. package/public/saltcorn-common.js +31 -8
  18. package/public/saltcorn.css +11 -0
  19. package/public/saltcorn.js +211 -70
  20. package/restart_watcher.js +1 -0
  21. package/routes/actions.js +6 -14
  22. package/routes/admin.js +229 -79
  23. package/routes/api.js +19 -2
  24. package/routes/common_lists.js +137 -134
  25. package/routes/delete.js +6 -5
  26. package/routes/diagram.js +43 -117
  27. package/routes/edit.js +5 -10
  28. package/routes/fields.js +63 -29
  29. package/routes/files.js +137 -101
  30. package/routes/homepage.js +2 -2
  31. package/routes/infoarch.js +2 -2
  32. package/routes/list.js +12 -13
  33. package/routes/page.js +16 -3
  34. package/routes/pageedit.js +13 -8
  35. package/routes/scapi.js +1 -1
  36. package/routes/search.js +1 -1
  37. package/routes/tables.js +9 -14
  38. package/routes/tag_entries.js +31 -10
  39. package/routes/tags.js +10 -10
  40. package/routes/tenant.js +114 -50
  41. package/routes/utils.js +12 -0
  42. package/routes/view.js +3 -4
  43. package/routes/viewedit.js +57 -55
  44. package/serve.js +5 -0
  45. package/tests/admin.test.js +6 -2
  46. package/tests/auth.test.js +20 -0
  47. package/tests/fields.test.js +1 -0
  48. package/tests/files.test.js +11 -20
  49. package/tests/tenant.test.js +12 -2
  50. package/tests/viewedit.test.js +15 -1
package/auth/admin.js CHANGED
@@ -5,9 +5,6 @@
5
5
  * @subcategory auth
6
6
  */
7
7
  // todo refactor to few modules + rename to be in sync with router url
8
- /**
9
- * @type {module:express-promise-router}
10
- */
11
8
  const Router = require("express-promise-router");
12
9
  const { contract, is } = require("contractis");
13
10
 
@@ -24,10 +21,10 @@ const {
24
21
  settingsDropdown,
25
22
  post_dropdown_item,
26
23
  } = require("@saltcorn/markup");
27
- const { isAdmin, setTenant, error_catcher } = require("../routes/utils");
24
+ const { isAdmin, error_catcher } = require("../routes/utils");
28
25
  const { send_reset_email } = require("./resetpw");
29
26
  const { getState } = require("@saltcorn/data/db/state");
30
- const { a, div, text, span, code, h5, i, p } = require("@saltcorn/markup/tags");
27
+ const { a, div, span, code, h5, i, p } = require("@saltcorn/markup/tags");
31
28
  const Table = require("@saltcorn/data/models/table");
32
29
  const {
33
30
  send_users_page,
@@ -38,7 +35,9 @@ const {
38
35
  is_hsts_tld,
39
36
  } = require("../markup/admin");
40
37
  const { send_verification_email } = require("@saltcorn/data/models/email");
41
-
38
+ const {
39
+ expressionValidator,
40
+ } = require("@saltcorn/data/models/expression");
42
41
  /**
43
42
  * @type {object}
44
43
  * @const
@@ -177,33 +176,33 @@ const user_dropdown = (user, req, can_reset) =>
177
176
  req
178
177
  ),
179
178
  can_reset &&
180
- post_dropdown_item(
181
- `/useradmin/reset-password/${user.id}`,
182
- '<i class="fas fa-envelope"></i>&nbsp;' +
183
- req.__("Send password reset email"),
184
- req
185
- ),
179
+ post_dropdown_item(
180
+ `/useradmin/reset-password/${user.id}`,
181
+ '<i class="fas fa-envelope"></i>&nbsp;' +
182
+ req.__("Send password reset email"),
183
+ req
184
+ ),
186
185
  can_reset &&
187
- !user.verified_on &&
188
- getState().getConfig("verification_view", "") &&
189
- post_dropdown_item(
190
- `/useradmin/send-verification/${user.id}`,
191
- '<i class="fas fa-envelope"></i>&nbsp;' +
192
- req.__("Send verification email"),
193
- req
194
- ),
186
+ !user.verified_on &&
187
+ getState().getConfig("verification_view", "") &&
188
+ post_dropdown_item(
189
+ `/useradmin/send-verification/${user.id}`,
190
+ '<i class="fas fa-envelope"></i>&nbsp;' +
191
+ req.__("Send verification email"),
192
+ req
193
+ ),
195
194
  user.disabled &&
196
- post_dropdown_item(
197
- `/useradmin/enable/${user.id}`,
198
- '<i class="fas fa-play"></i>&nbsp;' + req.__("Enable"),
199
- req
200
- ),
195
+ post_dropdown_item(
196
+ `/useradmin/enable/${user.id}`,
197
+ '<i class="fas fa-play"></i>&nbsp;' + req.__("Enable"),
198
+ req
199
+ ),
201
200
  !user.disabled &&
202
- post_dropdown_item(
203
- `/useradmin/disable/${user.id}`,
204
- '<i class="fas fa-pause"></i>&nbsp;' + req.__("Disable"),
205
- req
206
- ),
201
+ post_dropdown_item(
202
+ `/useradmin/disable/${user.id}`,
203
+ '<i class="fas fa-pause"></i>&nbsp;' + req.__("Disable"),
204
+ req
205
+ ),
207
206
  div({ class: "dropdown-divider" }),
208
207
  post_dropdown_item(
209
208
  `/useradmin/delete/${user.id}`,
@@ -215,6 +214,7 @@ const user_dropdown = (user, req, can_reset) =>
215
214
  ]);
216
215
 
217
216
  /**
217
+ * Users List (HTTP Get)
218
218
  * @name get
219
219
  * @function
220
220
  * @memberof module:auth/admin~auth/adminRouter
@@ -225,8 +225,8 @@ router.get(
225
225
  error_catcher(async (req, res) => {
226
226
  const users = await User.find({}, { orderBy: "id" });
227
227
  const roles = await User.get_roles();
228
- var roleMap = {};
229
- roles.forEach((r) => {
228
+ let roleMap = {};
229
+ roles.forEach(r => {
230
230
  roleMap[r.id] = r.role;
231
231
  });
232
232
  const can_reset = getState().getConfig("smtp_host", "") !== "";
@@ -257,8 +257,8 @@ router.get(
257
257
  key: (r) =>
258
258
  !!r.verified_on
259
259
  ? i({
260
- class: "fas fa-check-circle text-success",
261
- })
260
+ class: "fas fa-check-circle text-success",
261
+ })
262
262
  : "",
263
263
  },
264
264
  { label: req.__("Role"), key: (r) => roleMap[r.role_id] },
@@ -303,37 +303,67 @@ router.get(
303
303
  );
304
304
 
305
305
  /**
306
- *
306
+ * Authentication Setting Form
307
+ * @param {object} req
308
+ * @returns {Form}
309
+ */
310
+ const auth_settings_form = async (req) =>
311
+ await config_fields_form({
312
+ req,
313
+ field_names: [
314
+ "allow_signup",
315
+ "login_menu",
316
+ "allow_forgot",
317
+ "new_user_form",
318
+ "login_form",
319
+ "signup_form",
320
+ "user_settings_form",
321
+ "verification_view",
322
+ "elevate_verified",
323
+ "email_mask",
324
+ ],
325
+ action: "/useradmin/settings",
326
+ submitLabel: req.__("Save"),
327
+ });
328
+
329
+ /**
330
+ * HTTP Settings Form
331
+ * @param {object} req
332
+ * @returns {Form}
333
+ */
334
+ const http_settings_form = async (req) =>
335
+ await config_fields_form({
336
+ req,
337
+ field_names: [
338
+ "timeout",
339
+ "cookie_duration",
340
+ "cookie_duration_remember",
341
+ "cookie_sessions",
342
+ "custom_http_headers",
343
+ ],
344
+ action: "/useradmin/http",
345
+ submitLabel: req.__("Save"),
346
+ });
347
+
348
+
349
+ /**
350
+ * Permissions Setting Form
307
351
  * @param {object} req
308
352
  * @returns {Form}
309
353
  */
310
- const user_settings_form = (req) =>
311
- config_fields_form({
312
- req,
313
- field_names: [
314
- "allow_signup",
315
- "login_menu",
316
- "new_user_form",
317
- "login_form",
318
- "signup_form",
319
- "user_settings_form",
320
- "verification_view",
321
- "elevate_verified",
322
- "min_role_upload",
323
- "min_role_apikeygen",
324
- "timeout",
325
- "email_mask",
326
- "allow_forgot",
327
- "cookie_duration",
328
- "cookie_duration_remember",
329
- "cookie_sessions",
330
- "custom_http_headers",
331
- ],
332
- action: "/useradmin/settings",
333
- submitLabel: req.__("Save"),
334
- });
354
+ const permissions_settings_form = async (req) =>
355
+ await config_fields_form({
356
+ req,
357
+ field_names: [
358
+ "min_role_upload",
359
+ "min_role_apikeygen",
360
+ ],
361
+ action: "/useradmin/permissions",
362
+ submitLabel: req.__("Save"),
363
+ });
335
364
 
336
365
  /**
366
+ * HTTP GET for /useradmin/settings
337
367
  * @name get/settings
338
368
  * @function
339
369
  * @memberof module:auth/admin~auth/adminRouter
@@ -342,7 +372,7 @@ router.get(
342
372
  "/settings",
343
373
  isAdmin,
344
374
  error_catcher(async (req, res) => {
345
- const form = await user_settings_form(req);
375
+ const form = await auth_settings_form(req);
346
376
  send_users_page({
347
377
  res,
348
378
  req,
@@ -357,6 +387,7 @@ router.get(
357
387
  );
358
388
 
359
389
  /**
390
+ * HTTP POST for /useradmin/settings
360
391
  * @name post/settings
361
392
  * @function
362
393
  * @memberof module:auth/admin~auth/adminRouter
@@ -365,7 +396,7 @@ router.post(
365
396
  "/settings",
366
397
  isAdmin,
367
398
  error_catcher(async (req, res) => {
368
- const form = await user_settings_form(req);
399
+ const form = await auth_settings_form(req);
369
400
  form.validate(req.body);
370
401
  if (form.hasErrors) {
371
402
  send_users_page({
@@ -380,7 +411,7 @@ router.post(
380
411
  });
381
412
  } else {
382
413
  await save_config_from_form(form);
383
- req.flash("success", req.__("User settings updated"));
414
+ req.flash("success", req.__("Authentication settings updated"));
384
415
  if (!req.xhr) res.redirect("/useradmin/settings");
385
416
  else res.json({ success: "ok" });
386
417
  }
@@ -388,6 +419,119 @@ router.post(
388
419
  );
389
420
 
390
421
  /**
422
+ * HTTP GET for /useradmin/http
423
+ * @name get/settings
424
+ * @function
425
+ * @memberof module:auth/admin~auth/adminRouter
426
+ */
427
+ router.get(
428
+ "/http",
429
+ isAdmin,
430
+ error_catcher(async (req, res) => {
431
+ const form = await http_settings_form(req);
432
+ send_users_page({
433
+ res,
434
+ req,
435
+ active_sub: "HTTP",
436
+ contents: {
437
+ type: "card",
438
+ title: req.__("HTTP settings"),
439
+ contents: [renderForm(form, req.csrfToken())],
440
+ },
441
+ });
442
+ })
443
+ );
444
+
445
+ /**
446
+ * HTTP POST for /useradmin/http
447
+ * @name post/settings
448
+ * @function
449
+ * @memberof module:auth/admin~auth/adminRouter
450
+ */
451
+ router.post(
452
+ "/http",
453
+ isAdmin,
454
+ error_catcher(async (req, res) => {
455
+ const form = await http_settings_form(req);
456
+ form.validate(req.body);
457
+ if (form.hasErrors) {
458
+ send_users_page({
459
+ res,
460
+ req,
461
+ active_sub: "HTTP",
462
+ contents: {
463
+ type: "card",
464
+ title: req.__("HTTP settings"),
465
+ contents: [renderForm(form, req.csrfToken())],
466
+ },
467
+ });
468
+ } else {
469
+ await save_config_from_form(form);
470
+ req.flash("success", req.__("HTTP settings updated"));
471
+ if (!req.xhr) res.redirect("/useradmin/http");
472
+ else res.json({ success: "ok" });
473
+ }
474
+ })
475
+ );
476
+
477
+ /**
478
+ * HTTP GET for /useradmin/permissions
479
+ * @name get/settings
480
+ * @function
481
+ * @memberof module:auth/admin~auth/adminRouter
482
+ */
483
+ router.get(
484
+ "/permissions",
485
+ isAdmin,
486
+ error_catcher(async (req, res) => {
487
+ const form = await permissions_settings_form(req);
488
+ send_users_page({
489
+ res,
490
+ req,
491
+ active_sub: "Permissions",
492
+ contents: {
493
+ type: "card",
494
+ title: req.__("Permissions settings"),
495
+ contents: [renderForm(form, req.csrfToken())],
496
+ },
497
+ });
498
+ })
499
+ );
500
+
501
+ /**
502
+ * HTTP POST for /useradmin/permissions
503
+ * @name post/settings
504
+ * @function
505
+ * @memberof module:auth/admin~auth/adminRouter
506
+ */
507
+ router.post(
508
+ "/permissions",
509
+ isAdmin,
510
+ error_catcher(async (req, res) => {
511
+ const form = await permissions_settings_form(req);
512
+ form.validate(req.body);
513
+ if (form.hasErrors) {
514
+ send_users_page({
515
+ res,
516
+ req,
517
+ active_sub: "Permissions",
518
+ contents: {
519
+ type: "card",
520
+ title: req.__("Permissions settings"),
521
+ contents: [renderForm(form, req.csrfToken())],
522
+ },
523
+ });
524
+ } else {
525
+ await save_config_from_form(form);
526
+ req.flash("success", req.__("Permissions settings updated"));
527
+ if (!req.xhr) res.redirect("/useradmin/permissions");
528
+ else res.json({ success: "ok" });
529
+ }
530
+ })
531
+ );
532
+
533
+ /**
534
+ * HTTP GET for /useradmin/ssl
391
535
  * @name get/ssl
392
536
  * @function
393
537
  * @memberof module:auth/admin~auth/adminRouter
@@ -421,15 +565,15 @@ router.get(
421
565
  above: [
422
566
  ...(letsencrypt && has_custom
423
567
  ? [
424
- {
425
- type: "card",
426
- contents: p(
427
- req.__(
428
- "You have enabled both Let's Encrypt certificates and custom SSL certificates. Let's Encrypt takes priority and the custom certificates will be ignored."
429
- )
430
- ),
431
- },
432
- ]
568
+ {
569
+ type: "card",
570
+ contents: p(
571
+ req.__(
572
+ "You have enabled both Let's Encrypt certificates and custom SSL certificates. Let's Encrypt takes priority and the custom certificates will be ignored."
573
+ )
574
+ ),
575
+ },
576
+ ]
433
577
  : []),
434
578
  {
435
579
  type: "card",
@@ -450,33 +594,33 @@ router.get(
450
594
  ),
451
595
  letsencrypt
452
596
  ? post_btn(
453
- "/config/delete/letsencrypt",
454
- req.__("Disable LetsEncrypt HTTPS"),
455
- req.csrfToken(),
456
- { btnClass: "btn-danger", req }
457
- )
597
+ "/config/delete/letsencrypt",
598
+ req.__("Disable LetsEncrypt HTTPS"),
599
+ req.csrfToken(),
600
+ { btnClass: "btn-danger", req }
601
+ )
458
602
  : post_btn(
459
- "/admin/enable-letsencrypt",
460
- req.__("Enable LetsEncrypt HTTPS"),
461
- req.csrfToken(),
462
- { confirm: true, req }
463
- ),
603
+ "/admin/enable-letsencrypt",
604
+ req.__("Enable LetsEncrypt HTTPS"),
605
+ req.csrfToken(),
606
+ { confirm: true, req }
607
+ ),
464
608
  !letsencrypt &&
465
- show_warning &&
466
- !has_custom &&
467
- div(
468
- { class: "mt-3 alert alert-danger" },
469
- p(
470
- req.__(
471
- "The address you are using to reach Saltcorn does not match the Base URL."
472
- )
473
- ),
474
- p(
475
- req.__(
476
- "The DNS A records (for * and @, or a subdomain) should point to this server's IP address before enabling LetsEncrypt"
477
- )
609
+ show_warning &&
610
+ !has_custom &&
611
+ div(
612
+ { class: "mt-3 alert alert-danger" },
613
+ p(
614
+ req.__(
615
+ "The address you are using to reach Saltcorn does not match the Base URL."
478
616
  )
479
617
  ),
618
+ p(
619
+ req.__(
620
+ "The DNS A records (for * and @, or a subdomain) should point to this server's IP address before enabling LetsEncrypt"
621
+ )
622
+ )
623
+ ),
480
624
  ],
481
625
  },
482
626
  {
@@ -508,17 +652,19 @@ router.get(
508
652
  );
509
653
 
510
654
  /**
655
+ * SSL Setting form
511
656
  * @param {object} req
512
657
  * @returns {Form}
513
658
  */
514
- const ssl_form = (req) =>
515
- config_fields_form({
516
- req,
517
- field_names: ["custom_ssl_certificate", "custom_ssl_private_key"],
518
- action: "/useradmin/ssl/custom",
519
- });
659
+ const ssl_form = async (req) =>
660
+ await config_fields_form({
661
+ req,
662
+ field_names: ["custom_ssl_certificate", "custom_ssl_private_key"],
663
+ action: "/useradmin/ssl/custom",
664
+ });
520
665
 
521
666
  /**
667
+ * HTTP GET for /useradmin/ssl/custom
522
668
  * @name get/ssl/custom
523
669
  * @function
524
670
  * @memberof module:auth/admin~auth/adminRouter
@@ -543,6 +689,7 @@ router.get(
543
689
  );
544
690
 
545
691
  /**
692
+ * HTTP POST for /useradmin/ssl/custom
546
693
  * @name post/ssl/custom
547
694
  * @function
548
695
  * @memberof module:auth/admin~auth/adminRouter
@@ -570,8 +717,8 @@ router.post(
570
717
  req.flash(
571
718
  "success",
572
719
  req.__("Custom SSL enabled. Restart for changes to take effect.") +
573
- " " +
574
- a({ href: "/admin/system" }, req.__("Restart here"))
720
+ " " +
721
+ a({ href: "/admin/system" }, req.__("Restart here"))
575
722
  );
576
723
  if (!req.xhr) {
577
724
  res.redirect("/useradmin/ssl");
@@ -580,6 +727,104 @@ router.post(
580
727
  })
581
728
  );
582
729
 
730
+ /**
731
+ * HTTP GET for /useradmin/table-access
732
+ * @name get/ssl/custom
733
+ * @function
734
+ * @memberof module:auth/admin~auth/adminRouter
735
+ */
736
+ router.get(
737
+ "/table-access",
738
+ isAdmin,
739
+ error_catcher(async (req, res) => {
740
+ const tables = await Table.find()
741
+ const roleOptions = (await User.get_roles()).map((r) => ({
742
+ value: r.id,
743
+ label: r.role,
744
+ }));
745
+
746
+ const contents = []
747
+ for (const table of tables) {
748
+ if (table.external) continue
749
+ const fields = await table.getFields();
750
+ const userFields = fields
751
+ .filter((f) => f.reftable_name === "users")
752
+ .map((f) => ({ value: f.id, label: f.name }));
753
+ const form = new Form({
754
+ action: "/table",
755
+ noSubmitButton: true,
756
+ onChange: "saveAndContinue(this)",
757
+ fields: [
758
+ {
759
+ label: req.__("Ownership field"),
760
+ name: "ownership_field_id",
761
+ sublabel: req.__(
762
+ "The user referred to in this field will be the owner of the row"
763
+ ),
764
+ input_type: "select",
765
+ options: [
766
+ { value: "", label: req.__("None") },
767
+ ...userFields,
768
+ { value: "_formula", label: req.__("Formula") },
769
+ ],
770
+ },
771
+ {
772
+ name: "ownership_formula",
773
+ label: req.__("Ownership formula"),
774
+ validator: expressionValidator,
775
+ type: "String",
776
+ class: "validate-expression",
777
+ sublabel:
778
+ req.__("User is treated as owner if true. In scope: ") +
779
+ ["user", ...fields.map((f) => f.name)]
780
+ .map((fn) => code(fn))
781
+ .join(", "),
782
+ showIf: { ownership_field_id: "_formula" },
783
+ },
784
+ {
785
+ label: req.__("Minimum role to read"),
786
+ sublabel: req.__(
787
+ "User must have this role or higher to read rows from the table, unless they are the owner"
788
+ ),
789
+ name: "min_role_read",
790
+ input_type: "select",
791
+ options: roleOptions,
792
+ attributes: { asideNext: true }
793
+ },
794
+ {
795
+ label: req.__("Minimum role to write"),
796
+ name: "min_role_write",
797
+ input_type: "select",
798
+ sublabel: req.__(
799
+ "User must have this role or higher to edit or create new rows in the table, unless they are the owner"
800
+ ),
801
+ options: roleOptions,
802
+ },
803
+ ]
804
+ })
805
+ form.hidden("id", "name");
806
+ form.values = table
807
+ if (table.ownership_formula && !table.ownership_field_id)
808
+ form.values.ownership_field_id = "_formula";
809
+ contents.push(div(
810
+ h5(a({ href: `/table/${table.id}` }, table.name)),
811
+ renderForm(form, req.csrfToken())
812
+ ))
813
+ }
814
+ send_users_page({
815
+ res,
816
+ req,
817
+ active_sub: "Table access",
818
+ contents: {
819
+ type: "card",
820
+ title: req.__("Table access"),
821
+ contents
822
+ },
823
+ });
824
+ })
825
+ );
826
+
827
+
583
828
  /**
584
829
  * @name get/:id
585
830
  * @function
@@ -613,9 +858,9 @@ router.get(
613
858
  div(
614
859
  user.api_token
615
860
  ? span(
616
- { class: "me-1" },
617
- req.__("API token for this user: ")
618
- ) + code(user.api_token)
861
+ { class: "me-1" },
862
+ req.__("API token for this user: ")
863
+ ) + code(user.api_token)
619
864
  : req.__("No API token issued")
620
865
  ),
621
866
  // button for reset or generate api token
@@ -629,16 +874,16 @@ router.get(
629
874
  ),
630
875
  // button for remove api token
631
876
  user.api_token &&
632
- div(
633
- { class: "mt-4 ms-2 d-inline-block" },
634
- post_btn(
635
- `/useradmin/remove-api-token/${user.id}`,
636
- // TBD localization
637
- user.api_token ? req.__("Remove") : req.__("Generate"),
638
- req.csrfToken(),
639
- { req: req, confirm: true }
640
- )
641
- ),
877
+ div(
878
+ { class: "mt-4 ms-2 d-inline-block" },
879
+ post_btn(
880
+ `/useradmin/remove-api-token/${user.id}`,
881
+ // TBD localization
882
+ user.api_token ? req.__("Remove") : req.__("Generate"),
883
+ req.csrfToken(),
884
+ { req: req, confirm: true }
885
+ )
886
+ ),
642
887
  ],
643
888
  },
644
889
  ],
@@ -707,7 +952,7 @@ router.post(
707
952
  role_id: +role_id,
708
953
  ...rest,
709
954
  });
710
- // refactored to catch user errors errors and stop processing if any errors
955
+ // refactored to catch user errors and stop processing if any errors
711
956
  if (u.error) {
712
957
  req.flash("error", u.error); // todo change to prompt near field like done for views
713
958
  // todo return to create user form
@@ -727,7 +972,7 @@ router.post(
727
972
  );
728
973
 
729
974
  /**
730
- * Reset password for yser
975
+ * Reset password for user
731
976
  * @name post/reset-password/:id
732
977
  * @function
733
978
  * @memberof module:auth/admin~auth/adminRouter
@@ -757,8 +1002,13 @@ router.post(
757
1002
  error_catcher(async (req, res) => {
758
1003
  const { id } = req.params;
759
1004
  const u = await User.findOne({ id });
760
- const result = await send_verification_email(u);
761
- if (result.error) req.flash("danger", result.error);
1005
+ // todo add test case
1006
+ const result = await send_verification_email(u, req);
1007
+ if (result.error)
1008
+ req.flash(
1009
+ "danger",
1010
+ req.__(`Verification email sender error:`, result.error)
1011
+ );
762
1012
  else
763
1013
  req.flash(
764
1014
  "success",