@easy-cms/plugin-multi-tenant 0.48.0 → 0.60.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 +15 -12
- package/dist/index.js +26 -27
- package/package.json +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ interface Member {
|
|
|
34
34
|
readonly role: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
interface
|
|
37
|
+
interface MultiTenantPluginOptions<S extends string = string, T extends string = string> {
|
|
38
38
|
/**
|
|
39
39
|
* Collections whose documents belong to one tenant, by slug, e.g. `['posts', 'pages',
|
|
40
40
|
* 'media']`. With `media` and `upload.folders`, the media folders too. Others are shared.
|
|
@@ -49,7 +49,9 @@ interface MultiTenantOptions<S extends string = string, T extends string = strin
|
|
|
49
49
|
*/
|
|
50
50
|
readonly editShared?: readonly string[];
|
|
51
51
|
/** Slug of the tenants collection. Default `tenants`. */
|
|
52
|
-
readonly
|
|
52
|
+
readonly slugs?: {
|
|
53
|
+
readonly tenants?: T;
|
|
54
|
+
};
|
|
53
55
|
/**
|
|
54
56
|
* Users who see and manage every tenant and the whole system. Default: users whose own role
|
|
55
57
|
* is `admin`.
|
|
@@ -129,24 +131,25 @@ type MultiTenantPluginTypes<S extends string, T extends string = 'tenants'> = {
|
|
|
129
131
|
* and globals with a value per tenant. Requests name their tenant with a header, `?tenant=`,
|
|
130
132
|
* the admin's cookie or the domain.
|
|
131
133
|
*/
|
|
132
|
-
declare function multiTenantPlugin<const S extends string, const T extends string = 'tenants'>(options:
|
|
134
|
+
declare function multiTenantPlugin<const S extends string, const T extends string = 'tenants'>(options: MultiTenantPluginOptions<S, T>): TypedPlugin<MultiTenantPluginTypes<S, T>>;
|
|
133
135
|
|
|
136
|
+
/** A tenant by its slug or one of its domains. */
|
|
137
|
+
interface TenantLookup {
|
|
138
|
+
readonly slug?: string;
|
|
139
|
+
readonly host?: string;
|
|
140
|
+
/** Slug of the tenants collection, when the plugin's `slugs.tenants` changed it. */
|
|
141
|
+
readonly collection?: string;
|
|
142
|
+
}
|
|
134
143
|
/**
|
|
135
144
|
* The tenant of a page of your site, by slug or by domain (`domains` of the tenant), e.g. from
|
|
136
145
|
* the request's host. `null` when none matches.
|
|
137
146
|
*/
|
|
138
|
-
declare function findTenantFor(cms: EasyCMS, by:
|
|
139
|
-
readonly slug?: string;
|
|
140
|
-
readonly host?: string;
|
|
141
|
-
}, tenantsSlug?: string): Promise<TenantRef | null>;
|
|
147
|
+
declare function findTenantFor(cms: EasyCMS, by: TenantLookup): Promise<TenantRef | null>;
|
|
142
148
|
/**
|
|
143
149
|
* The `context` for Local API calls that read one tenant's content, e.g.
|
|
144
150
|
* `cms.find('posts', { overrideAccess: false, user: null, context: await tenantContext(cms, { host }) })`.
|
|
145
151
|
* A tenant that doesn't exist gives a context that finds nothing.
|
|
146
152
|
*/
|
|
147
|
-
declare function tenantContext(cms: EasyCMS, by:
|
|
148
|
-
readonly slug?: string;
|
|
149
|
-
readonly host?: string;
|
|
150
|
-
}, tenantsSlug?: string): Promise<RequestContext>;
|
|
153
|
+
declare function tenantContext(cms: EasyCMS, by: TenantLookup): Promise<RequestContext>;
|
|
151
154
|
|
|
152
|
-
export { ALL, type Member, type Membership, type
|
|
155
|
+
export { ALL, type Member, type Membership, type MultiTenantPluginOptions, type MultiTenantPluginTypes, type TenantContext, type TenantLookup, type TenantRef, findTenantFor, multiTenantPlugin, tenantContext, tenantOf };
|
package/dist/index.js
CHANGED
|
@@ -52,16 +52,17 @@ var tenantOfHost = (list, host) => {
|
|
|
52
52
|
// src/plugin.ts
|
|
53
53
|
import {
|
|
54
54
|
definePlugin,
|
|
55
|
-
|
|
56
|
-
MEDIA,
|
|
57
|
-
MEDIA_FOLDERS,
|
|
55
|
+
isStaff,
|
|
58
56
|
ValidationError as ValidationError2
|
|
59
57
|
} from "@easy-cms/core";
|
|
58
|
+
import { checkRenamedOptions, MEDIA, MEDIA_FOLDERS } from "@easy-cms/core/internal";
|
|
60
59
|
|
|
61
60
|
// src/info.ts
|
|
62
61
|
var INFO = {
|
|
63
62
|
name: "@easy-cms/plugin-multi-tenant",
|
|
64
|
-
|
|
63
|
+
// The plugin API it is written for (`PLUGIN_API_VERSION` of @easy-cms/core).
|
|
64
|
+
apiVersion: 1,
|
|
65
|
+
...true ? { version: "0.60.0" } : {}
|
|
65
66
|
};
|
|
66
67
|
|
|
67
68
|
// src/members.ts
|
|
@@ -234,8 +235,8 @@ function membersEndpoints(options) {
|
|
|
234
235
|
|
|
235
236
|
// src/plugin.ts
|
|
236
237
|
var BUILTIN_ACCESS = {
|
|
237
|
-
[MEDIA]: { read: () => true, create:
|
|
238
|
-
[MEDIA_FOLDERS]: { read:
|
|
238
|
+
[MEDIA]: { read: () => true, create: isStaff, update: isStaff, delete: isStaff },
|
|
239
|
+
[MEDIA_FOLDERS]: { read: isStaff, create: isStaff, update: isStaff, delete: isStaff }
|
|
239
240
|
};
|
|
240
241
|
function readCookie(header, name) {
|
|
241
242
|
for (const part of header?.split(";") ?? []) {
|
|
@@ -246,8 +247,9 @@ function readCookie(header, name) {
|
|
|
246
247
|
return void 0;
|
|
247
248
|
}
|
|
248
249
|
function multiTenantPlugin(options) {
|
|
250
|
+
checkRenamedOptions("multiTenantPlugin", options, { tenantsSlug: "slugs.tenants" });
|
|
249
251
|
return definePlugin((config) => {
|
|
250
|
-
const tenantsSlug = options.
|
|
252
|
+
const tenantsSlug = options.slugs?.tenants ?? "tenants";
|
|
251
253
|
const header = (options.header ?? "x-easy-cms-tenant").toLowerCase();
|
|
252
254
|
const cookie = options.cookie ?? "ecms-tenant";
|
|
253
255
|
const publicReads = options.publicReads ?? "none";
|
|
@@ -275,7 +277,7 @@ function multiTenantPlugin(options) {
|
|
|
275
277
|
const superUser = (user, _context) => !!user && user.member !== true && user.scoped !== true && isSuper(user);
|
|
276
278
|
const everywhere = (user, context) => tenantOf(context).allTenants || superUser(user, context);
|
|
277
279
|
const editShared = new Set(options.editShared ?? []);
|
|
278
|
-
const sharedWrite = (slug, original) => async (args) => args.user?.scoped === true && !editShared.has(slug) ? false : (original ??
|
|
280
|
+
const sharedWrite = (slug, original) => async (args) => args.user?.scoped === true && !editShared.has(slug) ? false : (original ?? isStaff)(args);
|
|
279
281
|
const tenantFilter = (args, op) => {
|
|
280
282
|
const { tenant } = tenantOf(args.context);
|
|
281
283
|
if (op === "create") return tenant !== null || superUser(args.user, args.context);
|
|
@@ -284,7 +286,7 @@ function multiTenantPlugin(options) {
|
|
|
284
286
|
return superUser(args.user, args.context);
|
|
285
287
|
};
|
|
286
288
|
const narrow = (original, op) => async (args) => {
|
|
287
|
-
const base = await (original ??
|
|
289
|
+
const base = await (original ?? isStaff)(args);
|
|
288
290
|
if (base === false) return false;
|
|
289
291
|
const filter = tenantFilter(args, op);
|
|
290
292
|
if (filter === false) return false;
|
|
@@ -319,13 +321,13 @@ function multiTenantPlugin(options) {
|
|
|
319
321
|
type: "relationship",
|
|
320
322
|
to: tenantsSlug,
|
|
321
323
|
index: true,
|
|
322
|
-
position: "sidebar",
|
|
323
324
|
label: { en: "Tenant", th: "Tenant" },
|
|
324
325
|
// Set from the tenant the request works in; only users with access to all tenants choose.
|
|
325
326
|
access: { update: ({ user, context }) => superUser(user, context) },
|
|
326
327
|
admin: {
|
|
328
|
+
position: "sidebar",
|
|
327
329
|
// New documents start in the chosen tenant.
|
|
328
|
-
|
|
330
|
+
initialValue: ({ context }) => tenantOf(context).tenant ?? void 0,
|
|
329
331
|
// A column while every tenant is shown.
|
|
330
332
|
column: ({ user, context }) => superUser(user, context) && tenantOf(context).tenant === null,
|
|
331
333
|
allowCreate: false
|
|
@@ -348,7 +350,7 @@ function multiTenantPlugin(options) {
|
|
|
348
350
|
}
|
|
349
351
|
return data;
|
|
350
352
|
};
|
|
351
|
-
const checkTenant = (slug) => ({ data, operation,
|
|
353
|
+
const checkTenant = (slug) => ({ data, operation, previousDoc, context, user }) => {
|
|
352
354
|
const value = data[TENANT_FIELD] ?? null;
|
|
353
355
|
const { tenant } = tenantOf(context);
|
|
354
356
|
if (!user && tenant === null) return data;
|
|
@@ -358,7 +360,7 @@ function multiTenantPlugin(options) {
|
|
|
358
360
|
if (value === null) fail("is required: choose a tenant");
|
|
359
361
|
if (superUser(user, context)) return data;
|
|
360
362
|
if (!sameId(value, tenant)) fail("must be the tenant you work in");
|
|
361
|
-
const before =
|
|
363
|
+
const before = previousDoc?.[TENANT_FIELD] ?? null;
|
|
362
364
|
if (operation === "update" && before !== null && !sameId(before, value))
|
|
363
365
|
fail("can't move to another tenant");
|
|
364
366
|
return data;
|
|
@@ -451,9 +453,9 @@ function multiTenantPlugin(options) {
|
|
|
451
453
|
const tenantsCollection = {
|
|
452
454
|
slug: tenantsSlug,
|
|
453
455
|
labels: { singular: { en: "Tenant", th: "Tenant" }, plural: { en: "Tenants", th: "Tenant" } },
|
|
454
|
-
icon: "building",
|
|
455
456
|
useAsTitle: "name",
|
|
456
457
|
admin: {
|
|
458
|
+
icon: "building",
|
|
457
459
|
group: "settings.people",
|
|
458
460
|
// Deleting a tenant deletes its content: its name is typed, and what goes is shown.
|
|
459
461
|
confirmDelete: { typeTitle: true, impact: "/tenant-impact" }
|
|
@@ -562,9 +564,7 @@ function multiTenantPlugin(options) {
|
|
|
562
564
|
});
|
|
563
565
|
const previous = config.onRequest;
|
|
564
566
|
const onRequest = async (args) => {
|
|
565
|
-
const
|
|
566
|
-
const user = before?.user !== void 0 ? before.user : args.user;
|
|
567
|
-
const context = { ...before?.context };
|
|
567
|
+
const user = args.user;
|
|
568
568
|
const list = await tenantList(args.cms, tenantsSlug);
|
|
569
569
|
const asked = args.headers.get(header) ?? args.url?.searchParams.get("tenant") ?? readCookie(args.headers.get("cookie"), cookie) ?? null;
|
|
570
570
|
const byHost = tenantOfHost(
|
|
@@ -575,7 +575,7 @@ function multiTenantPlugin(options) {
|
|
|
575
575
|
const boundTenant = bound ? tenantOf(bound) : void 0;
|
|
576
576
|
if (user && user.member !== true && isSuper(user) && (!boundTenant || boundTenant.tenant === null)) {
|
|
577
577
|
const chosen2 = asked === ALL ? void 0 : findTenant(list, asked);
|
|
578
|
-
return { user, context: {
|
|
578
|
+
return { user, context: { tenant: chosen2?.id ?? null, allTenants: true } };
|
|
579
579
|
}
|
|
580
580
|
if (user && user.member !== true) {
|
|
581
581
|
const mine = membershipsOf(user);
|
|
@@ -583,14 +583,13 @@ function multiTenantPlugin(options) {
|
|
|
583
583
|
const membership = boundTenant && boundTenant.tenant !== null ? mine.find((m) => sameId(m.tenant, boundTenant.tenant)) ?? (isSuper(user) ? { tenant: boundTenant.tenant, role: user.role } : void 0) : member(findTenant(list, asked)) ?? member(byHost) ?? mine.find((m) => list.some((t) => sameId(t.id, m.tenant)));
|
|
584
584
|
return {
|
|
585
585
|
user: { ...user, role: membership?.role ?? user.role, scoped: true },
|
|
586
|
-
context: {
|
|
586
|
+
context: { tenant: membership?.tenant ?? null, allTenants: false }
|
|
587
587
|
};
|
|
588
588
|
}
|
|
589
589
|
const chosen = asked && asked !== ALL ? findTenant(list, asked) : byHost;
|
|
590
590
|
return {
|
|
591
591
|
user,
|
|
592
592
|
context: {
|
|
593
|
-
...context,
|
|
594
593
|
tenant: chosen?.id ?? null,
|
|
595
594
|
allTenants: !chosen && publicReads === "all"
|
|
596
595
|
}
|
|
@@ -707,19 +706,19 @@ of its own. Run it once after adding the plugin to a site that has content.
|
|
|
707
706
|
...config,
|
|
708
707
|
collections,
|
|
709
708
|
globals,
|
|
710
|
-
onRequest,
|
|
709
|
+
onRequest: [...typeof previous === "function" ? [previous] : previous ?? [], onRequest],
|
|
711
710
|
endpoints: [
|
|
712
711
|
...config.endpoints ?? [],
|
|
713
712
|
optionsEndpoint,
|
|
714
713
|
impactEndpoint,
|
|
715
714
|
...membersEndpoints({ tenantsSlug, superUser, isSuper })
|
|
716
715
|
],
|
|
717
|
-
|
|
716
|
+
cliCommands: [...config.cliCommands ?? [], assign],
|
|
718
717
|
// Audit log entries belong to their tenant: its admins see them.
|
|
719
718
|
...config.audit ? {
|
|
720
719
|
audit: {
|
|
721
720
|
...config.audit === true ? {} : config.audit,
|
|
722
|
-
scope: (context) => {
|
|
721
|
+
scope: ({ context }) => {
|
|
723
722
|
const { tenant } = tenantOf(context);
|
|
724
723
|
return tenant === null ? null : String(tenant);
|
|
725
724
|
}
|
|
@@ -778,12 +777,12 @@ async function removeTenant(cms, id, collections) {
|
|
|
778
777
|
}
|
|
779
778
|
|
|
780
779
|
// src/index.ts
|
|
781
|
-
async function findTenantFor(cms, by
|
|
782
|
-
const list = await tenantList(cms,
|
|
780
|
+
async function findTenantFor(cms, by) {
|
|
781
|
+
const list = await tenantList(cms, by.collection ?? "tenants");
|
|
783
782
|
return (by.slug ? findTenant(list, by.slug) : void 0) ?? tenantOfHost(list, by.host) ?? null;
|
|
784
783
|
}
|
|
785
|
-
async function tenantContext(cms, by
|
|
786
|
-
const tenant = await findTenantFor(cms, by
|
|
784
|
+
async function tenantContext(cms, by) {
|
|
785
|
+
const tenant = await findTenantFor(cms, by);
|
|
787
786
|
return { tenant: tenant?.id ?? null, allTenants: false };
|
|
788
787
|
}
|
|
789
788
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@easy-cms/plugin-multi-tenant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.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.
|
|
20
|
+
"@easy-cms/core": "^0.60.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@easy-cms/core": "^0.
|
|
24
|
-
"@easy-cms/db-sqlite": "^0.
|
|
25
|
-
"@easy-cms/plugin-form-builder": "^0.
|
|
26
|
-
"@easy-cms/plugin-nested-docs": "^0.
|
|
27
|
-
"@easy-cms/plugin-redirects": "^0.
|
|
23
|
+
"@easy-cms/core": "^0.60.0",
|
|
24
|
+
"@easy-cms/db-sqlite": "^0.60.0",
|
|
25
|
+
"@easy-cms/plugin-form-builder": "^0.60.0",
|
|
26
|
+
"@easy-cms/plugin-nested-docs": "^0.60.0",
|
|
27
|
+
"@easy-cms/plugin-redirects": "^0.60.0"
|
|
28
28
|
},
|
|
29
29
|
"engines": {
|
|
30
30
|
"node": ">=22.12"
|