@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.
package/app.js CHANGED
@@ -85,6 +85,8 @@ const getApp = async (opts = {}) => {
85
85
  const development_mode = getState().getConfig("development_mode", false);
86
86
  // switch on sql logging - but it was initiated before???
87
87
  if (getState().getConfig("log_sql", false)) db.set_sql_logging();
88
+ // for multi-tenant with localhost, we need 1 instead of the default of 2
89
+ if (opts.subdomainOffset) app.set("subdomain offset", opts.subdomainOffset);
88
90
 
89
91
  // https://www.npmjs.com/package/helmet
90
92
  // helmet is secure app by adding HTTP headers
@@ -120,12 +122,10 @@ const getApp = async (opts = {}) => {
120
122
  app.use(passport.initialize());
121
123
  app.use(passport.authenticate(["jwt", "session"]));
122
124
  app.use((req, res, next) => {
123
- if (jwt_extractor(req) && req.cookies && req.cookies["connect.sid"])
124
- throw new Error(
125
- "Don't set a session cookie and JSON Web Token at the same time."
126
- );
127
- next();
128
- });
125
+ // no jwt and session id at the same time
126
+ if (!(jwt_extractor(req) && req.cookies && req.cookies["connect.sid"]))
127
+ next();
128
+ });
129
129
  app.use(flash());
130
130
 
131
131
  //static serving
@@ -168,6 +168,15 @@ const getApp = async (opts = {}) => {
168
168
  }
169
169
  )
170
170
  );
171
+ app.use(
172
+ `/static_assets/${version_tag}`,
173
+ express.static(
174
+ path.dirname(require.resolve("@saltcorn/filemanager/package.json")) + "/public/build",
175
+ {
176
+ maxAge: development_mode ? 0 : "100d",
177
+ }
178
+ )
179
+ );
171
180
 
172
181
  passport.use(
173
182
  "local",
@@ -225,8 +234,9 @@ const getApp = async (opts = {}) => {
225
234
  })
226
235
  );
227
236
  passport.use(
228
- new JwtStrategy(jwtOpts, (jwt_payload, done) => {
229
- User.findOne({ email: jwt_payload.sub }).then((u) => {
237
+ new JwtStrategy(jwtOpts, async (jwt_payload, done) => {
238
+ const userCheck = async () => {
239
+ const u = await User.findOne({ email: jwt_payload.sub });
230
240
  if (
231
241
  u &&
232
242
  u.last_mobile_login &&
@@ -242,7 +252,16 @@ const getApp = async (opts = {}) => {
242
252
  } else {
243
253
  return done(null, { role_id: 10 });
244
254
  }
245
- });
255
+ };
256
+ if (
257
+ db.is_it_multi_tenant() &&
258
+ jwt_payload.tenant?.length > 0 &&
259
+ jwt_payload.tenant !== db.connectObj.default_schema
260
+ ) {
261
+ return await db.runWithTenant(jwt_payload.tenant, userCheck);
262
+ } else {
263
+ return await userCheck();
264
+ }
246
265
  })
247
266
  );
248
267
  passport.use(
@@ -259,6 +278,12 @@ const getApp = async (opts = {}) => {
259
278
  passport.deserializeUser(function (user, done) {
260
279
  done(null, user);
261
280
  });
281
+ app.use(function (req, res, next) {
282
+ if (req.headers["x-saltcorn-client"] === "mobile-app") {
283
+ req.smr = true; // saltcorn-mobile-request
284
+ }
285
+ return next();
286
+ });
262
287
  app.use(setTenant);
263
288
 
264
289
  // Change into s3storage compatible selector
@@ -267,12 +292,15 @@ const getApp = async (opts = {}) => {
267
292
  app.use(s3storage.middlewareTransform);
268
293
 
269
294
  app.use(wrapper(version_tag));
295
+
270
296
  const csurf = csrf();
271
297
  if (!opts.disableCsrf)
272
298
  app.use(function (req, res, next) {
273
299
  if (
274
- req.url.startsWith("/api/") ||
275
- req.url === "/auth/login-with/jwt" ||
300
+ (req.smr &&
301
+ (req.url.startsWith("/api/") ||
302
+ req.url === "/auth/login-with/jwt" ||
303
+ req.url === "/auth/signup")) ||
276
304
  jwt_extractor(req)
277
305
  )
278
306
  return disabledCsurf(req, res, next);
@@ -280,13 +308,6 @@ const getApp = async (opts = {}) => {
280
308
  });
281
309
  else app.use(disabledCsurf);
282
310
 
283
- app.use(function (req, res, next) {
284
- if (req.headers["x-saltcorn-client"] === "mobile-app") {
285
- req.smr = true; // saltcorn-mobile-request
286
- }
287
- return next();
288
- });
289
-
290
311
  mountRoutes(app);
291
312
  // set tenant homepage as / root
292
313
  app.get("/", error_catcher(homepage));
@@ -327,13 +348,13 @@ Sitemap: ${base}sitemap.xml
327
348
  <urlset
328
349
  xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
329
350
  ${urls
330
- .map(
331
- (url) => `<url>
351
+ .map(
352
+ (url) => `<url>
332
353
  <loc>${url}</loc>
333
354
  <lastmod>${now}</lastmod>
334
355
  </url>`
335
- )
336
- .join("")}
356
+ )
357
+ .join("")}
337
358
 
338
359
  </urlset>`);
339
360
  })
package/auth/admin.js CHANGED
@@ -38,7 +38,9 @@ const {
38
38
  is_hsts_tld,
39
39
  } = require("../markup/admin");
40
40
  const { send_verification_email } = require("@saltcorn/data/models/email");
41
-
41
+ const {
42
+ expressionValidator,
43
+ } = require("@saltcorn/data/models/expression");
42
44
  /**
43
45
  * @type {object}
44
46
  * @const
@@ -177,33 +179,33 @@ const user_dropdown = (user, req, can_reset) =>
177
179
  req
178
180
  ),
179
181
  can_reset &&
180
- post_dropdown_item(
181
- `/useradmin/reset-password/${user.id}`,
182
- '<i class="fas fa-envelope"></i>&nbsp;' +
183
- req.__("Send password reset email"),
184
- req
185
- ),
182
+ post_dropdown_item(
183
+ `/useradmin/reset-password/${user.id}`,
184
+ '<i class="fas fa-envelope"></i>&nbsp;' +
185
+ req.__("Send password reset email"),
186
+ req
187
+ ),
186
188
  can_reset &&
187
- !user.verified_on &&
188
- getState().getConfig("verification_view", "") &&
189
- post_dropdown_item(
190
- `/useradmin/send-verification/${user.id}`,
191
- '<i class="fas fa-envelope"></i>&nbsp;' +
192
- req.__("Send verification email"),
193
- req
194
- ),
189
+ !user.verified_on &&
190
+ getState().getConfig("verification_view", "") &&
191
+ post_dropdown_item(
192
+ `/useradmin/send-verification/${user.id}`,
193
+ '<i class="fas fa-envelope"></i>&nbsp;' +
194
+ req.__("Send verification email"),
195
+ req
196
+ ),
195
197
  user.disabled &&
196
- post_dropdown_item(
197
- `/useradmin/enable/${user.id}`,
198
- '<i class="fas fa-play"></i>&nbsp;' + req.__("Enable"),
199
- req
200
- ),
198
+ post_dropdown_item(
199
+ `/useradmin/enable/${user.id}`,
200
+ '<i class="fas fa-play"></i>&nbsp;' + req.__("Enable"),
201
+ req
202
+ ),
201
203
  !user.disabled &&
202
- post_dropdown_item(
203
- `/useradmin/disable/${user.id}`,
204
- '<i class="fas fa-pause"></i>&nbsp;' + req.__("Disable"),
205
- req
206
- ),
204
+ post_dropdown_item(
205
+ `/useradmin/disable/${user.id}`,
206
+ '<i class="fas fa-pause"></i>&nbsp;' + req.__("Disable"),
207
+ req
208
+ ),
207
209
  div({ class: "dropdown-divider" }),
208
210
  post_dropdown_item(
209
211
  `/useradmin/delete/${user.id}`,
@@ -257,8 +259,8 @@ router.get(
257
259
  key: (r) =>
258
260
  !!r.verified_on
259
261
  ? i({
260
- class: "fas fa-check-circle text-success",
261
- })
262
+ class: "fas fa-check-circle text-success",
263
+ })
262
264
  : "",
263
265
  },
264
266
  { label: req.__("Role"), key: (r) => roleMap[r.role_id] },
@@ -421,15 +423,15 @@ router.get(
421
423
  above: [
422
424
  ...(letsencrypt && has_custom
423
425
  ? [
424
- {
425
- type: "card",
426
- contents: p(
427
- req.__(
428
- "You have enabled both Let's Encrypt certificates and custom SSL certificates. Let's Encrypt takes priority and the custom certificates will be ignored."
429
- )
430
- ),
431
- },
432
- ]
426
+ {
427
+ type: "card",
428
+ contents: p(
429
+ req.__(
430
+ "You have enabled both Let's Encrypt certificates and custom SSL certificates. Let's Encrypt takes priority and the custom certificates will be ignored."
431
+ )
432
+ ),
433
+ },
434
+ ]
433
435
  : []),
434
436
  {
435
437
  type: "card",
@@ -450,33 +452,33 @@ router.get(
450
452
  ),
451
453
  letsencrypt
452
454
  ? post_btn(
453
- "/config/delete/letsencrypt",
454
- req.__("Disable LetsEncrypt HTTPS"),
455
- req.csrfToken(),
456
- { btnClass: "btn-danger", req }
457
- )
455
+ "/config/delete/letsencrypt",
456
+ req.__("Disable LetsEncrypt HTTPS"),
457
+ req.csrfToken(),
458
+ { btnClass: "btn-danger", req }
459
+ )
458
460
  : post_btn(
459
- "/admin/enable-letsencrypt",
460
- req.__("Enable LetsEncrypt HTTPS"),
461
- req.csrfToken(),
462
- { confirm: true, req }
463
- ),
461
+ "/admin/enable-letsencrypt",
462
+ req.__("Enable LetsEncrypt HTTPS"),
463
+ req.csrfToken(),
464
+ { confirm: true, req }
465
+ ),
464
466
  !letsencrypt &&
465
- show_warning &&
466
- !has_custom &&
467
- div(
468
- { class: "mt-3 alert alert-danger" },
469
- p(
470
- req.__(
471
- "The address you are using to reach Saltcorn does not match the Base URL."
472
- )
473
- ),
474
- p(
475
- req.__(
476
- "The DNS A records (for * and @, or a subdomain) should point to this server's IP address before enabling LetsEncrypt"
477
- )
467
+ show_warning &&
468
+ !has_custom &&
469
+ div(
470
+ { class: "mt-3 alert alert-danger" },
471
+ p(
472
+ req.__(
473
+ "The address you are using to reach Saltcorn does not match the Base URL."
478
474
  )
479
475
  ),
476
+ p(
477
+ req.__(
478
+ "The DNS A records (for * and @, or a subdomain) should point to this server's IP address before enabling LetsEncrypt"
479
+ )
480
+ )
481
+ ),
480
482
  ],
481
483
  },
482
484
  {
@@ -570,8 +572,8 @@ router.post(
570
572
  req.flash(
571
573
  "success",
572
574
  req.__("Custom SSL enabled. Restart for changes to take effect.") +
573
- " " +
574
- a({ href: "/admin/system" }, req.__("Restart here"))
575
+ " " +
576
+ a({ href: "/admin/system" }, req.__("Restart here"))
575
577
  );
576
578
  if (!req.xhr) {
577
579
  res.redirect("/useradmin/ssl");
@@ -580,6 +582,103 @@ router.post(
580
582
  })
581
583
  );
582
584
 
585
+ /**
586
+ * @name get/ssl/custom
587
+ * @function
588
+ * @memberof module:auth/admin~auth/adminRouter
589
+ */
590
+ router.get(
591
+ "/table-access",
592
+ isAdmin,
593
+ error_catcher(async (req, res) => {
594
+ const tables = await Table.find()
595
+ const roleOptions = (await User.get_roles()).map((r) => ({
596
+ value: r.id,
597
+ label: r.role,
598
+ }));
599
+
600
+ const contents = []
601
+ for (const table of tables) {
602
+ if (table.external) continue
603
+ const fields = await table.getFields();
604
+ const userFields = fields
605
+ .filter((f) => f.reftable_name === "users")
606
+ .map((f) => ({ value: f.id, label: f.name }));
607
+ const form = new Form({
608
+ action: "/table",
609
+ noSubmitButton: true,
610
+ onChange: "saveAndContinue(this)",
611
+ fields: [
612
+ {
613
+ label: req.__("Ownership field"),
614
+ name: "ownership_field_id",
615
+ sublabel: req.__(
616
+ "The user referred to in this field will be the owner of the row"
617
+ ),
618
+ input_type: "select",
619
+ options: [
620
+ { value: "", label: req.__("None") },
621
+ ...userFields,
622
+ { value: "_formula", label: req.__("Formula") },
623
+ ],
624
+ },
625
+ {
626
+ name: "ownership_formula",
627
+ label: req.__("Ownership formula"),
628
+ validator: expressionValidator,
629
+ type: "String",
630
+ class: "validate-expression",
631
+ sublabel:
632
+ req.__("User is treated as owner if true. In scope: ") +
633
+ ["user", ...fields.map((f) => f.name)]
634
+ .map((fn) => code(fn))
635
+ .join(", "),
636
+ showIf: { ownership_field_id: "_formula" },
637
+ },
638
+ {
639
+ label: req.__("Minimum role to read"),
640
+ sublabel: req.__(
641
+ "User must have this role or higher to read rows from the table, unless they are the owner"
642
+ ),
643
+ name: "min_role_read",
644
+ input_type: "select",
645
+ options: roleOptions,
646
+ attributes: { asideNext: true }
647
+ },
648
+ {
649
+ label: req.__("Minimum role to write"),
650
+ name: "min_role_write",
651
+ input_type: "select",
652
+ sublabel: req.__(
653
+ "User must have this role or higher to edit or create new rows in the table, unless they are the owner"
654
+ ),
655
+ options: roleOptions,
656
+ },
657
+ ]
658
+ })
659
+ form.hidden("id", "name");
660
+ form.values = table
661
+ if (table.ownership_formula && !table.ownership_field_id)
662
+ form.values.ownership_field_id = "_formula";
663
+ contents.push(div(
664
+ h5(a({ href: `/table/${table.id}` }, table.name)),
665
+ renderForm(form, req.csrfToken())
666
+ ))
667
+ }
668
+ send_users_page({
669
+ res,
670
+ req,
671
+ active_sub: "Table access",
672
+ contents: {
673
+ type: "card",
674
+ title: req.__("Table access"),
675
+ contents
676
+ },
677
+ });
678
+ })
679
+ );
680
+
681
+
583
682
  /**
584
683
  * @name get/:id
585
684
  * @function
@@ -613,9 +712,9 @@ router.get(
613
712
  div(
614
713
  user.api_token
615
714
  ? span(
616
- { class: "me-1" },
617
- req.__("API token for this user: ")
618
- ) + code(user.api_token)
715
+ { class: "me-1" },
716
+ req.__("API token for this user: ")
717
+ ) + code(user.api_token)
619
718
  : req.__("No API token issued")
620
719
  ),
621
720
  // button for reset or generate api token
@@ -629,16 +728,16 @@ router.get(
629
728
  ),
630
729
  // button for remove api token
631
730
  user.api_token &&
632
- div(
633
- { class: "mt-4 ms-2 d-inline-block" },
634
- post_btn(
635
- `/useradmin/remove-api-token/${user.id}`,
636
- // TBD localization
637
- user.api_token ? req.__("Remove") : req.__("Generate"),
638
- req.csrfToken(),
639
- { req: req, confirm: true }
640
- )
641
- ),
731
+ div(
732
+ { class: "mt-4 ms-2 d-inline-block" },
733
+ post_btn(
734
+ `/useradmin/remove-api-token/${user.id}`,
735
+ // TBD localization
736
+ user.api_token ? req.__("Remove") : req.__("Generate"),
737
+ req.csrfToken(),
738
+ { req: req, confirm: true }
739
+ )
740
+ ),
642
741
  ],
643
742
  },
644
743
  ],
package/auth/routes.js CHANGED
@@ -199,33 +199,66 @@ const getAuthLinks = (current, noMethods) => {
199
199
  return links;
200
200
  };
201
201
 
202
- const loginWithJwt = async (email, password, res) => {
203
- const user = await User.findOne({ email });
204
- if (user && user.checkPassword(password)) {
205
- const now = new Date();
202
+ const loginWithJwt = async (email, password, saltcornApp, res) => {
203
+ const loginFn = async () => {
204
+ const publicUserLink = getState().getConfig("public_user_link");
206
205
  const jwt_secret = db.connectObj.jwt_secret;
207
- const token = jwt.sign(
208
- {
209
- sub: email,
210
- user: {
211
- id: user.id,
212
- email: user.email,
213
- role_id: user.role_id,
214
- language: user.language ? user.language : "en",
215
- disabled: user.disabled,
206
+ if (email && password) {
207
+ // with credentials
208
+ const user = await User.findOne({ email });
209
+ if (user && user.checkPassword(password)) {
210
+ const now = new Date();
211
+ const token = jwt.sign(
212
+ {
213
+ sub: email,
214
+ user: {
215
+ id: user.id,
216
+ email: user.email,
217
+ role_id: user.role_id,
218
+ language: user.language ? user.language : "en",
219
+ disabled: user.disabled,
220
+ },
221
+ iss: "saltcorn@saltcorn",
222
+ aud: "saltcorn-mobile-app",
223
+ iat: now.valueOf(),
224
+ tenant: db.getTenantSchema(),
225
+ },
226
+ jwt_secret
227
+ );
228
+ if (!user.last_mobile_login) await user.updateLastMobileLogin(now);
229
+ res.json(token);
230
+ } else {
231
+ res.json({
232
+ alerts: [{ type: "danger", msg: "Incorrect user or password" }],
233
+ });
234
+ }
235
+ } else if (publicUserLink) {
236
+ // public login
237
+ const token = jwt.sign(
238
+ {
239
+ sub: "public",
240
+ user: {
241
+ role_id: 10,
242
+ language: "en",
243
+ },
244
+ iss: "saltcorn@saltcorn",
245
+ aud: "saltcorn-mobile-app",
246
+ iat: new Date().valueOf(),
247
+ tenant: db.getTenantSchema(),
216
248
  },
217
- iss: "saltcorn@saltcorn",
218
- aud: "saltcorn-mobile-app",
219
- iat: now.valueOf(),
220
- },
221
- jwt_secret
222
- );
223
- if (!user.last_mobile_login) await user.updateLastMobileLogin(now);
224
- res.json(token);
249
+ jwt_secret
250
+ );
251
+ res.json(token);
252
+ } else {
253
+ res.json({
254
+ alerts: [{ type: "danger", msg: "The public login is deactivated" }],
255
+ });
256
+ }
257
+ };
258
+ if (saltcornApp && saltcornApp !== db.connectObj.default_schema) {
259
+ await db.runWithTenant(saltcornApp, loginFn);
225
260
  } else {
226
- res.json({
227
- alerts: [{ type: "danger", msg: "Incorrect user or password" }],
228
- });
261
+ await loginFn();
229
262
  }
230
263
  };
231
264
 
@@ -899,7 +932,13 @@ router.post(
899
932
  } else {
900
933
  const u = await User.create({ email, password });
901
934
  await send_verification_email(u, req);
902
- if (req.smr) await loginWithJwt(email, password, res);
935
+ if (req.smr)
936
+ await loginWithJwt(
937
+ email,
938
+ password,
939
+ req.headers["x-saltcorn-app"],
940
+ res
941
+ );
903
942
  else signup_login_with_user(u, req, res);
904
943
  }
905
944
  }
@@ -1008,7 +1047,7 @@ router.get(
1008
1047
  const { method } = req.params;
1009
1048
  if (method === "jwt") {
1010
1049
  const { email, password } = req.query;
1011
- await loginWithJwt(email, password, res);
1050
+ await loginWithJwt(email, password, req.headers["x-saltcorn-app"], res);
1012
1051
  } else {
1013
1052
  const auth = getState().auth_methods[method];
1014
1053
  if (auth) {
@@ -1144,7 +1183,7 @@ const setLanguageForm = (req, user) =>
1144
1183
  option(
1145
1184
  {
1146
1185
  value: locale,
1147
- ...(user && user.language === locale && { selected: true }),
1186
+ ...(((user && user.language === locale) || (user && !user.language && req.getLocale() === locale)) && { selected: true }),
1148
1187
  },
1149
1188
  language
1150
1189
  )
@@ -1368,7 +1407,7 @@ router.get(
1368
1407
  return;
1369
1408
  }
1370
1409
  res.sendWrap(
1371
- req.__("User settings"),
1410
+ req.__("User settings") || "User settings",
1372
1411
  await userSettings({ req, res, pwform: changPwForm(req), user })
1373
1412
  );
1374
1413
  })
package/locales/en.json CHANGED
@@ -983,5 +983,57 @@
983
983
  "Restore/download automated backups &raquo;": "Restore/download automated backups &raquo;",
984
984
  "Snapshots store your application structure and definition, without the table data. Individual views and pages can be restored from snapshots from the <a href='/viewedit'>view</a> or <a href='/pageedit'>pages</a> overviews (\"Restore\" from individual page or view dropdowns).": "Snapshots store your application structure and definition, without the table data. Individual views and pages can be restored from snapshots from the <a href='/viewedit'>view</a> or <a href='/pageedit'>pages</a> overviews (\"Restore\" from individual page or view dropdowns).",
985
985
  "List/download snapshots &raquo;": "List/download snapshots &raquo;",
986
- "Discover tables that are already in the Database, but not known to Saltcorn": "Discover tables that are already in the Database, but not known to Saltcorn"
987
- }
986
+ "Discover tables that are already in the Database, but not known to Saltcorn": "Discover tables that are already in the Database, but not known to Saltcorn",
987
+ "Split paste": "Split paste",
988
+ "Separate paste content into separate inputs": "Separate paste content into separate inputs",
989
+ "Add entries to tag": "Add entries to tag",
990
+ "Add pages": "Add pages",
991
+ "Add triggers": "Add triggers",
992
+ "Formula value": "Formula value",
993
+ "The build was successfully": "The build was successfully",
994
+ "Unable to build the app": "Unable to build the app",
995
+ "Add tag": "Add tag",
996
+ "Create new row": "Create new row",
997
+ "Specify how to create a new row": "Specify how to create a new row",
998
+ "Preview": "Preview",
999
+ "Minimum role updated": "Minimum role updated",
1000
+ "Module not found": "Module not found",
1001
+ "View %s not found": "View %s not found",
1002
+ "Query %s not found": "Query %s not found",
1003
+ "Open": "Open",
1004
+ "Only the android build supports docker.": "Only the android build supports docker.",
1005
+ "Please enter a valid server URL.": "Please enter a valid server URL.",
1006
+ "Table access": "Table access",
1007
+ "Download one of the backups above": "Download one of the backups above",
1008
+ "Clear this application": "Clear this application",
1009
+ "(tick all boxes)": "(tick all boxes)",
1010
+ "When prompted to create the first user, click the link to restore a backup": "When prompted to create the first user, click the link to restore a backup",
1011
+ "Select the downloaded backup file": "Select the downloaded backup file",
1012
+ "Units": "Units",
1013
+ "Descending?": "Descending?",
1014
+ "Small": "Small",
1015
+ "Medium": "Medium",
1016
+ "Large": "Large",
1017
+ "Extra-large": "Extra-large",
1018
+ "Please select at least one item": "Please select at least one item",
1019
+ "Only include rows where this formula is true. ": "Only include rows where this formula is true. ",
1020
+ "Use %s to access current user ID": "Use %s to access current user ID",
1021
+ "Action not found": "Action not found",
1022
+ "Development settings": "Development settings",
1023
+ "All entities": "All entities",
1024
+ "no tags": "no tags",
1025
+ "Development mode settings updated": "Development mode settings updated",
1026
+ "Locale identifier short code, e.g. en, zh, fr, ar etc. ": "Locale identifier short code, e.g. en, zh, fr, ar etc. ",
1027
+ "Is this the default language in which the application is built?": "Is this the default language in which the application is built?",
1028
+ "Database type": "Database type",
1029
+ "Database schema name": "Database schema name",
1030
+ "Database user": "Database user",
1031
+ "Database host": "Database host",
1032
+ "Database port": "Database port",
1033
+ "Creator email": "Creator email",
1034
+ "Create tenant warning text": "Create tenant warning text",
1035
+ "Provide your own create warning text if need": "Provide your own create warning text if need",
1036
+ "Specify some description for tenant if need": "Specify some description for tenant if need",
1037
+ "Created": "Created",
1038
+ "First user E-mail": "First user E-mail"
1039
+ }