@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/routes/admin.js CHANGED
@@ -97,13 +97,6 @@ const { getConfigFile } = require("@saltcorn/data/db/connect");
97
97
  const os = require("os");
98
98
  const Page = require("@saltcorn/data/models/page");
99
99
 
100
- /**
101
- * @type {object}
102
- * @const
103
- * @namespace routes/adminRouter
104
- * @category server
105
- * @subcategory routes
106
- */
107
100
  const router = new Router();
108
101
  module.exports = router;
109
102
 
@@ -123,9 +116,6 @@ const site_id_form = (req) =>
123
116
  "base_url",
124
117
  "page_custom_css",
125
118
  "page_custom_html",
126
- "development_mode",
127
- "log_sql",
128
- "log_level",
129
119
  "plugins_store_endpoint",
130
120
  "packs_store_endpoint",
131
121
  ...(getConfigFile() ? ["multitenancy_enabled"] : []),
@@ -139,7 +129,7 @@ const site_id_form = (req) =>
139
129
  * @returns {Promise<Form>} form
140
130
  */
141
131
  const email_form = async (req) => {
142
- const form = await config_fields_form({
132
+ return await config_fields_form({
143
133
  req,
144
134
  field_names: [
145
135
  "smtp_host",
@@ -151,10 +141,9 @@ const email_form = async (req) => {
151
141
  ],
152
142
  action: "/admin/email",
153
143
  });
154
- return form;
155
144
  };
156
145
 
157
- const app_files_table = (files, req) =>
146
+ const app_files_table = (files, buildDirName, req) =>
158
147
  mkTable(
159
148
  [
160
149
  {
@@ -163,9 +152,21 @@ const app_files_table = (files, req) =>
163
152
  },
164
153
  { label: req.__("Size (KiB)"), key: "size_kb", align: "right" },
165
154
  { label: req.__("Media type"), key: (r) => r.mimetype },
155
+ {
156
+ label: req.__("Open"),
157
+ key: (r) =>
158
+ link(
159
+ `/files/serve/mobile_app/${buildDirName}/${r.filename}`,
160
+ req.__("Open")
161
+ ),
162
+ },
166
163
  {
167
164
  label: req.__("Download"),
168
- key: (r) => link(`/files/download/${r.id}`, req.__("Download")),
165
+ key: (r) =>
166
+ link(
167
+ `/files/download/mobile_app/${buildDirName}/${r.filename}`,
168
+ req.__("Download")
169
+ ),
169
170
  },
170
171
  ],
171
172
  files
@@ -734,6 +735,7 @@ router.post(
734
735
  await auto_backup_now();
735
736
  req.flash("success", req.__("Backup successful"));
736
737
  } catch (e) {
738
+ getState().log(1, e);
737
739
  req.flash("error", e.message);
738
740
  }
739
741
  res.json({ reload_page: true });
@@ -873,12 +875,40 @@ router.get(
873
875
  ),
874
876
  tr(th(req.__("Node.js version")), td(process.version)),
875
877
  tr(
876
- th(req.__("Database")),
878
+ th(req.__("Database type")),
877
879
  td(db.isSQLite ? "SQLite " : "PostgreSQL ", dbversion)
878
880
  ),
881
+ (isRoot?
882
+ tr(
883
+ th(req.__("Database host")),
884
+ td(db.connectObj.host)
885
+ )
886
+ : ""),
887
+ (isRoot?
888
+ tr(
889
+ th(req.__("Database port")),
890
+ td(db.connectObj.port)
891
+ )
892
+ : ""),
893
+ (isRoot?
894
+ tr(
895
+ th(req.__("Database name")),
896
+ td(db.connectObj.database)
897
+ )
898
+ : ""),
899
+ (isRoot?
900
+ tr(
901
+ th(req.__("Database user")),
902
+ td(db.connectObj.user)
903
+ )
904
+ : ""),
905
+ tr(
906
+ th(req.__("Database schema")),
907
+ td(db.getTenantSchema())
908
+ ),
879
909
  tr(
880
- th(req.__("Process uptime")),
881
- td(moment(get_process_init_time()).fromNow(true))
910
+ th(req.__("Process uptime")),
911
+ td(moment(get_process_init_time()).fromNow(true))
882
912
  )
883
913
  )
884
914
  ),
@@ -1275,10 +1305,11 @@ const buildDialogScript = () => {
1275
1305
 
1276
1306
  function handleMessages() {
1277
1307
  notifyAlert("This is still under development and might run longer.")
1278
- ${getState().getConfig("apple_team_id") &&
1308
+ ${
1309
+ getState().getConfig("apple_team_id") &&
1279
1310
  getState().getConfig("apple_team_id") !== "null"
1280
- ? ""
1281
- : `
1311
+ ? ""
1312
+ : `
1282
1313
  if ($("#iOSCheckboxId")[0].checked) {
1283
1314
  notifyAlert(
1284
1315
  "No 'Apple Team ID' is configured, I will try to build a project for the iOS simulator."
@@ -1288,7 +1319,9 @@ const buildDialogScript = () => {
1288
1319
  }
1289
1320
  </script>`;
1290
1321
  };
1291
-
1322
+ /**
1323
+ * Build mobile app
1324
+ */
1292
1325
  router.get(
1293
1326
  "/build-mobile-app",
1294
1327
  isAdmin,
@@ -1481,8 +1514,9 @@ router.get(
1481
1514
  ),
1482
1515
  button(
1483
1516
  {
1484
- type: "submit",
1485
- onClick: `handleMessages(); press_store_button(this);`,
1517
+ id: "buildMobileAppBtnId",
1518
+ type: "button",
1519
+ onClick: `build_mobile_app(this);`,
1486
1520
  class: "btn btn-warning",
1487
1521
  },
1488
1522
  i({ class: "fas fa-hammer pe-2" }),
@@ -1498,6 +1532,57 @@ router.get(
1498
1532
  })
1499
1533
  );
1500
1534
 
1535
+ const checkFiles = async (outDir, fileNames) => {
1536
+ const rootFolder = await File.rootFolder();
1537
+ const mobile_app_dir = path.join(rootFolder.location, "mobile_app", outDir);
1538
+ const entries = fs.readdirSync(mobile_app_dir);
1539
+ return fileNames.some((fileName) => entries.indexOf(fileName) >= 0);
1540
+ };
1541
+
1542
+ // check if a build has finished (poll service)
1543
+ router.get(
1544
+ "/build-mobile-app/finished",
1545
+ isAdmin,
1546
+ error_catcher(async (req, res) => {
1547
+ const { build_dir } = req.query;
1548
+ res.json({
1549
+ finished: await checkFiles(build_dir, ["logs.txt", "error_logs.txt"]),
1550
+ });
1551
+ })
1552
+ );
1553
+
1554
+ router.get(
1555
+ "/build-mobile-app/result",
1556
+ isAdmin,
1557
+ error_catcher(async (req, res) => {
1558
+ const { build_dir_name } = req.query;
1559
+ const rootFolder = await File.rootFolder();
1560
+ const buildDir = path.join(
1561
+ rootFolder.location,
1562
+ "mobile_app",
1563
+ build_dir_name
1564
+ );
1565
+ const files = await Promise.all(
1566
+ fs
1567
+ .readdirSync(buildDir)
1568
+ .map(async (outFile) => await File.from_file_on_disk(outFile, buildDir))
1569
+ );
1570
+ const resultMsg = files.find((file) => file.filename === "logs.txt")
1571
+ ? req.__("The build was successfully")
1572
+ : req.__("Unable to build the app");
1573
+ res.sendWrap(req.__(`Admin`), {
1574
+ above: [
1575
+ {
1576
+ type: "card",
1577
+ title: req.__("Build Result"),
1578
+ contents: div(resultMsg),
1579
+ },
1580
+ files.length > 0 ? app_files_table(files, build_dir_name, req) : "",
1581
+ ],
1582
+ });
1583
+ })
1584
+ );
1585
+
1501
1586
  router.post(
1502
1587
  "/build-mobile-app",
1503
1588
  isAdmin,
@@ -1512,24 +1597,27 @@ router.post(
1512
1597
  serverURL,
1513
1598
  } = req.body;
1514
1599
  if (!androidPlatform && !iOSPlatform) {
1515
- req.flash(
1516
- "error",
1517
- req.__("Please select at least one platform (android or iOS).")
1518
- );
1519
- return res.redirect("/admin/build-mobile-app");
1600
+ return res.json({
1601
+ error: req.__("Please select at least one platform (android or iOS)."),
1602
+ });
1520
1603
  }
1521
1604
  if (!androidPlatform && useDocker) {
1522
- req.flash("error", req.__("Only the android build supports docker."));
1523
- return res.redirect("/admin/build-mobile-app");
1605
+ return res.json({
1606
+ error: req.__("Only the android build supports docker."),
1607
+ });
1524
1608
  }
1525
1609
  if (!serverURL || serverURL.length == 0) {
1526
1610
  serverURL = getState().getConfig("base_url") || "";
1527
1611
  }
1528
1612
  if (!serverURL.startsWith("http")) {
1529
- req.flash("error", req.__("Please enter a valid server URL."));
1530
- return res.redirect("/admin/build-mobile-app");
1613
+ return res.json({
1614
+ error: req.__("Please enter a valid server URL."),
1615
+ });
1531
1616
  }
1532
- const appOut = path.join(__dirname, "..", "mobile-app-out");
1617
+ const outDirName = `build_${new Date().valueOf()}`;
1618
+ const rootFolder = await File.rootFolder();
1619
+ const buildDir = path.join(rootFolder.location, "mobile_app", outDirName);
1620
+ await File.new_folder(outDirName, "/mobile_app");
1533
1621
  const spawnParams = [
1534
1622
  "build-app",
1535
1623
  "-e",
@@ -1537,7 +1625,7 @@ router.post(
1537
1625
  "-t",
1538
1626
  entryPointType,
1539
1627
  "-c",
1540
- appOut,
1628
+ buildDir,
1541
1629
  "-b",
1542
1630
  `${os.userInfo().homedir}/mobile_app_build`,
1543
1631
  ];
@@ -1558,6 +1646,9 @@ router.post(
1558
1646
  ) {
1559
1647
  spawnParams.push("--tenantAppName", db.getTenantSchema());
1560
1648
  }
1649
+ // end http call, return the out directory name
1650
+ // the gui polls for results
1651
+ res.json({ build_dir_name: outDirName });
1561
1652
  const child = spawn("saltcorn", spawnParams, {
1562
1653
  stdio: ["ignore", "pipe", "pipe"],
1563
1654
  cwd: ".",
@@ -1572,60 +1663,37 @@ router.post(
1572
1663
  childOutputs.push(data.toString());
1573
1664
  });
1574
1665
  child.on("exit", async function (exitCode, signal) {
1575
- if (exitCode === 0) {
1576
- const files = await Promise.all(
1577
- fs
1578
- .readdirSync(appOut)
1579
- .map(
1580
- async (outFile) =>
1581
- await File.from_existing_file(appOut, outFile, req.user.id)
1582
- )
1583
- );
1584
- res.sendWrap(req.__(`Admin`), {
1585
- above: [
1586
- {
1587
- type: "card",
1588
- title: req.__("Build Result"),
1589
- contents: div(req.__("The build was successfully")),
1590
- },
1591
- files.length > 0 ? app_files_table(files, req) : "",
1592
- ],
1593
- });
1594
- } else
1595
- res.sendWrap(req.__(`Admin`), {
1596
- above: [
1597
- {
1598
- type: "card",
1599
- title: req.__("Build Result"),
1600
- contents: div(
1601
- req.__("Unable to build the app:"),
1602
- pre(code(childOutputs.join("<br/>")))
1603
- ),
1604
- },
1605
- ],
1606
- });
1666
+ const logFile = exitCode === 0 ? "logs.txt" : "error_logs.txt";
1667
+ fs.writeFile(
1668
+ path.join(buildDir, logFile),
1669
+ childOutputs.join("\n"),
1670
+ (error) => {
1671
+ if (error) {
1672
+ console.log(`unable to write '${logFile}' to '${buildDir}'`);
1673
+ console.log(error);
1674
+ }
1675
+ }
1676
+ );
1607
1677
  });
1608
1678
  child.on("error", function (msg) {
1609
1679
  const message = msg.message ? msg.message : msg.code;
1610
1680
  const stack = msg.stack ? msg.stack : "";
1611
- res.sendWrap(req.__(`Admin`), {
1612
- above: [
1613
- {
1614
- type: "card",
1615
- title: req.__("Build Result"),
1616
- contents: div(
1617
- p(req.__("Unable to build the app:")),
1618
- pre(code(message)),
1619
- pre(code(stack))
1620
- ),
1621
- },
1622
- ],
1623
- });
1681
+ fs.writeFile(
1682
+ path.join(buildDir, "error_logs.txt"),
1683
+ [message, stack].join("\n"),
1684
+ (error) => {
1685
+ if (error) {
1686
+ console.log(`unable to write '${logFile}' to '${buildDir}'`);
1687
+ console.log(error);
1688
+ }
1689
+ }
1690
+ );
1624
1691
  });
1625
1692
  })
1626
1693
  );
1627
1694
 
1628
1695
  /**
1696
+ * Clear all
1629
1697
  * @name post/clear-all
1630
1698
  * @function
1631
1699
  * @memberof module:routes/admin~routes/adminRouter
@@ -1744,3 +1812,85 @@ router.post(
1744
1812
  }
1745
1813
  })
1746
1814
  );
1815
+
1816
+ /**
1817
+ * Developer settings form
1818
+ * @param {object} req request
1819
+ * @returns {Promise<Form>} form
1820
+ */
1821
+ const dev_form = async (req) => {
1822
+ return await config_fields_form({
1823
+ req,
1824
+ field_names: [
1825
+ "development_mode",
1826
+ "log_sql",
1827
+ "log_level",
1828
+ ],
1829
+ action: "/admin/dev",
1830
+ });
1831
+ };
1832
+ /**
1833
+ * Developer Mode page
1834
+ * @name get/dev
1835
+ * @function
1836
+ * @memberof module:routes/admin~routes/adminRouter
1837
+ */
1838
+ router.get(
1839
+ "/dev",
1840
+ isAdmin,
1841
+ error_catcher(async (req, res) => {
1842
+ const form = await dev_form(req);
1843
+ send_admin_page({
1844
+ res,
1845
+ req,
1846
+ active_sub: "Development",
1847
+ contents: {
1848
+ type: "card",
1849
+ title: req.__("Development settings"),
1850
+ contents: [
1851
+ renderForm(form, req.csrfToken())/*,
1852
+ a(
1853
+ {
1854
+ id: "testemail",
1855
+ href: "/admin/send-test-email",
1856
+ class: "btn btn-primary",
1857
+ },
1858
+ req.__("Send test email")
1859
+ ),*/
1860
+ ],
1861
+ },
1862
+ });
1863
+ })
1864
+ );
1865
+
1866
+ /**
1867
+ * Development mode
1868
+ * @name post/email
1869
+ * @function
1870
+ * @memberof module:routes/admin~routes/adminRouter
1871
+ */
1872
+ router.post(
1873
+ "/dev",
1874
+ isAdmin,
1875
+ error_catcher(async (req, res) => {
1876
+ const form = await dev_form(req);
1877
+ form.validate(req.body);
1878
+ if (form.hasErrors) {
1879
+ send_admin_page({
1880
+ res,
1881
+ req,
1882
+ active_sub: "Development",
1883
+ contents: {
1884
+ type: "card",
1885
+ title: req.__("Development settings"),
1886
+ contents: [renderForm(form, req.csrfToken())],
1887
+ },
1888
+ });
1889
+ } else {
1890
+ await save_config_from_form(form);
1891
+ req.flash("success", req.__("Development mode settings updated"));
1892
+ if (!req.xhr) res.redirect("/admin/dev");
1893
+ else res.json({ success: "ok" });
1894
+ }
1895
+ })
1896
+ );
package/routes/api.js CHANGED
@@ -124,8 +124,17 @@ router.post(
124
124
  error_catcher(async (req, res, next) => {
125
125
  let { viewName, queryName } = req.params;
126
126
  const view = await View.findOne({ name: viewName });
127
+ const db = require("@saltcorn/data/db");
127
128
  if (!view) {
128
- res.status(404).json({ error: req.__("Not found") });
129
+ res.status(404).json({
130
+ error: req.__("View %s not found", viewName),
131
+ view: viewName,
132
+ queryName: queryName,
133
+ smr: req.smr,
134
+ smrHeader: req.headers["x-saltcorn-client"],
135
+ schema: db.getTenantSchema(),
136
+ userTenant: req.user?.tenant,
137
+ });
129
138
  return;
130
139
  }
131
140
  await passport.authenticate(
@@ -143,7 +152,15 @@ router.post(
143
152
  const resp = await queries[queryName](...args, true);
144
153
  res.json({ success: resp, alerts: getFlashes(req) });
145
154
  } else {
146
- res.status(404).json({ error: req.__("Not found") });
155
+ res.status(404).json({
156
+ error: req.__("Query %s not found", queryName),
157
+ view: viewName,
158
+ queryName: queryName,
159
+ smr: req.smr,
160
+ smrHeader: req.headers["x-saltcorn-client"],
161
+ schema: db.getTenantSchema(),
162
+ userTenant: req.user?.tenant,
163
+ });
147
164
  }
148
165
  } else {
149
166
  res.status(401).json({ error: req.__("Not authorized") });