@easy-cms/plugin-multi-tenant 0.47.3 → 0.48.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/dist/index.d.ts CHANGED
@@ -42,6 +42,12 @@ interface MultiTenantOptions<S extends string = string, T extends string = strin
42
42
  readonly collections: readonly S[];
43
43
  /** Globals with a value per tenant, e.g. `['site-settings']`. Others are shared. */
44
44
  readonly globals?: readonly string[];
45
+ /**
46
+ * Shared collections and globals (not listed above) that the people of a tenant, its admins
47
+ * included, may change. A change applies to every tenant. Default: none; only users with
48
+ * access to all tenants change shared data.
49
+ */
50
+ readonly editShared?: readonly string[];
45
51
  /** Slug of the tenants collection. Default `tenants`. */
46
52
  readonly tenantsSlug?: T;
47
53
  /**
package/dist/index.js CHANGED
@@ -61,7 +61,7 @@ import {
61
61
  // src/info.ts
62
62
  var INFO = {
63
63
  name: "@easy-cms/plugin-multi-tenant",
64
- ...true ? { version: "0.47.3" } : {}
64
+ ...true ? { version: "0.48.0" } : {}
65
65
  };
66
66
 
67
67
  // src/members.ts
@@ -70,6 +70,24 @@ import {
70
70
  NotFoundError,
71
71
  ValidationError
72
72
  } from "@easy-cms/core";
73
+ async function addedEmail(cms, tenantsSlug, tenant, to, origin) {
74
+ const found = await cms.findById(tenantsSlug, tenant, { depth: 0 });
75
+ const name = typeof found?.name === "string" ? found.name : String(tenant);
76
+ const admin = `${cms.config.serverURL ?? origin}${cms.config.admin.path}`;
77
+ return {
78
+ to,
79
+ subject: `You were added to ${name}`,
80
+ text: [
81
+ `An admin of ${name} added you to it. Sign in as before to work there:`,
82
+ admin,
83
+ "",
84
+ `\u0E1C\u0E39\u0E49\u0E14\u0E39\u0E41\u0E25\u0E02\u0E2D\u0E07 ${name} \u0E40\u0E1E\u0E34\u0E48\u0E21\u0E04\u0E38\u0E13\u0E40\u0E02\u0E49\u0E32\u0E44\u0E1B\u0E41\u0E25\u0E49\u0E27 \u0E40\u0E02\u0E49\u0E32\u0E2A\u0E39\u0E48\u0E23\u0E30\u0E1A\u0E1A\u0E14\u0E49\u0E27\u0E22\u0E1A\u0E31\u0E0D\u0E0A\u0E35\u0E40\u0E14\u0E34\u0E21\u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E17\u0E33\u0E07\u0E32\u0E19\u0E17\u0E35\u0E48\u0E19\u0E31\u0E48\u0E19:`,
85
+ admin,
86
+ "",
87
+ "If you don't know why, ask your admin. / \u0E2B\u0E32\u0E01\u0E44\u0E21\u0E48\u0E17\u0E23\u0E32\u0E1A\u0E17\u0E35\u0E48\u0E21\u0E32 \u0E42\u0E1B\u0E23\u0E14\u0E2A\u0E2D\u0E1A\u0E16\u0E32\u0E21\u0E1C\u0E39\u0E49\u0E14\u0E39\u0E41\u0E25"
88
+ ].join("\n")
89
+ };
90
+ }
73
91
  function membersEndpoints(options) {
74
92
  const tenantFor = (user, context) => {
75
93
  const { tenant } = tenantOf(context);
@@ -139,6 +157,12 @@ function membersEndpoints(options) {
139
157
  });
140
158
  const existing = docs[0];
141
159
  if (existing) {
160
+ const account = { id: existing.id, email, role: existing.role };
161
+ const memberRoles = cms.config.auth.members?.roles ?? [];
162
+ if (memberRoles.includes(String(existing.role)) || options.isSuper(account))
163
+ throw new ValidationError("users", [
164
+ { field: "email", message: "can't be added to a tenant" }
165
+ ]);
142
166
  const list = rows(existing).filter((row) => !sameId(row.tenant, tenant));
143
167
  const doc2 = await cms.update(
144
168
  "users",
@@ -146,7 +170,17 @@ function membersEndpoints(options) {
146
170
  { [MEMBERSHIPS_FIELD]: [...list, { tenant, role }] },
147
171
  { depth: 0 }
148
172
  );
149
- return { member: member(doc2, tenant), invited: false };
173
+ let invited2 = false;
174
+ if (cms.auth.canSendPasswordLinks(url.origin)) {
175
+ await cms.sendEmail(
176
+ await addedEmail(cms, options.tenantsSlug, tenant, email, url.origin)
177
+ );
178
+ invited2 = true;
179
+ }
180
+ return Response.json(
181
+ { member: { ...member(doc2, tenant), name: null }, invited: invited2 },
182
+ { status: 201 }
183
+ );
150
184
  }
151
185
  const doc = await cms.create(
152
186
  "users",
@@ -240,6 +274,8 @@ function multiTenantPlugin(options) {
240
274
  );
241
275
  const superUser = (user, _context) => !!user && user.member !== true && user.scoped !== true && isSuper(user);
242
276
  const everywhere = (user, context) => tenantOf(context).allTenants || superUser(user, context);
277
+ const editShared = new Set(options.editShared ?? []);
278
+ const sharedWrite = (slug, original) => async (args) => args.user?.scoped === true && !editShared.has(slug) ? false : (original ?? isLoggedIn)(args);
243
279
  const tenantFilter = (args, op) => {
244
280
  const { tenant } = tenantOf(args.context);
245
281
  if (op === "create") return tenant !== null || superUser(args.user, args.context);
@@ -482,7 +518,17 @@ function multiTenantPlugin(options) {
482
518
  access: { ...c.access, ...usersAccess(c.access) }
483
519
  });
484
520
  } else if (scoped.has(c.slug)) collections.push(scopeCollection(c, BUILTIN_ACCESS[c.slug]));
485
- else collections.push({ ...c, fields: withTenantFilters(c.fields) });
521
+ else
522
+ collections.push({
523
+ ...c,
524
+ fields: withTenantFilters(c.fields),
525
+ access: {
526
+ ...c.access,
527
+ create: sharedWrite(c.slug, c.access?.create),
528
+ update: sharedWrite(c.slug, c.access?.update),
529
+ delete: sharedWrite(c.slug, c.access?.delete)
530
+ }
531
+ });
486
532
  }
487
533
  if (!hasUsers) collections.push({ slug: "users", fields: [memberships], access: usersAccess() });
488
534
  for (const builtin of [MEDIA, MEDIA_FOLDERS]) {
@@ -500,7 +546,11 @@ function multiTenantPlugin(options) {
500
546
  }
501
547
  const globals = (config.globals ?? []).map((g) => {
502
548
  const filtered = { ...g, fields: withTenantFilters(g.fields) };
503
- if (!perTenant.has(g.slug)) return filtered;
549
+ if (!perTenant.has(g.slug))
550
+ return {
551
+ ...filtered,
552
+ access: { ...g.access, update: sharedWrite(g.slug, g.access?.update) }
553
+ };
504
554
  return {
505
555
  ...filtered,
506
556
  // One value per tenant; none when the request has no tenant.
@@ -662,7 +712,7 @@ of its own. Run it once after adding the plugin to a site that has content.
662
712
  ...config.endpoints ?? [],
663
713
  optionsEndpoint,
664
714
  impactEndpoint,
665
- ...membersEndpoints({ tenantsSlug, superUser })
715
+ ...membersEndpoints({ tenantsSlug, superUser, isSuper })
666
716
  ],
667
717
  commands: [...config.commands ?? [], assign],
668
718
  // Audit log entries belong to their tenant: its admins see them.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/plugin-multi-tenant",
3
- "version": "0.47.3",
3
+ "version": "0.48.0",
4
4
  "description": "Multi-tenant Easy CMS: several sites or clients in one CMS, each with its own content, members and roles",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,14 +17,14 @@
17
17
  "dist"
18
18
  ],
19
19
  "peerDependencies": {
20
- "@easy-cms/core": "^0.47.3"
20
+ "@easy-cms/core": "^0.48.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@easy-cms/core": "^0.47.3",
24
- "@easy-cms/db-sqlite": "^0.47.3",
25
- "@easy-cms/plugin-form-builder": "^0.47.3",
26
- "@easy-cms/plugin-nested-docs": "^0.47.3",
27
- "@easy-cms/plugin-redirects": "^0.47.3"
23
+ "@easy-cms/core": "^0.48.0",
24
+ "@easy-cms/db-sqlite": "^0.48.0",
25
+ "@easy-cms/plugin-form-builder": "^0.48.0",
26
+ "@easy-cms/plugin-nested-docs": "^0.48.0",
27
+ "@easy-cms/plugin-redirects": "^0.48.0"
28
28
  },
29
29
  "engines": {
30
30
  "node": ">=22.12"