@saltcorn/server 0.8.8-beta.3 → 0.8.8-beta.4

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/locales/en.json CHANGED
@@ -1242,5 +1242,9 @@
1242
1242
  "unsynched": "unsynched",
1243
1243
  "synched": "synched",
1244
1244
  "Sync information": "Sync information",
1245
- "Sync information tracks the last modification or deletion timestamp so that the table data can be synchronized with the mobile app": "Sync information tracks the last modification or deletion timestamp so that the table data can be synchronized with the mobile app"
1246
- }
1245
+ "Sync information tracks the last modification or deletion timestamp so that the table data can be synchronized with the mobile app": "Sync information tracks the last modification or deletion timestamp so that the table data can be synchronized with the mobile app",
1246
+ "Included Plugins": "Included Plugins",
1247
+ "exclude": "exclude",
1248
+ "include": "include",
1249
+ "Auto public login": "Auto public login"
1250
+ }
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.8.8-beta.3",
3
+ "version": "0.8.8-beta.4",
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
- "@saltcorn/base-plugin": "0.8.8-beta.3",
10
- "@saltcorn/builder": "0.8.8-beta.3",
11
- "@saltcorn/data": "0.8.8-beta.3",
12
- "@saltcorn/admin-models": "0.8.8-beta.3",
13
- "@saltcorn/filemanager": "0.8.8-beta.3",
14
- "@saltcorn/markup": "0.8.8-beta.3",
15
- "@saltcorn/sbadmin2": "0.8.8-beta.3",
9
+ "@saltcorn/base-plugin": "0.8.8-beta.4",
10
+ "@saltcorn/builder": "0.8.8-beta.4",
11
+ "@saltcorn/data": "0.8.8-beta.4",
12
+ "@saltcorn/admin-models": "0.8.8-beta.4",
13
+ "@saltcorn/filemanager": "0.8.8-beta.4",
14
+ "@saltcorn/markup": "0.8.8-beta.4",
15
+ "@saltcorn/sbadmin2": "0.8.8-beta.4",
16
16
  "@socket.io/cluster-adapter": "^0.2.1",
17
17
  "@socket.io/sticky": "^1.0.1",
18
18
  "adm-zip": "0.5.10",
@@ -678,6 +678,10 @@ function build_mobile_app(button) {
678
678
  params.synchedTables = Array.from($("#synched-tbls-select-id")[0].options)
679
679
  .filter((option) => !option.hidden)
680
680
  .map((option) => option.value);
681
+ const pluginsSelect = $("#included-plugins-select-id")[0];
682
+ params.includedPlugins = Array.from(pluginsSelect.options)
683
+ .filter((option) => !option.hidden)
684
+ .map((option) => option.value);
681
685
  ajax_post("/admin/build-mobile-app", {
682
686
  data: params,
683
687
  success: (data) => {
@@ -717,6 +721,32 @@ function move_to_unsynched() {
717
721
  }
718
722
  }
719
723
 
724
+ function move_plugin_to_included() {
725
+ const opts = $("#excluded-plugins-select-id");
726
+ $("#included-plugins-select-id").removeAttr("selected");
727
+ for (const selected of opts.val()) {
728
+ const jExclOpt = $(`[id='${selected}_excluded_opt']`);
729
+ jExclOpt.attr("hidden", "true");
730
+ jExclOpt.removeAttr("selected");
731
+ const jInclOpt = $(`[id='${selected}_included_opt']`);
732
+ jInclOpt.removeAttr("hidden");
733
+ jInclOpt.removeAttr("selected");
734
+ }
735
+ }
736
+
737
+ function move_plugin_to_excluded() {
738
+ const opts = $("#included-plugins-select-id");
739
+ $("#excluded-plugins-select-id").removeAttr("selected");
740
+ for (const selected of opts.val()) {
741
+ const jInclOpt = $(`[id='${selected}_included_opt']`);
742
+ jInclOpt.attr("hidden", "true");
743
+ jInclOpt.removeAttr("selected");
744
+ const jExclOpt = $(`[id='${selected}_excluded_opt']`);
745
+ jExclOpt.removeAttr("hidden");
746
+ jExclOpt.removeAttr("selected");
747
+ }
748
+ }
749
+
720
750
  function toggle_tbl_sync() {
721
751
  if ($("#offlineModeBoxId")[0].checked === true) {
722
752
  $("#tblSyncSelectorId").attr("hidden", false);
package/routes/admin.js CHANGED
@@ -1492,6 +1492,9 @@ router.get(
1492
1492
  image.filename?.endsWith(".png")
1493
1493
  );
1494
1494
  const withSyncInfo = await Table.find({ has_sync_info: true });
1495
+ const plugins = (await Plugin.find()).filter(
1496
+ (plugin) => ["base", "sbadmin2"].indexOf(plugin.name) < 0
1497
+ );
1495
1498
  send_admin_page({
1496
1499
  res,
1497
1500
  req,
@@ -1750,7 +1753,29 @@ router.get(
1750
1753
  )
1751
1754
  )
1752
1755
  ),
1753
-
1756
+ // auto public login box
1757
+ div(
1758
+ { class: "row pb-2" },
1759
+ div(
1760
+ { class: "col-sm-4" },
1761
+ input({
1762
+ type: "checkbox",
1763
+ id: "autoPublLoginId",
1764
+ class: "form-check-input me-2",
1765
+ name: "autoPublicLogin",
1766
+ value: "autoPublicLogin",
1767
+ checked: false,
1768
+ }),
1769
+ label(
1770
+ {
1771
+ for: "autoPublLoginId",
1772
+ class: "form-label",
1773
+ },
1774
+ req.__("Auto public login")
1775
+ )
1776
+ )
1777
+ ),
1778
+ // allow offline mode box
1754
1779
  div(
1755
1780
  { class: "row pb-2" },
1756
1781
  div(
@@ -1773,10 +1798,11 @@ router.get(
1773
1798
  )
1774
1799
  )
1775
1800
  ),
1801
+ // synched/unsynched tables
1776
1802
  div(
1777
1803
  {
1778
1804
  id: "tblSyncSelectorId",
1779
- class: "row pb-2",
1805
+ class: "row pb-3",
1780
1806
  },
1781
1807
  div(
1782
1808
  label(
@@ -1864,6 +1890,97 @@ router.get(
1864
1890
  )
1865
1891
  )
1866
1892
  )
1893
+ ),
1894
+ // included/excluded plugins
1895
+ div(
1896
+ {
1897
+ id: "pluginsSelectorId",
1898
+ class: "row pb-2",
1899
+ },
1900
+ div(
1901
+ label({ class: "form-label fw-bold" }, req.__("Plugins"))
1902
+ ),
1903
+ div(
1904
+ { class: "container" },
1905
+ div(
1906
+ { class: "row" },
1907
+ div(
1908
+ { class: "col-sm-4 text-center" },
1909
+ req.__("exclude")
1910
+ ),
1911
+ div({ class: "col-sm-1" }),
1912
+ div(
1913
+ { class: "col-sm-4 text-center" },
1914
+ req.__("include")
1915
+ )
1916
+ ),
1917
+ div(
1918
+ { class: "row" },
1919
+ div(
1920
+ { class: "col-sm-4" },
1921
+ select(
1922
+ {
1923
+ id: "excluded-plugins-select-id",
1924
+ class: "form-control form-select",
1925
+ multiple: true,
1926
+ },
1927
+ plugins.map((plugin) =>
1928
+ option({
1929
+ id: `${plugin.name}_excluded_opt`,
1930
+ value: plugin.name,
1931
+ label: plugin.name,
1932
+ hidden: "true",
1933
+ })
1934
+ )
1935
+ )
1936
+ ),
1937
+ div(
1938
+ { class: "col-sm-1 d-flex justify-content-center" },
1939
+ div(
1940
+ div(
1941
+ button(
1942
+ {
1943
+ id: "move-plugin-right-btn-id",
1944
+ type: "button",
1945
+ onClick: `move_plugin_to_included()`,
1946
+ class: "btn btn-light pt-1 mb-1",
1947
+ },
1948
+ i({ class: "fas fa-arrow-right" })
1949
+ )
1950
+ ),
1951
+ div(
1952
+ button(
1953
+ {
1954
+ id: "move-plugin-left-btn-id",
1955
+ type: "button",
1956
+ onClick: `move_plugin_to_excluded()`,
1957
+ class: "btn btn-light pt-1",
1958
+ },
1959
+ i({ class: "fas fa-arrow-left" })
1960
+ )
1961
+ )
1962
+ )
1963
+ ),
1964
+ div(
1965
+ { class: "col-sm-4" },
1966
+ select(
1967
+ {
1968
+ id: "included-plugins-select-id",
1969
+ class: "form-control form-select",
1970
+ multiple: true,
1971
+ },
1972
+ plugins.map((plugin) =>
1973
+ option({
1974
+ id: `${plugin.name}_included_opt`,
1975
+ value: plugin.name,
1976
+ label: plugin.name,
1977
+ // hidden: "true",
1978
+ })
1979
+ )
1980
+ )
1981
+ )
1982
+ )
1983
+ )
1867
1984
  )
1868
1985
  ),
1869
1986
  button(
@@ -1969,8 +2086,10 @@ router.post(
1969
2086
  appIcon,
1970
2087
  serverURL,
1971
2088
  splashPage,
2089
+ autoPublicLogin,
1972
2090
  allowOfflineMode,
1973
2091
  synchedTables,
2092
+ includedPlugins,
1974
2093
  } = req.body;
1975
2094
  if (!androidPlatform && !iOSPlatform) {
1976
2095
  return res.json({
@@ -2022,8 +2141,14 @@ router.post(
2022
2141
  if (serverURL) spawnParams.push("-s", serverURL);
2023
2142
  if (splashPage) spawnParams.push("--splashPage", splashPage);
2024
2143
  if (allowOfflineMode) spawnParams.push("--allowOfflineMode");
2144
+ if (autoPublicLogin) spawnParams.push("--autoPublicLogin");
2025
2145
  if (synchedTables?.length > 0)
2026
2146
  spawnParams.push("--synchedTables", ...synchedTables.map((tbl) => tbl));
2147
+ if (includedPlugins?.length > 0)
2148
+ spawnParams.push(
2149
+ "--includedPlugins",
2150
+ ...includedPlugins.map((pluginName) => pluginName)
2151
+ );
2027
2152
  if (
2028
2153
  db.is_it_multi_tenant() &&
2029
2154
  db.getTenantSchema() !== db.connectObj.default_schema
package/routes/sync.js CHANGED
@@ -24,9 +24,30 @@ router.get(
24
24
  })
25
25
  );
26
26
 
27
- const getSyncRows = async (syncInfo, table, syncUntil, client) => {
27
+ const getSyncRows = async (syncInfo, table, syncUntil, client, user) => {
28
28
  const tblName = table.name;
29
29
  const pkName = table.pk_name;
30
+ const minRole = table.min_role_read;
31
+ const role = user?.role_id || 100;
32
+ let ownerFieldName = null;
33
+ if (
34
+ role > minRole &&
35
+ ((!table.ownership_field_id && !table.ownership_formula) || role === 100)
36
+ )
37
+ return null;
38
+ if (user?.id && role < 100 && role > minRole && table.ownership_field_id) {
39
+ const ownerField = table
40
+ .getFields()
41
+ .find((f) => f.id === table.ownership_field_id);
42
+ if (!ownerField) {
43
+ getState().log(
44
+ 5,
45
+ `GET /load_changes: The ownership field of '${table.name}' does not exist.`
46
+ );
47
+ return null;
48
+ }
49
+ ownerFieldName = ownerField.name;
50
+ }
30
51
  const schema = db.getTenantSchemaPrefix();
31
52
  if (!syncInfo.syncFrom) {
32
53
  const { rows } = await client.query(
@@ -43,9 +64,9 @@ const getSyncRows = async (syncInfo, table, syncUntil, client) => {
43
64
  on info_tbl.ref = data_tbl."${db.sqlsanitize(
44
65
  pkName
45
66
  )}" and info_tbl.deleted = false
46
- where data_tbl."${db.sqlsanitize(pkName)}" > ${
47
- syncInfo.maxLoadedId
48
- } order by data_tbl."${db.sqlsanitize(pkName)}"`
67
+ where data_tbl."${db.sqlsanitize(pkName)}" > ${syncInfo.maxLoadedId}
68
+ ${ownerFieldName ? `and data_tbl."${ownerFieldName}" = ${user.id}` : ""}
69
+ order by data_tbl."${db.sqlsanitize(pkName)}"`
49
70
  );
50
71
  for (const row of rows) {
51
72
  if (row._sync_info_tbl_last_modified_)
@@ -76,6 +97,7 @@ const getSyncRows = async (syncInfo, table, syncUntil, client) => {
76
97
  })
77
98
  and info_tbl.deleted = false
78
99
  and info_tbl.ref > ${syncInfo.maxLoadedId}
100
+ ${ownerFieldName ? `and data_tbl."${ownerFieldName}" = ${user.id}` : ""}
79
101
  order by info_tbl.ref`
80
102
  );
81
103
  for (const row of rows) {
@@ -114,7 +136,14 @@ router.post(
114
136
  const table = Table.findOne({ name: tblName });
115
137
  if (!table) throw new Error(`The table '${tblName}' does not exists`);
116
138
  const pkName = table.pk_name;
117
- let rows = await getSyncRows(syncInfo, table, loadUntil, client);
139
+ let rows = await getSyncRows(
140
+ syncInfo,
141
+ table,
142
+ loadUntil,
143
+ client,
144
+ req.user
145
+ );
146
+ if (!rows) continue;
118
147
  if (role > table.min_role_read) {
119
148
  if (
120
149
  role === 100 ||
@@ -10,6 +10,8 @@ const db = require("@saltcorn/data/db");
10
10
  const { sleep } = require("@saltcorn/data/tests/mocks");
11
11
 
12
12
  const Table = require("@saltcorn/data/models/table");
13
+ const Field = require("@saltcorn/data/models/field");
14
+ const User = require("@saltcorn/data/models/user");
13
15
 
14
16
  beforeAll(async () => {
15
17
  await resetToFixtures();
@@ -32,7 +34,7 @@ const initSyncInfo = async (tbls) => {
32
34
  describe("load remote insert/updates", () => {
33
35
  if (!db.isSQLite) {
34
36
  beforeAll(async () => {
35
- await initSyncInfo(["books", "publisher"]);
37
+ await initSyncInfo(["books", "publisher", "patients"]);
36
38
  });
37
39
  it("check params", async () => {
38
40
  const app = await getApp({ disableCsrf: true });
@@ -178,6 +180,99 @@ describe("load remote insert/updates", () => {
178
180
  expect(data.books.rows[1].author).toBe("Leo Tolstoy");
179
181
  }
180
182
  });
183
+
184
+ it("load sync not authorized", async () => {
185
+ const app = await getApp({ disableCsrf: true });
186
+ const loginCookie = await getUserLoginCookie();
187
+ const loadUntil = new Date();
188
+ const resp = await request(app)
189
+ .post("/sync/load_changes")
190
+ .set("Cookie", loginCookie)
191
+ .send({
192
+ loadUntil: loadUntil.valueOf(),
193
+ syncInfos: {
194
+ patients: {
195
+ maxLoadedId: 0,
196
+ syncFrom: 1000,
197
+ },
198
+ },
199
+ });
200
+ expect(resp.status).toBe(200);
201
+ const data = resp._body;
202
+ expect(Object.keys(data).length).toBe(0);
203
+ });
204
+
205
+ const addOwnerField = async () => {
206
+ const patients = Table.findOne({ name: "patients" });
207
+ const users = Table.findOne({ name: "users" });
208
+ const ownerField = await Field.create({
209
+ table: patients,
210
+ name: "owner",
211
+ label: "Pages",
212
+ type: "Key",
213
+ reftable: users,
214
+ attributes: { summary_field: "id" },
215
+ });
216
+ patients.ownership_field_id = ownerField.id;
217
+ await patients.update(patients);
218
+ const user = await User.findOne({ email: "user@foo.com" });
219
+ await patients.updateRow({ owner: user.id }, 1);
220
+ };
221
+
222
+ it("load sync authorized with ownership", async () => {
223
+ await addOwnerField();
224
+ const app = await getApp({ disableCsrf: true });
225
+ const loginCookie = await getUserLoginCookie();
226
+ const loadUntil = new Date();
227
+ const resp = await request(app)
228
+ .post("/sync/load_changes")
229
+ .set("Cookie", loginCookie)
230
+ .send({
231
+ loadUntil: loadUntil.valueOf(),
232
+ syncInfos: {
233
+ patients: {
234
+ maxLoadedId: 0,
235
+ },
236
+ },
237
+ });
238
+ expect(resp.status).toBe(200);
239
+ const data = resp._body;
240
+ expect(Object.keys(data).length).toBe(1);
241
+ expect(data.patients).toBeDefined();
242
+ expect(data.patients.rows.length).toBe(1);
243
+ expect(data.patients.rows[0].id).toBe(1);
244
+ });
245
+
246
+ it("load sync authorized with ownership and syncFrom", async () => {
247
+ const patients = Table.findOne({ name: "patients" });
248
+ if (!patients.ownership_field_id) await addOwnerField();
249
+ const rows = await patients.getRows();
250
+ for (const row of rows) {
251
+ await patients.updateRow(row, row.id);
252
+ }
253
+
254
+ const app = await getApp({ disableCsrf: true });
255
+ const loginCookie = await getUserLoginCookie();
256
+ const loadUntil = new Date();
257
+ const resp = await request(app)
258
+ .post("/sync/load_changes")
259
+ .set("Cookie", loginCookie)
260
+ .send({
261
+ loadUntil: loadUntil.valueOf(),
262
+ syncInfos: {
263
+ patients: {
264
+ maxLoadedId: 0,
265
+ syncFrom: 1000,
266
+ },
267
+ },
268
+ });
269
+ expect(resp.status).toBe(200);
270
+ const data = resp._body;
271
+ expect(Object.keys(data).length).toBe(1);
272
+ expect(data.patients).toBeDefined();
273
+ expect(data.patients.rows.length).toBe(1);
274
+ expect(data.patients.rows[0].id).toBe(1);
275
+ });
181
276
  } else
182
277
  it("only pq support", () => {
183
278
  expect(true).toBe(true);