@saltcorn/server 1.1.0-beta.0 → 1.1.0-beta.10

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