@saltcorn/server 0.7.4-beta.3 → 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.
@@ -1,9 +1,6 @@
1
1
  const {
2
- a,
3
2
  div,
4
- text,
5
3
  button,
6
- i,
7
4
  form,
8
5
  select,
9
6
  option,
@@ -81,10 +78,10 @@ const formOptions = async (type, tag_id) => {
81
78
  ),
82
79
  };
83
80
  }
84
- case "trigger": {
81
+ case "triggers": {
85
82
  const ids = await tag.getTriggerIds();
86
83
  return {
87
- trigger: (await Trigger.find()).filter(
84
+ triggers: (Trigger.find()).filter(
88
85
  (value) => ids.indexOf(value.id) === -1
89
86
  ),
90
87
  };
@@ -101,7 +98,7 @@ router.get(
101
98
  above: [
102
99
  {
103
100
  type: "breadcrumbs",
104
- crumbs: [{ text: `Tag entry` }],
101
+ crumbs: [{ text: req.__(`Tag entry`) }],
105
102
  },
106
103
  {
107
104
  type: "card",
@@ -120,15 +117,19 @@ router.get(
120
117
 
121
118
  const idField = (entryType) => {
122
119
  switch (entryType) {
123
- case "tables": {
120
+ case "tables":
121
+ case "table": {
124
122
  return "table_id";
125
123
  }
126
- case "views": {
124
+ case "views":
125
+ case "view": {
127
126
  return "view_id";
128
127
  }
129
- case "pages": {
128
+ case "pages":
129
+ case "page": {
130
130
  return "page_id";
131
131
  }
132
+ case "triggers":
132
133
  case "trigger": {
133
134
  return "trigger_id";
134
135
  }
@@ -136,6 +137,7 @@ const idField = (entryType) => {
136
137
  return null;
137
138
  };
138
139
 
140
+ // add multiple objects to one tag
139
141
  router.post(
140
142
  "/add/:entry_type/:tag_id",
141
143
  isAdmin,
@@ -155,6 +157,24 @@ router.post(
155
157
  })
156
158
  );
157
159
 
160
+ // add one object to multiple tags
161
+ router.post(
162
+ "/add/multiple_tags/:entry_type/:object_id",
163
+ isAdmin,
164
+ error_catcher(async (req, res) => {
165
+ let { entry_type, object_id } = req.params;
166
+ let { tag_ids } = req.body;
167
+ object_id = parseInt(object_id);
168
+ tag_ids = tag_ids.map((id) => parseInt(id));
169
+ const tags = (await Tag.find()).filter((tag) => tag_ids.includes(tag.id));
170
+ const fieldName = idField(entry_type);
171
+ for (const tag of tags) {
172
+ await tag.addEntry({ [fieldName]: object_id });
173
+ }
174
+ res.json({ tags });
175
+ })
176
+ );
177
+
158
178
  router.post(
159
179
  "/remove/:entry_type/:entry_id/:tag_id",
160
180
  isAdmin,
@@ -168,6 +188,7 @@ router.post(
168
188
  } else {
169
189
  await TagEntry.update(entry.id, { [fieldName]: null });
170
190
  }
171
- res.redirect(`/tag/${tag_id}?show_list=${entry_type}`);
191
+ if (!req.xhr) res.redirect(`/tag/${tag_id}?show_list=${entry_type}`);
192
+ else res.json({ okay: true });
172
193
  })
173
194
  );
package/routes/tags.js CHANGED
@@ -35,30 +35,34 @@ router.get(
35
35
  res,
36
36
  req,
37
37
  active_sub: "Tags",
38
- contents: [
39
- mkTable(
40
- [
41
- {
42
- label: req.__("Tagname"),
43
- key: (r) =>
44
- link(`/tag/${r.id || r.name}?show_list=tables`, text(r.name)),
45
- },
38
+ contents: {
39
+ type: "card",
40
+ title: req.__("Tags"),
41
+ contents: [
42
+ mkTable(
43
+ [
44
+ {
45
+ label: req.__("Tagname"),
46
+ key: (r) =>
47
+ link(`/tag/${r.id || r.name}?show_list=tables`, text(r.name)),
48
+ },
49
+ {
50
+ label: req.__("Delete"),
51
+ key: (r) => post_delete_btn(`/tag/delete/${r.id}`, req, r.name),
52
+ },
53
+ ],
54
+ rows,
55
+ {}
56
+ ),
57
+ a(
46
58
  {
47
- label: req.__("Delete"),
48
- key: (r) => post_delete_btn(`/tag/delete/${r.id}`, req, r.name),
59
+ href: `/tag/new`,
60
+ class: "btn btn-primary",
49
61
  },
50
- ],
51
- rows,
52
- {}
53
- ),
54
- a(
55
- {
56
- href: `/tag/new`,
57
- class: "btn btn-primary",
58
- },
59
- req.__("Create tag")
60
- ),
61
- ],
62
+ req.__("Create tag")
63
+ ),
64
+ ],
65
+ },
62
66
  });
63
67
  })
64
68
  );
@@ -124,18 +128,18 @@ router.get(
124
128
  const views = await tag.getViews();
125
129
  await setTableRefs(views);
126
130
  const pages = await tag.getPages();
127
- const trigger = await tag.getTrigger();
131
+ const triggers = await tag.getTriggers();
128
132
  const roles = await User.get_roles();
129
133
 
130
134
  const tablesDomId = "tablesListId";
131
135
  const viewsDomId = "viewsListId";
132
136
  const pagesDomId = "pagesDomId";
133
- const triggerDomId = "triggerDomId";
137
+ const triggersDomId = "triggerDomId";
134
138
  res.sendWrap(req.__("%s Tag", tag.name), {
135
139
  above: [
136
140
  {
137
141
  type: "breadcrumbs",
138
- crumbs: [{ text: `Tag: ${tag.name}` }],
142
+ crumbs: [{ text: req.__(`Tag: %s`, tag.name) }],
139
143
  },
140
144
  {
141
145
  type: "card",
@@ -207,19 +211,19 @@ router.get(
207
211
  type: "card",
208
212
  bodyId: "collapseTriggerCard",
209
213
  title: headerWithCollapser(
210
- req.__("Trigger"),
211
- triggerDomId,
212
- isShowList(show_list, "trigger")
214
+ req.__("Triggers"),
215
+ triggersDomId,
216
+ isShowList(show_list, "triggers")
213
217
  ),
214
218
  contents: [
215
- getTriggerList(trigger, req, {
219
+ getTriggerList(triggers, req, {
216
220
  tagId: tag.id,
217
- domId: triggerDomId,
218
- showList: isShowList(show_list, "trigger"),
221
+ domId: triggersDomId,
222
+ showList: isShowList(show_list, "triggers"),
219
223
  }),
220
224
  a(
221
225
  {
222
- href: `/tag-entries/add/trigger/${tag.id}`,
226
+ href: `/tag-entries/add/triggers/${tag.id}`,
223
227
  class: "btn btn-primary",
224
228
  },
225
229
  req.__("Add triggers")
package/routes/tenant.js CHANGED
@@ -8,13 +8,14 @@
8
8
  const Router = require("express-promise-router");
9
9
  const Form = require("@saltcorn/data/models/form");
10
10
  const { getState, add_tenant } = require("@saltcorn/data/db/state");
11
- const { create_tenant } = require("@saltcorn/admin-models/models/tenant");
12
11
  const {
12
+ create_tenant,
13
13
  getAllTenants,
14
14
  domain_sanitize,
15
15
  deleteTenant,
16
16
  switchToTenant,
17
17
  insertTenant,
18
+ Tenant,
18
19
  } = require("@saltcorn/admin-models/models/tenant");
19
20
  const {
20
21
  renderForm,
@@ -24,7 +25,6 @@ const {
24
25
  } = require("@saltcorn/markup");
25
26
  const {
26
27
  div,
27
- nbsp,
28
28
  p,
29
29
  a,
30
30
  h4,
@@ -87,6 +87,11 @@ const tenant_form = (req) =>
87
87
  input_type: "text",
88
88
  postText: text(req.hostname),
89
89
  },
90
+ {
91
+ name: "description",
92
+ label: req.__("Description"),
93
+ input_type: "text",
94
+ },
90
95
  ],
91
96
  });
92
97
 
@@ -148,37 +153,49 @@ router.get(
148
153
  "You are trying to create a tenant while connecting via an IP address rather than a domain. This will probably not work."
149
154
  )
150
155
  );
151
- let create_tenant_warning = "";
152
- // todo add custom create tenant warning message
153
- if (getState().getConfig("create_tenant_warning"))
154
- create_tenant_warning = div(
155
- {
156
- class: "alert alert-warning alert-dismissible fade show mt-5",
157
- role: "alert",
158
- },
159
- h4(req.__("Warning")),
160
- p(
161
- req.__(
162
- "Hosting on this site is provided for free and with no guarantee of availability or security of your application. "
163
- ) +
164
- " " +
165
- req.__(
166
- "This facility is intended solely for you to evaluate the suitability of Saltcorn. "
167
- ) +
168
- " " +
169
- req.__(
170
- "If you would like to store private information that needs to be secure, please use self-hosted Saltcorn. "
171
- ) +
172
- " " +
173
- req.__(
174
- 'See <a href="https://github.com/saltcorn/saltcorn">GitHub repository</a> for instructions<p>'
175
- )
176
- )
177
- );
156
+ let create_tenant_warning_text = "";
157
+ if (getState().getConfig("create_tenant_warning")) {
158
+ create_tenant_warning_text = getState().getConfig("create_tenant_warning_text");
159
+ if (create_tenant_warning_text && create_tenant_warning_text.length > 0)
160
+ create_tenant_warning_text = div(
161
+ {
162
+ class: "alert alert-warning alert-dismissible fade show mt-5",
163
+ role: "alert",
164
+ },
165
+ h4(req.__("Warning")),
166
+ p( create_tenant_warning_text
167
+ )
168
+ );
169
+ else
170
+ create_tenant_warning_text = div(
171
+ {
172
+ class: "alert alert-warning alert-dismissible fade show mt-5",
173
+ role: "alert",
174
+ },
175
+ h4(req.__("Warning")),
176
+ p(
177
+ req.__(
178
+ "Hosting on this site is provided for free and with no guarantee of availability or security of your application. "
179
+ ) +
180
+ " " +
181
+ req.__(
182
+ "This facility is intended solely for you to evaluate the suitability of Saltcorn. "
183
+ ) +
184
+ " " +
185
+ req.__(
186
+ "If you would like to store private information that needs to be secure, please use self-hosted Saltcorn. "
187
+ ) +
188
+ " " +
189
+ req.__(
190
+ 'See <a href="https://github.com/saltcorn/saltcorn">GitHub repository</a> for instructions<p>'
191
+ )
192
+ )
193
+ );
194
+ }
178
195
 
179
196
  res.sendWrap(
180
197
  req.__("Create application"),
181
- create_tenant_warning +
198
+ create_tenant_warning_text +
182
199
  renderForm(tenant_form(req), req.csrfToken()) +
183
200
  p(
184
201
  { class: "mt-2" },
@@ -246,6 +263,8 @@ router.post(
246
263
  else {
247
264
  // normalize domain name
248
265
  const subdomain = domain_sanitize(valres.success.subdomain);
266
+ // get description
267
+ const description = valres.success.description;
249
268
  // get list of tenants
250
269
  const allTens = await getAllTenants();
251
270
  if (allTens.includes(subdomain) || !subdomain) {
@@ -258,9 +277,15 @@ router.post(
258
277
  renderForm(form, req.csrfToken())
259
278
  );
260
279
  } else {
280
+ // tenant url
261
281
  const newurl = getNewURL(req, subdomain);
282
+ // tenant template
262
283
  const tenant_template = getState().getConfig("tenant_template");
263
- await switchToTenant(await insertTenant(subdomain), newurl);
284
+ // tenant creator
285
+ const user_email = req.user && req.user.email;
286
+ // switch to tenant
287
+ await switchToTenant(await insertTenant(subdomain, user_email, description, tenant_template), newurl);
288
+ // add tenant to global state
264
289
  add_tenant(subdomain);
265
290
  await create_tenant({
266
291
  t: subdomain,
@@ -348,6 +373,15 @@ router.get(
348
373
  {
349
374
  label: req.__("Description"),
350
375
  key: (r) => text(r.description),
376
+ //blurb: req.__("Specify some description for tenant if need"),
377
+ },
378
+ {
379
+ label: req.__("Creator email"),
380
+ key: (r) => text(r.email),
381
+ },
382
+ {
383
+ label: req.__("Created"),
384
+ key: (r) => text(r.created),
351
385
  },
352
386
  {
353
387
  label: req.__("Information"),
@@ -387,6 +421,7 @@ const tenant_settings_form = (req) =>
387
421
  field_names: [
388
422
  "role_to_create_tenant",
389
423
  "create_tenant_warning",
424
+ "create_tenant_warning_text",
390
425
  "tenant_template",
391
426
  ],
392
427
  action: "/tenant/settings",
@@ -468,8 +503,19 @@ router.post(
468
503
  const get_tenant_info = async (subdomain) => {
469
504
  const saneDomain = domain_sanitize(subdomain);
470
505
 
506
+ let info = {};
507
+
508
+ // get tenant row
509
+ const ten = await Tenant.findOne({ subdomain: saneDomain });
510
+ if (ten) {
511
+ info.description = ten.description;
512
+ info.created = ten.created;
513
+ }
514
+
515
+
516
+ // get data from tenant schema
471
517
  return await db.runWithTenant(saneDomain, async () => {
472
- let info = {};
518
+
473
519
  // TBD fix the first user issue because not always firt user by id is creator of tenant
474
520
  const firstUser = await User.find({}, { orderBy: "id", limit: 1 });
475
521
  if (firstUser && firstUser.length > 0) {
@@ -497,7 +543,11 @@ const get_tenant_info = async (subdomain) => {
497
543
  info.nconfigs = await db.count("_sc_config");
498
544
  // plugins count
499
545
  info.nplugins = await db.count("_sc_plugins");
500
- // TBD decide Do we need count tenants, table constraints, migrations
546
+ // migration count
547
+ info.nmigrations = await db.count("_sc_migrations");
548
+ // library count
549
+ info.nlibrary = await db.count("_sc_library");
550
+ // TBD decide Do we need count tenants, table constraints
501
551
  // base url
502
552
  info.base_url = await getConfig("base_url");
503
553
  return info;
@@ -525,6 +575,7 @@ router.get(
525
575
  return;
526
576
  }
527
577
  const { subdomain } = req.params;
578
+ // get tenant info
528
579
  const info = await get_tenant_info(subdomain);
529
580
  // get list of files
530
581
  let files;
@@ -545,7 +596,7 @@ router.get(
545
596
  contents: [
546
597
  table(
547
598
  tr(
548
- th(req.__("E-mail")),
599
+ th(req.__("First user E-mail")),
549
600
  td(
550
601
  a(
551
602
  { href: "mailto:" + info.first_user_email },
@@ -616,8 +667,13 @@ router.get(
616
667
  label: req.__("Base URL"),
617
668
  type: "String",
618
669
  },
670
+ {
671
+ name: "description",
672
+ label: req.__("Description"),
673
+ type: "String",
674
+ },
619
675
  ],
620
- values: { base_url: info.base_url },
676
+ values: { base_url: info.base_url, description: info.description },
621
677
  }),
622
678
  req.csrfToken()
623
679
  ),
@@ -673,6 +729,12 @@ router.post(
673
729
  const { base_url } = req.body;
674
730
  const saneDomain = domain_sanitize(subdomain);
675
731
 
732
+ // save description
733
+ const { description } = req.body;
734
+ await Tenant.update( saneDomain, {description: description});
735
+
736
+
737
+
676
738
  await db.runWithTenant(saneDomain, async () => {
677
739
  await getState().setConfig("base_url", base_url);
678
740
  });
@@ -701,7 +763,7 @@ router.post(
701
763
  return;
702
764
  }
703
765
  const { sub } = req.params;
704
-
766
+ // todo warning before deletion
705
767
  await deleteTenant(sub);
706
768
  res.redirect(`/tenant/list`);
707
769
  })
package/routes/utils.js CHANGED
@@ -20,6 +20,8 @@ const { validateHeaderName, validateHeaderValue } = require("http");
20
20
  const Crash = require("@saltcorn/data/models/crash");
21
21
 
22
22
  /**
23
+ * Checks that user logged or not.
24
+ * If not shows than shows flash and redirects to login
23
25
  * @param {object} req
24
26
  * @param {object} res
25
27
  * @param {function} next
@@ -35,6 +37,8 @@ function loggedIn(req, res, next) {
35
37
  }
36
38
 
37
39
  /**
40
+ * Checks that user has admin role or not.
41
+ * If user hasn't admin role shows flash and redirects user to login or totp
38
42
  * @param {object} req
39
43
  * @param {object} res
40
44
  * @param {function} next
@@ -60,6 +64,7 @@ function isAdmin(req, res, next) {
60
64
  }
61
65
 
62
66
  /**
67
+ * Sets language for HTTP Request / HTTP Responce
63
68
  * @param {object} req
64
69
  * @param {object} res
65
70
  * @param {string} state
@@ -73,6 +78,7 @@ const setLanguage = (req, res, state) => {
73
78
  };
74
79
 
75
80
  /**
81
+ * Sets Custom HTTP headers using data from "custom_http_headers" config variable
76
82
  * @param {object} res
77
83
  * @param {string} state
78
84
  * @returns {void}
@@ -96,6 +102,7 @@ const set_custom_http_headers = (res, state) => {
96
102
  };
97
103
 
98
104
  /**
105
+ * Tries to recognize tenant from HTTP Request
99
106
  * @param {object} req
100
107
  * @returns {string}
101
108
  */
@@ -113,37 +120,62 @@ const get_tenant_from_req = (req) => {
113
120
  };
114
121
 
115
122
  /**
123
+ * middleware to extract the tenant domain and call runWithtenant()
116
124
  * @param {object} req
117
125
  * @param {object} res
118
126
  * @param {function} next
119
127
  */
120
128
  const setTenant = (req, res, next) => {
121
129
  if (db.is_it_multi_tenant()) {
122
- const other_domain = get_other_domain_tenant(req.hostname);
123
- if (other_domain) {
124
- const state = getTenant(other_domain);
125
- if (!state) {
126
- setLanguage(req, res);
127
- next();
128
- } else {
129
- db.runWithTenant(other_domain, () => {
130
- setLanguage(req, res, state);
131
- state.log(5, `${req.method} ${req.originalUrl}`);
130
+ // for a saltcorn mobile request use 'req.user.tenant'
131
+ if (req.smr) {
132
+ if (
133
+ req.user?.tenant &&
134
+ req.user.tenant !== db.connectObj.default_schema
135
+ ) {
136
+ const state = getTenant(req.user.tenant);
137
+ if (!state) {
138
+ setLanguage(req, res);
132
139
  next();
133
- });
140
+ } else {
141
+ db.runWithTenant(req.user.tenant, () => {
142
+ setLanguage(req, res, state);
143
+ state.log(5, `${req.method} ${req.originalUrl}`);
144
+ next();
145
+ });
146
+ }
134
147
  }
135
- } else {
136
- const ten = get_tenant_from_req(req);
137
- const state = getTenant(ten);
138
- if (!state) {
148
+ else {
139
149
  setLanguage(req, res);
140
150
  next();
151
+ }
152
+ } else {
153
+ const other_domain = get_other_domain_tenant(req.hostname);
154
+ if (other_domain) {
155
+ const state = getTenant(other_domain);
156
+ if (!state) {
157
+ setLanguage(req, res);
158
+ next();
159
+ } else {
160
+ db.runWithTenant(other_domain, () => {
161
+ setLanguage(req, res, state);
162
+ state.log(5, `${req.method} ${req.originalUrl}`);
163
+ next();
164
+ });
165
+ }
141
166
  } else {
142
- db.runWithTenant(ten, () => {
143
- setLanguage(req, res, state);
144
- state.log(5, `${req.method} ${req.originalUrl}`);
167
+ const ten = get_tenant_from_req(req);
168
+ const state = getTenant(ten);
169
+ if (!state) {
170
+ setLanguage(req, res);
145
171
  next();
146
- });
172
+ } else {
173
+ db.runWithTenant(ten, () => {
174
+ setLanguage(req, res, state);
175
+ state.log(5, `${req.method} ${req.originalUrl}`);
176
+ next();
177
+ });
178
+ }
147
179
  }
148
180
  }
149
181
  } else {
@@ -154,6 +186,7 @@ const setTenant = (req, res, next) => {
154
186
  };
155
187
 
156
188
  /**
189
+ * Injects hidden input "_csrf" for CSRF token
157
190
  * @param {object} req
158
191
  * @returns {input}
159
192
  */
@@ -165,6 +198,7 @@ const csrfField = (req) =>
165
198
  });
166
199
 
167
200
  /**
201
+ * Errors catcher
168
202
  * @param {function} fn
169
203
  * @returns {function}
170
204
  */
@@ -173,6 +207,7 @@ const error_catcher = (fn) => (request, response, next) => {
173
207
  };
174
208
 
175
209
  /**
210
+ * Scans for page title from contents
176
211
  * @param {string|object} contents
177
212
  * @param {string} viewname
178
213
  * @returns {string}
@@ -193,11 +228,13 @@ const scan_for_page_title = (contents, viewname) => {
193
228
  };
194
229
 
195
230
  /**
231
+ * Gets gir revision
196
232
  * @returns {string}
197
233
  */
198
234
  const getGitRevision = () => db.connectObj.git_commit;
199
235
 
200
236
  /**
237
+ * Gets session store
201
238
  * @returns {session|cookieSession}
202
239
  */
203
240
  const getSessionStore = () => {
@@ -232,6 +269,20 @@ const getSessionStore = () => {
232
269
  }
233
270
  };
234
271
 
272
+ /**
273
+ * appends 'req.query.on_done_redirect' to 'oldPath' if it exists
274
+ * @param {string} oldPath path without 'on_done_redirect'
275
+ * @param {any} req express request
276
+ * @returns a new string with or without on_done_redirect=...
277
+ */
278
+ const addOnDoneRedirect = (oldPath, req) => {
279
+ const separator = oldPath.indexOf("?") > -1 ? "&" : "?";
280
+ if (req.query.on_done_redirect) {
281
+ return `${oldPath}${separator}on_done_redirect=${req.query.on_done_redirect}`;
282
+ }
283
+ return oldPath;
284
+ };
285
+
235
286
  module.exports = {
236
287
  sqlsanitize,
237
288
  csrfField,
@@ -243,5 +294,6 @@ module.exports = {
243
294
  getGitRevision,
244
295
  getSessionStore,
245
296
  setTenant,
246
- get_tenant_from_req
297
+ get_tenant_from_req,
298
+ addOnDoneRedirect,
247
299
  };
package/routes/view.js CHANGED
@@ -8,7 +8,6 @@ const Router = require("express-promise-router");
8
8
 
9
9
  const View = require("@saltcorn/data/models/view");
10
10
  const Table = require("@saltcorn/data/models/table");
11
- const Page = require("@saltcorn/data/models/page");
12
11
 
13
12
  const { div, text, i, a } = require("@saltcorn/markup/tags");
14
13
  const { renderForm, link } = require("@saltcorn/markup");