@saltcorn/mobile-app 0.7.4 → 0.8.0-beta.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/config.xml CHANGED
@@ -11,4 +11,6 @@
11
11
  </platform>
12
12
  <content src="index.html"/>
13
13
  <access origin="*"/>
14
+ <allow-navigation href="*" />
15
+ <allow-navigation href="http://localhost/__cdvfile_files__/*" />
14
16
  </widget>
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@saltcorn/mobile-app",
3
3
  "displayName": "Saltcorn mobile app",
4
- "version": "0.7.4",
4
+ "version": "0.8.0-beta.0",
5
5
  "description": "Apache Cordova application with @saltcorn/markup",
6
6
  "main": "index.js",
7
7
  "scripts": {
8
8
  "add-platform": "cordova platform add",
9
+ "add-plugin": "cordova plugin add",
9
10
  "build-app": "cordova build "
10
11
  },
11
12
  "author": "Christian Hugo",
@@ -13,12 +14,7 @@
13
14
  "cordova": {
14
15
  "platforms": [
15
16
  "android"
16
- ],
17
- "plugins": {
18
- "cordova-sqlite-ext": {},
19
- "cordova-plugin-file": {},
20
- "cordova-plugin-inappbrowser": {}
21
- }
17
+ ]
22
18
  },
23
19
  "overrides": {
24
20
  "ansi-regex": "4.1.1"
@@ -28,8 +24,6 @@
28
24
  "cordova-sqlite-ext": "^6.0.0"
29
25
  },
30
26
  "devDependencies": {
31
- "cordova-android": "^10.1.2",
32
- "cordova-plugin-file": "^6.0.2",
33
- "cordova-plugin-inappbrowser": "^5.0.0"
27
+ "cordova-android": "^10.1.2"
34
28
  }
35
29
  }
package/www/index.html CHANGED
@@ -143,14 +143,30 @@
143
143
  iframe.contentWindow._sc_version_tag = config.version_tag;
144
144
  };
145
145
 
146
+ const initJwt = async () => {
147
+ if (!(await saltcorn.data.db.tableExists("jwt_table"))) {
148
+ await createJwtTable();
149
+ }
150
+ else {
151
+ const jwt = await getJwt();
152
+ if(jwt) {
153
+ const state = saltcorn.data.state.getState();
154
+ state.mobileConfig.jwt = jwt;
155
+ }
156
+ }
157
+ };
158
+
146
159
  const checkJWT = async () => {
147
- const jwt = localStorage.getItem("auth_jwt");
148
- if (!jwt || jwt === "undefined") return false;
149
- const response = await apiCall({
150
- method: "GET",
151
- path: "/auth/authenticated",
152
- });
153
- return response.data.authenticated;
160
+ const state = saltcorn.data.state.getState();
161
+ const jwt = state.mobileConfig.jwt;
162
+ if(jwt && jwt !== "undefined") {
163
+ const response = await apiCall({
164
+ method: "GET",
165
+ path: "/auth/authenticated",
166
+ });
167
+ return response.data.authenticated;
168
+ }
169
+ else return false;
154
170
  };
155
171
 
156
172
  const initI18Next = async () => {
@@ -199,6 +215,7 @@
199
215
  if (updateNeeded) {
200
216
  await updateDb(tablesJSON);
201
217
  }
218
+ await initJwt();
202
219
  await state.refresh_tables();
203
220
  await state.refresh_views();
204
221
  await state.refresh_pages();
@@ -212,8 +229,8 @@
212
229
  await initI18Next();
213
230
  try {
214
231
  if (await checkJWT()) {
215
- const decodedJwt = jwt_decode(localStorage.getItem("auth_jwt"));
216
232
  const mobileConfig = state.mobileConfig;
233
+ const decodedJwt = jwt_decode(mobileConfig.jwt);
217
234
  mobileConfig.role_id = decodedJwt.user.role_id
218
235
  ? decodedJwt.user.role_id
219
236
  : 10;
@@ -226,7 +243,7 @@
226
243
  pathname: entryPoint,
227
244
  fullWrap: true,
228
245
  });
229
- replaceIframe(page.content);
246
+ await replaceIframe(page.content);
230
247
  } else {
231
248
  const page = await router.resolve({
232
249
  pathname: "get/auth/login",
@@ -243,7 +260,7 @@
243
260
  },
244
261
  ],
245
262
  });
246
- replaceIframe(page.content);
263
+ await replaceIframe(page.content);
247
264
  console.error(error);
248
265
  }
249
266
  };
@@ -1,4 +1,4 @@
1
- function MobileRequest(xhr = false, files = undefined) {
1
+ function MobileRequest({ xhr = false, files = undefined, query = undefined }) {
2
2
  const roleId = saltcorn.data.state.getState().mobileConfig.role_id
3
3
  ? saltcorn.data.state.getState().mobileConfig.role_id
4
4
  : 10;
@@ -26,5 +26,6 @@ function MobileRequest(xhr = false, files = undefined) {
26
26
  csrfToken: () => "",
27
27
  xhr,
28
28
  files,
29
+ query,
29
30
  };
30
31
  }
@@ -87,7 +87,8 @@ const logoutAction = async () => {
87
87
  const config = saltcorn.data.state.getState().mobileConfig;
88
88
  const response = await apiCall({ method: "GET", path: "/auth/logout" });
89
89
  if (response.data.success) {
90
- localStorage.removeItem("auth_jwt");
90
+ await removeJwt();
91
+ config.jwt = undefined;
91
92
  return {
92
93
  content: renderLoginView(config.entry_point, config.version_tag),
93
94
  };
@@ -3,9 +3,9 @@ const postPageAction = async (context) => {
3
3
  const state = saltcorn.data.state.getState();
4
4
  const { page_name, rndid } = context.params;
5
5
  const page = await saltcorn.data.models.Page.findOne({ name: page_name });
6
- const req = new MobileRequest(context.xhr);
6
+ const req = new MobileRequest({ xhr: context.xhr });
7
7
  if (state.mobileConfig.role_id > page.min_role) {
8
- throw new Error(req.__("Not authorized"));
8
+ throw new Error(req.__("Not authorized"));
9
9
  }
10
10
  let col;
11
11
  saltcorn.data.models.layout.traverseSync(page.layout, {
@@ -16,7 +16,7 @@ const postPageAction = async (context) => {
16
16
  const result = await saltcorn.data.plugin_helper.run_action_column({
17
17
  col,
18
18
  referrer: "",
19
- req
19
+ req,
20
20
  });
21
21
  return result || {};
22
22
  };
@@ -26,9 +26,9 @@ const getPage = async (context) => {
26
26
  const state = saltcorn.data.state.getState();
27
27
  const { page_name } = context.params;
28
28
  const page = await saltcorn.data.models.Page.findOne({ name: page_name });
29
- const req = new MobileRequest(context.xhr);
29
+ const req = new MobileRequest({ xhr: context.xhr });
30
30
  if (state.mobileConfig.role_id > page.min_role) {
31
- throw new Error(req.__("Not authorized"));
31
+ throw new Error(req.__("Not authorized"));
32
32
  }
33
33
  const query = parseQuery(context.query);
34
34
  const res = new MobileResponse();
@@ -13,7 +13,7 @@ const postView = async (context) => {
13
13
  const view = await saltcorn.data.models.View.findOne({
14
14
  name: context.params.viewname,
15
15
  });
16
- const req = new MobileRequest(context.xhr, context.files);
16
+ const req = new MobileRequest({ xhr: context.xhr, files: context.files });
17
17
  const res = new MobileResponse();
18
18
  const state = saltcorn.data.state.getState();
19
19
  if (
@@ -43,7 +43,7 @@ const postViewRoute = async (context) => {
43
43
  const view = await saltcorn.data.models.View.findOne({
44
44
  name: context.params.viewname,
45
45
  });
46
- const req = new MobileRequest(context.xhr);
46
+ const req = new MobileRequest({ xhr: context.xhr });
47
47
  const res = new MobileResponse();
48
48
  const state = saltcorn.data.state.getState();
49
49
  if (state.mobileConfig.role_id > view.min_role) {
@@ -69,7 +69,7 @@ const getView = async (context) => {
69
69
  const query = parseQuery(context.query);
70
70
  const { viewname } = context.params;
71
71
  const view = saltcorn.data.models.View.findOne({ name: viewname });
72
- const req = new MobileRequest(context.xhr);
72
+ const req = new MobileRequest({ xhr: context.xhr, query });
73
73
  if (
74
74
  state.mobileConfig.role_id > view.min_role &&
75
75
  !(await view.authorise_get({ query, req, ...view }))
@@ -1,9 +1,4 @@
1
- const iframeStyle =
2
- "position: fixed; top: 0; left: 0; bottom: 0; right: 0; " +
3
- "width: 100%; height: 100%; border: none; margin: 0; padding: 0; " +
4
- "overflow: hidden; z-index: 999999; ";
5
-
6
- const routingHistory = [];
1
+ let routingHistory = [];
7
2
 
8
3
  function currentLocation() {
9
4
  if (routingHistory.length == 0) return undefined;
@@ -28,7 +23,7 @@ async function apiCall({ method, path, params, body, responseType }) {
28
23
  "X-Saltcorn-Client": "mobile-app",
29
24
  };
30
25
  if (config.tenantAppName) headers["X-Saltcorn-App"] = config.tenantAppName;
31
- const token = localStorage.getItem("auth_jwt");
26
+ const token = config.jwt;
32
27
  if (token) headers.Authorization = `jwt ${token}`;
33
28
  try {
34
29
  const result = await axios({
@@ -94,18 +89,11 @@ function splitPathQuery(url) {
94
89
  return { path, query };
95
90
  }
96
91
 
97
- function replaceIframe(content) {
92
+ async function replaceIframe(content) {
93
+ await write("content.html", `${cordova.file.dataDirectory}`, content);
94
+ const url = await getDirEntry(`${cordova.file.dataDirectory}content.html`);
98
95
  const iframe = document.getElementById("content-iframe");
99
- iframe.remove();
100
- const newIframe = document.createElement("iframe");
101
- document.body.appendChild(newIframe);
102
- const config = saltcorn.data.state.getState().mobileConfig;
103
- newIframe.contentWindow._sc_version_tag = config.version_tag;
104
- newIframe.setAttribute("style", iframeStyle);
105
- newIframe.id = "content-iframe";
106
- newIframe.contentWindow.document.open();
107
- newIframe.contentWindow.document.write(content);
108
- newIframe.contentWindow.document.close();
96
+ iframe.src = url.toURL();
109
97
  }
110
98
 
111
99
  function addScriptToIframeHead(iframeDoc, script) {
@@ -190,6 +178,10 @@ async function goBack(steps = 1, exitOnFirstPage = false) {
190
178
  await handleRoute("/");
191
179
  } else {
192
180
  routingHistory = routingHistory.slice(0, routingHistory.length - steps);
181
+ // don't repeat a post
182
+ if (routingHistory[routingHistory.length - 1].route.startsWith("post/")) {
183
+ routingHistory.pop();
184
+ }
193
185
  const newCurrent = routingHistory.pop();
194
186
  await handleRoute(newCurrent.route, newCurrent.query);
195
187
  }
@@ -57,8 +57,13 @@ async function saveAndContinue(e, action, k) {
57
57
  // TODO ch error (request.responseText?)
58
58
  }
59
59
 
60
- async function loginRequest(email, password, isSignup) {
61
- const opts = isSignup
60
+ async function loginRequest({ email, password, isSignup, isPublic }) {
61
+ const opts = isPublic
62
+ ? {
63
+ method: "GET",
64
+ path: "/auth/login-with/jwt",
65
+ }
66
+ : isSignup
62
67
  ? {
63
68
  method: "POST",
64
69
  path: "/auth/signup",
@@ -81,20 +86,21 @@ async function loginRequest(email, password, isSignup) {
81
86
 
82
87
  async function login(e, entryPoint, isSignup) {
83
88
  const formData = new FormData(e);
84
- const loginResult = await loginRequest(
85
- formData.get("email"),
86
- formData.get("password"),
87
- isSignup
88
- );
89
+ const loginResult = await loginRequest({
90
+ email: formData.get("email"),
91
+ password: formData.get("password"),
92
+ isSignup,
93
+ });
89
94
  if (typeof loginResult === "string") {
90
95
  // use it as a token
91
- parent.localStorage.setItem("auth_jwt", loginResult);
92
96
  const decodedJwt = parent.jwt_decode(loginResult);
93
97
  const config = parent.saltcorn.data.state.getState().mobileConfig;
94
98
  config.role_id = decodedJwt.user.role_id ? decodedJwt.user.role_id : 10;
95
99
  config.user_name = decodedJwt.user.email;
96
100
  config.language = decodedJwt.user.language;
97
101
  config.isPublicUser = false;
102
+ await parent.setJwt(loginResult);
103
+ config.jwt = loginResult;
98
104
  await parent.i18next.changeLanguage(config.language);
99
105
  parent.addRoute({ route: entryPoint, query: undefined });
100
106
  const page = await parent.router.resolve({
@@ -110,7 +116,7 @@ async function login(e, entryPoint, isSignup) {
110
116
  },
111
117
  ],
112
118
  });
113
- parent.replaceIframe(page.content);
119
+ await parent.replaceIframe(page.content);
114
120
  } else if (loginResult?.alerts) {
115
121
  parent.showAlerts(loginResult?.alerts);
116
122
  } else {
@@ -120,23 +126,32 @@ async function login(e, entryPoint, isSignup) {
120
126
 
121
127
  async function publicLogin(entryPoint) {
122
128
  try {
123
- const config = parent.saltcorn.data.state.getState().mobileConfig;
124
- config.role_id = 10;
125
- config.user_name = "public";
126
- config.language = "en";
127
- config.isPublicUser = true;
128
- parent.i18next.changeLanguage(config.language);
129
- const page = await parent.router.resolve({
130
- pathname: entryPoint,
131
- fullWrap: true,
132
- alerts: [
133
- {
134
- type: "success",
135
- msg: parent.i18next.t("Welcome to Saltcorn!"),
136
- },
137
- ],
138
- });
139
- parent.replaceIframe(page.content);
129
+ const loginResult = await loginRequest({ isPublic: true });
130
+ if (typeof loginResult === "string") {
131
+ const config = parent.saltcorn.data.state.getState().mobileConfig;
132
+ config.role_id = 10;
133
+ config.user_name = "public";
134
+ config.language = "en";
135
+ config.isPublicUser = true;
136
+ await parent.setJwt(loginResult);
137
+ config.jwt = loginResult;
138
+ parent.i18next.changeLanguage(config.language);
139
+ const page = await parent.router.resolve({
140
+ pathname: entryPoint,
141
+ fullWrap: true,
142
+ alerts: [
143
+ {
144
+ type: "success",
145
+ msg: parent.i18next.t("Welcome to Saltcorn!"),
146
+ },
147
+ ],
148
+ });
149
+ await parent.replaceIframe(page.content);
150
+ } else if (loginResult?.alerts) {
151
+ parent.showAlerts(loginResult?.alerts);
152
+ } else {
153
+ throw new Error("The login failed.");
154
+ }
140
155
  } catch (error) {
141
156
  parent.showAlerts([
142
157
  {
@@ -155,7 +170,7 @@ async function logout() {
155
170
  entryView: config.entry_point,
156
171
  versionTag: config.version_tag,
157
172
  });
158
- parent.replaceIframe(page.content);
173
+ await parent.replaceIframe(page.content);
159
174
  } catch (error) {
160
175
  parent.showAlerts([
161
176
  {
@@ -233,9 +248,22 @@ function updateQueryStringParameter(queryStr, key, value) {
233
248
  return params.join("&");
234
249
  }
235
250
 
251
+ function invalidate_pagings(currentQuery) {
252
+ let newQuery = currentQuery;
253
+ const queryObj = Object.fromEntries(new URLSearchParams(newQuery).entries());
254
+ const toRemove = Object.keys(queryObj).filter((val) => is_paging_param(val));
255
+ for (const k of toRemove) {
256
+ newQuery = removeQueryStringParameter(newQuery, k);
257
+ }
258
+ return newQuery;
259
+ }
260
+
236
261
  async function set_state_fields(kvs, href) {
237
262
  let queryParams = [];
238
263
  let currentQuery = parent.currentQuery();
264
+ if (Object.keys(kvs).some((k) => !is_paging_param(k))) {
265
+ currentQuery = invalidate_pagings(currentQuery);
266
+ }
239
267
  Object.entries(kvs).forEach((kv) => {
240
268
  if (kv[1].unset && kv[1].unset === true) {
241
269
  currentQuery = removeQueryStringParameter(currentQuery, kv[0]);
@@ -260,16 +288,23 @@ async function unset_state_field(key) {
260
288
  await parent.handleRoute(href, query);
261
289
  }
262
290
 
263
- async function sortby(k, desc, viewname) {
291
+ async function sortby(k, desc, viewIdentifier) {
264
292
  await set_state_fields(
265
- { _sortby: k, _sortdesc: desc ? "on" : { unset: true } },
266
- `get/view/${viewname}`
293
+ {
294
+ [`_${viewIdentifier}_sortby`]: k,
295
+ [`_${viewIdentifier}_sortdesc`]: desc ? "on" : { unset: true },
296
+ },
297
+ parent.currentLocation()
267
298
  );
268
299
  }
269
300
 
270
- async function gopage(n, pagesize, extra) {
301
+ async function gopage(n, pagesize, viewIdentifier, extra) {
271
302
  await set_state_fields(
272
- { ...extra, _page: n, _pagesize: pagesize },
303
+ {
304
+ ...extra,
305
+ [`_${viewIdentifier}_page`]: n,
306
+ [`_${viewIdentifier}_pagesize`]: pagesize,
307
+ },
273
308
  parent.currentLocation()
274
309
  );
275
310
  }
@@ -388,7 +423,7 @@ async function make_unique_field(
388
423
 
389
424
  async function buildEncodedImage(fileId, elementId) {
390
425
  const base64Encoded = await parent.loadEncodedFile(fileId);
391
- $(`#${elementId}`)[0].src = base64Encoded;
426
+ document.getElementById(elementId).src = base64Encoded;
392
427
  }
393
428
 
394
429
  async function buildEncodedBgImage(fileId, elementId) {
@@ -400,11 +435,13 @@ async function buildEncodedBgImage(fileId, elementId) {
400
435
  }
401
436
 
402
437
  function openFile(fileId) {
438
+ // TODO fileIds with whitespaces do not work
403
439
  const config = parent.saltcorn.data.state.getState().mobileConfig;
404
440
  const serverPath = config.server_path;
405
- const token = localStorage.getItem("auth_jwt");
441
+ const token = config.jwt;
442
+ const url = `${serverPath}/files/serve/${fileId}?jwt=${token}`;
406
443
  parent.cordova.InAppBrowser.open(
407
- `${serverPath}/files/serve/${fileId}?jwt=${token}`,
444
+ url,
408
445
  "_self",
409
446
  "clearcache=yes,clearsessioncache=yes,location=no"
410
447
  );
@@ -1,4 +1,5 @@
1
1
  const historyFile = "update_history";
2
+ const jwtTableName = "jwt_table";
2
3
 
3
4
  /**
4
5
  * drop tables that are no longer in the 'tables.json' file
@@ -7,14 +8,17 @@ const historyFile = "update_history";
7
8
  async function dropDeletedTables(incomingTables) {
8
9
  const existingTables = await saltcorn.data.models.Table.find();
9
10
  for (const table of existingTables) {
10
- if (table.name !== "users" && !incomingTables.find((row) => row.id === table.id)) {
11
- saltcorn.data.db.query(`DROP TABLE ${table.name}`);
11
+ if (
12
+ table.name !== "users" &&
13
+ !incomingTables.find((row) => row.id === table.id)
14
+ ) {
15
+ await saltcorn.data.db.query(`DROP TABLE ${table.name}`);
12
16
  }
13
17
  }
14
18
  }
15
19
 
16
20
  async function updateScTables(tablesJSON, skipScPlugins = true) {
17
- saltcorn.data.db.query("PRAGMA foreign_keys = OFF;");
21
+ await saltcorn.data.db.query("PRAGMA foreign_keys = OFF;");
18
22
  for (const { table, rows } of tablesJSON.sc_tables) {
19
23
  if (skipScPlugins && table === "_sc_plugins") continue;
20
24
  if (table === "_sc_tables") await dropDeletedTables(rows);
@@ -23,7 +27,7 @@ async function updateScTables(tablesJSON, skipScPlugins = true) {
23
27
  await saltcorn.data.db.insert(table, row);
24
28
  }
25
29
  }
26
- saltcorn.data.db.query("PRAGMA foreign_keys = ON;");
30
+ await saltcorn.data.db.query("PRAGMA foreign_keys = ON;");
27
31
  }
28
32
 
29
33
  async function updateScPlugins(tablesJSON) {
@@ -85,3 +89,23 @@ async function getTableIds(tableNames) {
85
89
  .filter((table) => tableNames.indexOf(table.name) > -1)
86
90
  .map((table) => table.id);
87
91
  }
92
+
93
+ async function createJwtTable() {
94
+ await saltcorn.data.db.query(`CREATE TABLE IF NOT EXISTS ${jwtTableName} (
95
+ jwt VARCHAR(500)
96
+ )`);
97
+ }
98
+
99
+ async function getJwt() {
100
+ const rows = await saltcorn.data.db.select(jwtTableName);
101
+ return rows?.length > 0 ? rows[0].jwt : null;
102
+ }
103
+
104
+ async function removeJwt() {
105
+ await saltcorn.data.db.deleteWhere(jwtTableName);
106
+ }
107
+
108
+ async function setJwt(jwt) {
109
+ await removeJwt();
110
+ await saltcorn.data.db.insert(jwtTableName, { jwt: jwt });
111
+ }