@saltcorn/server 0.9.0 → 0.9.1-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/auth/admin.js CHANGED
@@ -376,6 +376,8 @@ const http_settings_form = async (req) =>
376
376
  //"cookie_sessions",
377
377
  "public_cache_maxage",
378
378
  "custom_http_headers",
379
+ "body_limit",
380
+ "url_encoded_limit",
379
381
  ],
380
382
  action: "/useradmin/http",
381
383
  submitLabel: req.__("Save"),
package/auth/testhelp.js CHANGED
@@ -215,6 +215,18 @@ const succeedJsonWith = (pred) => (res) => {
215
215
  }
216
216
  };
217
217
 
218
+ const succeedJsonWithWholeBody = (pred) => (res) => {
219
+ if (res.statusCode !== 200) {
220
+ console.log(res.text);
221
+ throw new Error(`Expected status 200, received ${res.statusCode}`);
222
+ }
223
+
224
+ if (!pred(res.body)) {
225
+ console.log(res.body);
226
+ throw new Error(`Not satisfied`);
227
+ }
228
+ };
229
+
218
230
  /**
219
231
  *
220
232
  * @param {number} code
@@ -260,6 +272,7 @@ module.exports = {
260
272
  notAuthorized,
261
273
  respondJsonWith,
262
274
  toSucceedWithImage,
275
+ succeedJsonWithWholeBody,
263
276
  resToLoginCookie,
264
277
  itShouldIncludeTextForAdmin,
265
278
  };
@@ -0,0 +1,12 @@
1
+ This trigger can be run with by making an HTTP request to
2
+
3
+ {{scState.getConfig("base_url","").replace(/\/$/, "")}}/api/action/{{ query.name }}
4
+
5
+ If you make a POST request, the POST body is expected to be JSON and its value
6
+ is accessible using the `row` variable (e.g. in a `run_js_code`)
7
+
8
+ If you make a GET request, the query string values
9
+ are accessible as a JSON object using the `row` variable (e.g. in a `run_js_code`).
10
+
11
+ If you return a value from the action, it will be available in the `data`
12
+ subfield of the JSON body in the HTTP response
package/locales/en.json CHANGED
@@ -1268,5 +1268,10 @@
1268
1268
  "Pack file": "Pack file",
1269
1269
  "Upload a pack file": "Upload a pack file",
1270
1270
  "No menu": "No menu",
1271
- "Omit the menu from this page": "Omit the menu from this page"
1271
+ "Omit the menu from this page": "Omit the menu from this page",
1272
+ "%s finished without a result": "%s finished without a result",
1273
+ "Body size limit (Kb)": "Body size limit (Kb)",
1274
+ "Maximum request body size in kilobytes": "Maximum request body size in kilobytes",
1275
+ "URL encoded size limit (Kb)": "URL encoded size limit (Kb)",
1276
+ "Maximum URL encoded request size in kilobytes": "Maximum URL encoded request size in kilobytes"
1272
1277
  }
package/locales/fr.json CHANGED
@@ -286,5 +286,11 @@
286
286
  "Users and security": "Users and security",
287
287
  "Site structure": "Site structure",
288
288
  "Events": "Events",
289
- "Are you sure?": "Are you sure?"
289
+ "Are you sure?": "Are you sure?",
290
+ "API token": "API token",
291
+ "No API token issued": "No API token issued",
292
+ "Generate": "Generate",
293
+ "Two-factor authentication": "Two-factor authentication",
294
+ "Two-factor authentication is disabled": "Two-factor authentication is disabled",
295
+ "Enable 2FA": "Enable 2FA"
290
296
  }
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.9.0",
3
+ "version": "0.9.1-beta.0",
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.9.0",
10
- "@saltcorn/builder": "0.9.0",
11
- "@saltcorn/data": "0.9.0",
12
- "@saltcorn/admin-models": "0.9.0",
13
- "@saltcorn/filemanager": "0.9.0",
14
- "@saltcorn/markup": "0.9.0",
15
- "@saltcorn/sbadmin2": "0.9.0",
9
+ "@saltcorn/base-plugin": "0.9.1-beta.0",
10
+ "@saltcorn/builder": "0.9.1-beta.0",
11
+ "@saltcorn/data": "0.9.1-beta.0",
12
+ "@saltcorn/admin-models": "0.9.1-beta.0",
13
+ "@saltcorn/filemanager": "0.9.1-beta.0",
14
+ "@saltcorn/markup": "0.9.1-beta.0",
15
+ "@saltcorn/sbadmin2": "0.9.1-beta.0",
16
16
  "@socket.io/cluster-adapter": "^0.2.1",
17
17
  "@socket.io/sticky": "^1.0.1",
18
18
  "adm-zip": "0.5.10",
@@ -154,7 +154,8 @@ function apply_showif() {
154
154
  e.empty();
155
155
  e.prop("data-fetch-options-current-set", qs);
156
156
  const toAppend = [];
157
- if (!dynwhere.required) toAppend.push(`<option></option>`);
157
+ if (!dynwhere.required)
158
+ toAppend.push({ label: dynwhere.neutral_label || "" });
158
159
  let currentDataOption = undefined;
159
160
  const dataOptions = [];
160
161
  //console.log(success);
@@ -173,12 +174,28 @@ function apply_showif() {
173
174
  const selected = `${current}` === `${r[dynwhere.refname]}`;
174
175
  dataOptions.push({ text: label, value });
175
176
  if (selected) currentDataOption = value;
176
- const html = `<option ${
177
- selected ? "selected" : ""
178
- } value="${value}">${label}</option>`;
179
- toAppend.push(html);
177
+ toAppend.push({ selected, value, label });
180
178
  });
181
- e.html(toAppend.join(""));
179
+ toAppend.sort((a, b) =>
180
+ a.label === dynwhere.neutral_label
181
+ ? -1
182
+ : b.label === dynwhere.neutral_label
183
+ ? 1
184
+ : (a.label?.toLowerCase?.() || a.label) >
185
+ (b.label?.toLowerCase?.() || b.label)
186
+ ? 1
187
+ : -1
188
+ );
189
+ e.html(
190
+ toAppend
191
+ .map(
192
+ ({ label, value, selected }) =>
193
+ `<option${selected ? ` selected` : ""}${
194
+ value ? ` value="${value}"` : ""
195
+ }>${label || ""}</option>`
196
+ )
197
+ .join("")
198
+ );
182
199
 
183
200
  //TODO: also sort inserted HTML options
184
201
  dataOptions.sort((a, b) =>
@@ -1022,7 +1039,14 @@ function common_done(res, viewname, isWeb = true) {
1022
1039
  if (res.notify) handle(res.notify, notifyAlert);
1023
1040
  if (res.error)
1024
1041
  handle(res.error, (text) => notifyAlert({ type: "danger", text: text }));
1025
- if (res.eval_js) handle(res.eval_js, eval);
1042
+
1043
+ if (res.eval_js && res.row && res.field_names) {
1044
+ const f = new Function(`row, {${res.field_names}}`, res.eval_js);
1045
+ const evalres = f(res.row, res.row);
1046
+ if (evalres) common_done(evalres, viewname, isWeb);
1047
+ } else if (res.eval_js) {
1048
+ handle(res.eval_js, eval);
1049
+ }
1026
1050
 
1027
1051
  if (res.reload_page) {
1028
1052
  (isWeb ? location : parent).reload(); //TODO notify to cookie if reload or goto
@@ -572,6 +572,20 @@ function ajax_post_btn(e, reload_on_done, reload_delay) {
572
572
 
573
573
  return false;
574
574
  }
575
+
576
+ function api_action_call(name, body) {
577
+ $.ajax(`/api/action/${name}`, {
578
+ type: "POST",
579
+ headers: {
580
+ "CSRF-Token": _sc_globalCsrf,
581
+ },
582
+ data: body,
583
+ success: function (res) {
584
+ common_done(res.data);
585
+ },
586
+ });
587
+ }
588
+
575
589
  function make_unique_field(
576
590
  id,
577
591
  table_id,
package/routes/api.js CHANGED
@@ -332,7 +332,7 @@ router.get(
332
332
  * @function
333
333
  * @memberof module:routes/api~apiRouter
334
334
  */
335
- router.post(
335
+ router.all(
336
336
  "/action/:actionname/",
337
337
  error_catcher(async (req, res, next) => {
338
338
  const { actionname } = req.params;
@@ -361,7 +361,7 @@ router.post(
361
361
  const resp = await action.run({
362
362
  configuration: trigger.configuration,
363
363
  body: req.body,
364
- row: req.body,
364
+ row: req.method === "GET" ? req.query : req.body,
365
365
  req,
366
366
  user: user || req.user,
367
367
  });
@@ -9,7 +9,7 @@ const {
9
9
  post_dropdown_item,
10
10
  } = require("@saltcorn/markup");
11
11
  const { get_base_url } = require("./utils.js");
12
- const { h4, p, div, a, input, text } = require("@saltcorn/markup/tags");
12
+ const { h4, p, div, a, i, input, text } = require("@saltcorn/markup/tags");
13
13
 
14
14
  /**
15
15
  * @param {string} col
@@ -385,10 +385,16 @@ const getTriggerList = (triggers, req, { tagId, domId, showList } = {}) => {
385
385
  },
386
386
  {
387
387
  label: req.__("When"),
388
- key: (a) =>
389
- a.when_trigger === "API call"
390
- ? `API: ${base_url}api/action/${a.name}`
391
- : a.when_trigger,
388
+ key: (act) =>
389
+ act.when_trigger +
390
+ (act.when_trigger === "API call"
391
+ ? a(
392
+ {
393
+ href: `javascript:ajax_modal('/admin/help/API%20actions?name=${act.name}')`,
394
+ },
395
+ i({ class: "fas fa-question-circle ms-1" })
396
+ )
397
+ : ""),
392
398
  },
393
399
  {
394
400
  label: req.__("Test run"),
@@ -262,10 +262,16 @@ const actionsTab = async (req, triggers) => {
262
262
  },
263
263
  {
264
264
  label: req.__("When"),
265
- key: (a) =>
266
- a.when_trigger === "API call"
267
- ? `API: ${base_url}api/action/${a.name}`
268
- : a.when_trigger,
265
+ key: (act) =>
266
+ act.when_trigger +
267
+ (act.when_trigger === "API call"
268
+ ? a(
269
+ {
270
+ href: `javascript:ajax_modal('/admin/help/API%20actions?name=${act.name}')`,
271
+ },
272
+ i({ class: "fas fa-question-circle ms-1" })
273
+ )
274
+ : ""),
269
275
  },
270
276
  ],
271
277
  triggers
@@ -289,7 +289,7 @@ const wrap = (contents, noCard, req, page) => ({
289
289
  crumbs: [
290
290
  { text: req.__("Pages"), href: "/pageedit" },
291
291
  page
292
- ? { href: `/pageedit/edit/${page.name}`, text: page.name }
292
+ ? { href: `/page/${page.name}`, text: page.name }
293
293
  : { text: req.__("New") },
294
294
  ],
295
295
  },
package/tests/api.test.js CHANGED
@@ -1,6 +1,9 @@
1
1
  const request = require("supertest");
2
2
  const getApp = require("../app");
3
3
  const Table = require("@saltcorn/data/models/table");
4
+ const Trigger = require("@saltcorn/data/models/trigger");
5
+
6
+ const Field = require("@saltcorn/data/models/field");
4
7
  const {
5
8
  getStaffLoginCookie,
6
9
  getAdminLoginCookie,
@@ -9,6 +12,7 @@ const {
9
12
  succeedJsonWith,
10
13
  notAuthorized,
11
14
  toRedirect,
15
+ succeedJsonWithWholeBody,
12
16
  } = require("../auth/testhelp");
13
17
  const db = require("@saltcorn/data/db");
14
18
  const User = require("@saltcorn/data/models/user");
@@ -322,3 +326,66 @@ describe("API authentication", () => {
322
326
  .expect(succeedJsonWith((rows) => rows.length == 2));
323
327
  });
324
328
  });
329
+
330
+ describe("API action", () => {
331
+ it("should set up trigger", async () => {
332
+ const table = await Table.create("triggercounter");
333
+ await Field.create({
334
+ table,
335
+ name: "thing",
336
+ label: "TheThing",
337
+ type: "String",
338
+ });
339
+ await Trigger.create({
340
+ action: "run_js_code",
341
+ when_trigger: "API call",
342
+ name: "mywebhook",
343
+ min_role: 100,
344
+ configuration: {
345
+ code: `
346
+ const table = Table.findOne({ name: "triggercounter" });
347
+ await table.insertRow({ thing: row?.thing || "no body" });
348
+ return {studio: 54}
349
+ `,
350
+ },
351
+ });
352
+ });
353
+ it("should POST to trigger", async () => {
354
+ const app = await getApp({ disableCsrf: true });
355
+ await request(app)
356
+ .post("/api/action/mywebhook")
357
+ .send({
358
+ thing: "inthebody",
359
+ })
360
+ .set("Content-Type", "application/json")
361
+ .set("Accept", "application/json")
362
+ .expect(succeedJsonWithWholeBody((resp) => resp?.data?.studio === 54));
363
+ const table = Table.findOne({ name: "triggercounter" });
364
+ const counts = await table.getRows({});
365
+ expect(counts.map((c) => c.thing)).toContain("inthebody");
366
+ expect(counts.map((c) => c.thing)).not.toContain("no body");
367
+ });
368
+ it("should GET with query to trigger", async () => {
369
+ const app = await getApp({ disableCsrf: true });
370
+ await request(app)
371
+ .get("/api/action/mywebhook?thing=inthequery")
372
+ .set("Content-Type", "application/json")
373
+ .set("Accept", "application/json")
374
+ .expect(succeedJsonWithWholeBody((resp) => resp?.data?.studio === 54));
375
+ const table = Table.findOne({ name: "triggercounter" });
376
+ const counts = await table.getRows({});
377
+ expect(counts.map((c) => c.thing)).toContain("inthequery");
378
+ expect(counts.map((c) => c.thing)).not.toContain("no body");
379
+ });
380
+ it("should GET to trigger", async () => {
381
+ const app = await getApp({ disableCsrf: true });
382
+ await request(app)
383
+ .get("/api/action/mywebhook")
384
+ .set("Content-Type", "application/json")
385
+ .set("Accept", "application/json")
386
+ .expect(succeedJsonWithWholeBody((resp) => resp?.data?.studio === 54));
387
+ const table = Table.findOne({ name: "triggercounter" });
388
+ const counts = await table.getRows({});
389
+ expect(counts.map((c) => c.thing)).toContain("no body");
390
+ });
391
+ });