@easy-cms/core 0.43.0 → 0.45.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 +192 -10
- package/dist/index.js +446 -164
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/access.ts
|
|
2
|
+
var isSystemAdmin = (user) => user?.role === "admin" && !user.apiKey && user.scoped !== true;
|
|
2
3
|
var anyone = () => true;
|
|
3
4
|
var isLoggedIn = ({ user }) => user !== null;
|
|
4
5
|
var isAdmin = ({ user }) => user?.role === "admin";
|
|
@@ -41,8 +42,7 @@ function keyAllows(user, target, operation) {
|
|
|
41
42
|
const allowed2 = key2.permissions.globals?.[target.global];
|
|
42
43
|
return allowed2?.includes(operation) ?? false;
|
|
43
44
|
}
|
|
44
|
-
var
|
|
45
|
-
var ownKeys = ({ user }) => !user || user.apiKey ? false : isAdmin2(user) ? true : { user: { equals: user.id } };
|
|
45
|
+
var ownKeys = ({ user }) => !user || user.apiKey ? false : isSystemAdmin(user) ? true : { user: { equals: user.id } };
|
|
46
46
|
var system = { update: () => false };
|
|
47
47
|
var apiKeysCollection = {
|
|
48
48
|
slug: API_KEYS,
|
|
@@ -99,14 +99,17 @@ var apiKeysCollection = {
|
|
|
99
99
|
{ name: "keyHash", type: "text", hidden: true }
|
|
100
100
|
],
|
|
101
101
|
hooks: {
|
|
102
|
-
// Only real collections, globals and operations are kept.
|
|
102
|
+
// Only real collections, globals and operations are kept. A key keeps the context it was
|
|
103
|
+
// created in (`createApiKey`), whatever its permissions are changed to.
|
|
103
104
|
beforeChange: [
|
|
104
|
-
({ data, cms }) =>
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
({ data, cms, operation, originalDoc }) => {
|
|
106
|
+
if (data.permissions === void 0) return data;
|
|
107
|
+
const context = operation === "create" ? data.permissions.context : originalDoc?.permissions?.context;
|
|
108
|
+
const permissions = cleanPermissions(data.permissions, {
|
|
107
109
|
collections: cms.config.collections.map((c) => c.slug),
|
|
108
110
|
globals: cms.config.globals.map((g) => g.slug)
|
|
109
|
-
})
|
|
111
|
+
});
|
|
112
|
+
return { ...data, permissions: context ? { ...permissions, context } : permissions };
|
|
110
113
|
}
|
|
111
114
|
]
|
|
112
115
|
}
|
|
@@ -183,11 +186,11 @@ function withCreatedBy(config) {
|
|
|
183
186
|
}
|
|
184
187
|
var adminOrSelf = ({ user }) => {
|
|
185
188
|
if (!user) return false;
|
|
186
|
-
if (user
|
|
189
|
+
if (isSystemAdmin(user)) return true;
|
|
187
190
|
return { id: { equals: user.id } };
|
|
188
191
|
};
|
|
189
192
|
var adminOnly = {
|
|
190
|
-
update: ({ user }) => user
|
|
193
|
+
update: ({ user }) => isSystemAdmin(user)
|
|
191
194
|
};
|
|
192
195
|
function userFields(roles, rbac) {
|
|
193
196
|
const defaultRole = roles.includes("editor") ? "editor" : roles[roles.length - 1];
|
|
@@ -239,9 +242,9 @@ function withUsers(config) {
|
|
|
239
242
|
fields: [...userFields(roles, config.auth?.rbac === true), ...custom?.fields ?? []],
|
|
240
243
|
access: {
|
|
241
244
|
read: isLoggedIn,
|
|
242
|
-
create:
|
|
245
|
+
create: ({ user }) => isSystemAdmin(user),
|
|
243
246
|
update: adminOrSelf,
|
|
244
|
-
delete:
|
|
247
|
+
delete: ({ user }) => isSystemAdmin(user),
|
|
245
248
|
...custom?.access
|
|
246
249
|
}
|
|
247
250
|
};
|
|
@@ -421,13 +424,37 @@ function withMedia(config) {
|
|
|
421
424
|
]
|
|
422
425
|
}
|
|
423
426
|
};
|
|
424
|
-
const
|
|
427
|
+
const customFolders = config.collections?.find((c) => c.slug === MEDIA_FOLDERS);
|
|
428
|
+
const mediaFolders = customFolders ? {
|
|
429
|
+
...mediaFoldersCollection,
|
|
430
|
+
// A field named like a built-in one changes it (e.g. `key` unique per tenant).
|
|
431
|
+
fields: [
|
|
432
|
+
...mediaFoldersCollection.fields.map((field) => {
|
|
433
|
+
const own = customFolders.fields.find((f) => f.name === field.name);
|
|
434
|
+
return own ? { ...field, ...own } : field;
|
|
435
|
+
}),
|
|
436
|
+
...customFolders.fields.filter(
|
|
437
|
+
(f) => !mediaFoldersCollection.fields.some((b) => b.name === f.name)
|
|
438
|
+
)
|
|
439
|
+
],
|
|
440
|
+
access: { ...mediaFoldersCollection.access, ...customFolders.access },
|
|
441
|
+
hooks: {
|
|
442
|
+
...customFolders.hooks,
|
|
443
|
+
afterRead: [
|
|
444
|
+
...mediaFoldersCollection.hooks?.afterRead ?? [],
|
|
445
|
+
...customFolders.hooks?.afterRead ?? []
|
|
446
|
+
]
|
|
447
|
+
}
|
|
448
|
+
} : mediaFoldersCollection;
|
|
449
|
+
const others = (config.collections ?? []).filter(
|
|
450
|
+
(c) => c.slug !== MEDIA && c.slug !== MEDIA_FOLDERS
|
|
451
|
+
);
|
|
425
452
|
return {
|
|
426
453
|
...config,
|
|
427
454
|
collections: [
|
|
428
455
|
...others.slice(0, 1),
|
|
429
456
|
media,
|
|
430
|
-
...folders ? [
|
|
457
|
+
...folders ? [mediaFolders] : [],
|
|
431
458
|
...others.slice(1)
|
|
432
459
|
]
|
|
433
460
|
};
|
|
@@ -544,7 +571,9 @@ var auditLogsCollection = {
|
|
|
544
571
|
{ name: "userAgent", type: "text" },
|
|
545
572
|
{ name: "changes", type: "json" },
|
|
546
573
|
{ name: "detail", type: "json" },
|
|
547
|
-
{ name: "signature", type: "text" }
|
|
574
|
+
{ name: "signature", type: "text" },
|
|
575
|
+
// The part of the site, e.g. the tenant (`audit.scope`).
|
|
576
|
+
{ name: "scope", type: "text", index: true }
|
|
548
577
|
]
|
|
549
578
|
};
|
|
550
579
|
var emailDeliveriesCollection = {
|
|
@@ -617,7 +646,8 @@ var AuditLog = class {
|
|
|
617
646
|
return this.cms.config.audit || {
|
|
618
647
|
keep: DEFAULT_AUDIT_KEEP,
|
|
619
648
|
values: true,
|
|
620
|
-
failedLogins: DEFAULT_FAILED_LOGINS
|
|
649
|
+
failedLogins: DEFAULT_FAILED_LOGINS,
|
|
650
|
+
scope: null
|
|
621
651
|
};
|
|
622
652
|
}
|
|
623
653
|
/**
|
|
@@ -628,6 +658,7 @@ var AuditLog = class {
|
|
|
628
658
|
if (!this.enabled) return;
|
|
629
659
|
try {
|
|
630
660
|
const context = auditContext.getStore();
|
|
661
|
+
const scope = this.scopeOf(entry.context ?? context?.context);
|
|
631
662
|
const user = entry.user !== void 0 ? entry.user : context?.user ?? null;
|
|
632
663
|
const apiKey = user?.apiKey;
|
|
633
664
|
const row = {
|
|
@@ -642,7 +673,8 @@ var AuditLog = class {
|
|
|
642
673
|
userAgent: context?.userAgent ? context.userAgent.slice(0, 200) : null,
|
|
643
674
|
changes: entry.changes && entry.changes.length > 0 ? entry.changes : null,
|
|
644
675
|
detail: apiKey ? { ...entry.detail, apiKey: apiKey.name ?? "" } : entry.detail ?? null,
|
|
645
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
676
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
677
|
+
...scope ? { scope } : {}
|
|
646
678
|
};
|
|
647
679
|
row.signature = this.sign(row);
|
|
648
680
|
row.updatedAt = row.createdAt;
|
|
@@ -658,7 +690,7 @@ var AuditLog = class {
|
|
|
658
690
|
* A change to a document or global: what changed between `before` and `after`. Internal
|
|
659
691
|
* collections (sessions, versions…) are not logged.
|
|
660
692
|
*/
|
|
661
|
-
async content(config, global2, action, after, before, user) {
|
|
693
|
+
async content(config, global2, action, after, before, user, context) {
|
|
662
694
|
if (!this.enabled) return;
|
|
663
695
|
if (!global2 && INTERNAL_COLLECTIONS.has(config.slug)) return;
|
|
664
696
|
const doc = after ?? before;
|
|
@@ -674,7 +706,8 @@ var AuditLog = class {
|
|
|
674
706
|
title,
|
|
675
707
|
// A trusted call without a user keeps the request's (or is the system).
|
|
676
708
|
...user ? { user } : {},
|
|
677
|
-
changes: action === "delete" ? null : this.diff(config.fields, before ?? {}, after ?? {})
|
|
709
|
+
changes: action === "delete" ? null : this.diff(config.fields, before ?? {}, after ?? {}),
|
|
710
|
+
...context && Object.keys(context).length > 0 ? { context } : {}
|
|
678
711
|
});
|
|
679
712
|
}
|
|
680
713
|
/** Fields whose stored value changed. Hidden ones never (the password only as "changed"). */
|
|
@@ -819,8 +852,15 @@ var AuditLog = class {
|
|
|
819
852
|
return out;
|
|
820
853
|
}
|
|
821
854
|
// -------------------------------------------------------------------------
|
|
855
|
+
/** The part of the site a context is in (`scope`), or `null`. */
|
|
856
|
+
scopeOf(context) {
|
|
857
|
+
const scope = this.settings.scope;
|
|
858
|
+
return (scope && context ? scope(context) : null) ?? null;
|
|
859
|
+
}
|
|
822
860
|
sign(row) {
|
|
823
|
-
const
|
|
861
|
+
const values = SIGNED.map((key2) => row[key2] ?? null);
|
|
862
|
+
if (typeof row.scope === "string" && row.scope) values.push(row.scope);
|
|
863
|
+
const payload = stable(values);
|
|
824
864
|
return createHmac("sha256", this.cms.config.secret).update(`audit:${payload}`).digest("base64url");
|
|
825
865
|
}
|
|
826
866
|
async lastVerification() {
|
|
@@ -848,6 +888,7 @@ var AuditLog = class {
|
|
|
848
888
|
if (filter.actor) parts.push({ actorEmail: { like: filter.actor } });
|
|
849
889
|
if (filter.from) parts.push({ createdAt: { gte: filter.from } });
|
|
850
890
|
if (filter.to) parts.push({ createdAt: { lte: filter.to } });
|
|
891
|
+
if (filter.scope) parts.push({ scope: { equals: filter.scope } });
|
|
851
892
|
return parts.length === 0 ? void 0 : parts.length === 1 ? parts[0] : { and: parts };
|
|
852
893
|
}
|
|
853
894
|
};
|
|
@@ -1119,6 +1160,7 @@ function sizeKey(filename, size) {
|
|
|
1119
1160
|
var PRIVATE_KEY = /^[\p{L}\p{M}\p{N}-]+\.private(-[\p{L}\p{N}_-]+)?\.[a-z0-9]+$/u;
|
|
1120
1161
|
|
|
1121
1162
|
// src/fields.ts
|
|
1163
|
+
var uniqueWithinOf = (field) => field.uniqueWithin === void 0 ? [] : [field.uniqueWithin].flat();
|
|
1122
1164
|
var isHasMany = (field) => (field.type === "select" || field.type === "relationship" || field.type === "upload") && field.hasMany === true;
|
|
1123
1165
|
function mimeAllowedBy(mimeType, patterns) {
|
|
1124
1166
|
const type = mimeType.toLowerCase();
|
|
@@ -2872,7 +2914,7 @@ async function normalizeValue(field, raw, path, options, fail, errors, reference
|
|
|
2872
2914
|
field: path,
|
|
2873
2915
|
collection: target,
|
|
2874
2916
|
id,
|
|
2875
|
-
...field.
|
|
2917
|
+
...field.filterOptions ? { filterOptions: field.filterOptions } : {},
|
|
2876
2918
|
...field.type === "upload" && field.mimeTypes ? { mimeTypes: field.mimeTypes } : {},
|
|
2877
2919
|
...field.type === "upload" && field.folder && field.folderOnly ? { folderOnly: field.folder } : {}
|
|
2878
2920
|
});
|
|
@@ -3358,7 +3400,8 @@ var MediaFolders = class {
|
|
|
3358
3400
|
name: String(doc.name ?? ""),
|
|
3359
3401
|
key: typeof doc.key === "string" && doc.key ? doc.key : null,
|
|
3360
3402
|
private: doc.private === true,
|
|
3361
|
-
permissions: permissions && typeof permissions === "object" && !Array.isArray(permissions) ? permissions : null
|
|
3403
|
+
permissions: permissions && typeof permissions === "object" && !Array.isArray(permissions) ? permissions : null,
|
|
3404
|
+
doc
|
|
3362
3405
|
});
|
|
3363
3406
|
}
|
|
3364
3407
|
this.cache = { at: Date.now(), rows };
|
|
@@ -3428,18 +3471,15 @@ var MediaFolders = class {
|
|
|
3428
3471
|
});
|
|
3429
3472
|
return this.within(rows, [id]).map((f) => ({ id: f, private: this.privateIn(rows, f) }));
|
|
3430
3473
|
}
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
* there is none yet.
|
|
3434
|
-
*/
|
|
3435
|
-
async keyed(key2) {
|
|
3436
|
-
const find = async () => [...(await this.rows()).values()].find((r) => r.key === key2);
|
|
3474
|
+
async keyed(key2, scope = {}, create = true) {
|
|
3475
|
+
const find = async () => [...(await this.rows()).values()].find((r) => r.key === key2 && inScope(r.doc, scope));
|
|
3437
3476
|
let found = await find();
|
|
3438
3477
|
if (!found) {
|
|
3439
3478
|
this.invalidate();
|
|
3440
3479
|
found = await find();
|
|
3441
3480
|
}
|
|
3442
3481
|
if (found) return found.id;
|
|
3482
|
+
if (!create) return null;
|
|
3443
3483
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3444
3484
|
try {
|
|
3445
3485
|
const doc = await this.db.create({
|
|
@@ -3450,6 +3490,7 @@ var MediaFolders = class {
|
|
|
3450
3490
|
parent: null,
|
|
3451
3491
|
permissions: null,
|
|
3452
3492
|
private: false,
|
|
3493
|
+
...scope,
|
|
3453
3494
|
createdAt: now,
|
|
3454
3495
|
updatedAt: now
|
|
3455
3496
|
}
|
|
@@ -3464,10 +3505,10 @@ var MediaFolders = class {
|
|
|
3464
3505
|
}
|
|
3465
3506
|
}
|
|
3466
3507
|
/** Whether a file in `folder` is in the folder with this key, or below it. */
|
|
3467
|
-
async inKeyed(key2, folder) {
|
|
3508
|
+
async inKeyed(key2, folder, scope = {}) {
|
|
3468
3509
|
if (folder === null) return false;
|
|
3469
3510
|
const rows = await this.rows();
|
|
3470
|
-
const root = [...rows.values()].find((r) => r.key === key2);
|
|
3511
|
+
const root = [...rows.values()].find((r) => r.key === key2 && inScope(r.doc, scope));
|
|
3471
3512
|
return !!root && this.within(rows, [root.id]).some((id) => String(id) === String(folder));
|
|
3472
3513
|
}
|
|
3473
3514
|
/** The user's level in a folder (`null`: the top level). */
|
|
@@ -3592,6 +3633,11 @@ var MediaFolders = class {
|
|
|
3592
3633
|
return this.rows();
|
|
3593
3634
|
}
|
|
3594
3635
|
};
|
|
3636
|
+
function inScope(row, scope) {
|
|
3637
|
+
return Object.entries(scope).every(
|
|
3638
|
+
([name, value]) => value === null || value === void 0 ? row[name] === null || row[name] === void 0 : String(row[name]) === String(value)
|
|
3639
|
+
);
|
|
3640
|
+
}
|
|
3595
3641
|
|
|
3596
3642
|
// src/populate.ts
|
|
3597
3643
|
var DEFAULT_DEPTH = 1;
|
|
@@ -4079,8 +4125,10 @@ var Roles = class {
|
|
|
4079
4125
|
* it; without roles, nobody else (`status`, `deliveries`) or everybody (plugin pages).
|
|
4080
4126
|
*/
|
|
4081
4127
|
async canView(user, id) {
|
|
4082
|
-
if (user
|
|
4128
|
+
if (isSystemAdmin(user)) return true;
|
|
4083
4129
|
if (user.apiKey) return false;
|
|
4130
|
+
if (id === "audit" && user.role === "admin" && user.scoped === true && this.config.audit !== false && typeof this.config.audit.scope === "function")
|
|
4131
|
+
return true;
|
|
4084
4132
|
if (!this.enabled) return id.startsWith("page:") || id.startsWith("widget:");
|
|
4085
4133
|
return (await this.permissionsOf(user.role)).admin?.includes(id) ?? false;
|
|
4086
4134
|
}
|
|
@@ -4866,6 +4914,14 @@ function validateAdmin(config, add) {
|
|
|
4866
4914
|
"e.g. siteUrl: 'https://example.com'"
|
|
4867
4915
|
);
|
|
4868
4916
|
}
|
|
4917
|
+
const switcher = admin.switcher;
|
|
4918
|
+
if (switcher !== void 0 && switcher !== null) {
|
|
4919
|
+
const { cookie: cookie2, options } = switcher;
|
|
4920
|
+
if (typeof cookie2 !== "string" || !/^[A-Za-z0-9_-]+$/.test(cookie2))
|
|
4921
|
+
add("admin.switcher.cookie", "must be a cookie name (letters, digits, - and _)");
|
|
4922
|
+
if (typeof options !== "string" || !options.startsWith("/"))
|
|
4923
|
+
add("admin.switcher.options", 'must be a path under the API starting with "/"');
|
|
4924
|
+
}
|
|
4869
4925
|
const modules = admin.modules;
|
|
4870
4926
|
if (modules !== void 0) {
|
|
4871
4927
|
if (!Array.isArray(modules)) {
|
|
@@ -5501,6 +5557,22 @@ function validateField(field, path, siblings, collectionSlugs, add) {
|
|
|
5501
5557
|
if (cell !== void 0) validateComponent(cell, `${path}.admin.cell`, add);
|
|
5502
5558
|
}
|
|
5503
5559
|
}
|
|
5560
|
+
if (field.uniqueWithin !== void 0) {
|
|
5561
|
+
if (!field.unique && field.type !== "slug")
|
|
5562
|
+
add(`${path}.uniqueWithin`, "needs `unique: true` (or a slug field)");
|
|
5563
|
+
else
|
|
5564
|
+
for (const name of [field.uniqueWithin].flat()) {
|
|
5565
|
+
const scope = siblings.get(name);
|
|
5566
|
+
if (!scope || scope === field) {
|
|
5567
|
+
add(`${path}.uniqueWithin`, `no sibling field named "${name}"`);
|
|
5568
|
+
} else if (!["relationship", "select", "text", "number"].includes(scope.type) || "hasMany" in scope && scope.hasMany || scope.localized) {
|
|
5569
|
+
add(
|
|
5570
|
+
`${path}.uniqueWithin`,
|
|
5571
|
+
`"${name}" must be a single, unlocalized relationship, select, text or number field`
|
|
5572
|
+
);
|
|
5573
|
+
}
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5504
5576
|
switch (field.type) {
|
|
5505
5577
|
case "text":
|
|
5506
5578
|
case "textarea":
|
|
@@ -5521,17 +5593,6 @@ function validateField(field, path, siblings, collectionSlugs, add) {
|
|
|
5521
5593
|
add(`${path}.from`, `"${field.from}" must be a text field (got ${source.type})`);
|
|
5522
5594
|
}
|
|
5523
5595
|
}
|
|
5524
|
-
if (field.uniqueWithin !== void 0) {
|
|
5525
|
-
const scope = siblings.get(field.uniqueWithin);
|
|
5526
|
-
if (!scope || scope === field) {
|
|
5527
|
-
add(`${path}.uniqueWithin`, `no sibling field named "${field.uniqueWithin}"`);
|
|
5528
|
-
} else if (!["relationship", "select", "text", "number"].includes(scope.type) || "hasMany" in scope && scope.hasMany || scope.localized) {
|
|
5529
|
-
add(
|
|
5530
|
-
`${path}.uniqueWithin`,
|
|
5531
|
-
`"${field.uniqueWithin}" must be a single, unlocalized relationship, select, text or number field`
|
|
5532
|
-
);
|
|
5533
|
-
}
|
|
5534
|
-
}
|
|
5535
5596
|
break;
|
|
5536
5597
|
case "relationship":
|
|
5537
5598
|
if (typeof field.to !== "string" || !collectionSlugs.has(field.to)) {
|
|
@@ -5822,7 +5883,8 @@ async function resolveConfig(input) {
|
|
|
5822
5883
|
menu: config.admin?.menu ?? [],
|
|
5823
5884
|
modules: config.admin?.modules ?? [],
|
|
5824
5885
|
pages: config.admin?.pages ?? [],
|
|
5825
|
-
dashboard: config.admin?.dashboard ?? []
|
|
5886
|
+
dashboard: config.admin?.dashboard ?? [],
|
|
5887
|
+
switcher: config.admin?.switcher ?? null
|
|
5826
5888
|
},
|
|
5827
5889
|
upload: {
|
|
5828
5890
|
dir: config.upload?.dir ?? "uploads",
|
|
@@ -5885,7 +5947,8 @@ function resolveAudit(audit) {
|
|
|
5885
5947
|
return {
|
|
5886
5948
|
keep: audit.keep ?? DEFAULT_AUDIT_KEEP,
|
|
5887
5949
|
values: audit.values !== false,
|
|
5888
|
-
failedLogins: audit.failedLogins ?? DEFAULT_FAILED_LOGINS
|
|
5950
|
+
failedLogins: audit.failedLogins ?? DEFAULT_FAILED_LOGINS,
|
|
5951
|
+
scope: audit.scope ?? null
|
|
5889
5952
|
};
|
|
5890
5953
|
}
|
|
5891
5954
|
|
|
@@ -5969,6 +6032,29 @@ var EasyCMS = class {
|
|
|
5969
6032
|
);
|
|
5970
6033
|
this.auth = new Auth(this);
|
|
5971
6034
|
}
|
|
6035
|
+
/**
|
|
6036
|
+
* The user and context of a request, as the REST API works them out: its session cookie or
|
|
6037
|
+
* Bearer token, then `onRequest`. For framework routes and server code that act for a visitor;
|
|
6038
|
+
* pass both to the Local API with `overrideAccess: false`.
|
|
6039
|
+
*/
|
|
6040
|
+
async forRequest(request) {
|
|
6041
|
+
const headers = request instanceof Headers ? request : request.headers;
|
|
6042
|
+
const url = request instanceof Headers ? void 0 : new URL(request.url);
|
|
6043
|
+
return this.applyOnRequest(headers, url, await this.auth.userFromHeaders(headers));
|
|
6044
|
+
}
|
|
6045
|
+
/** @internal `onRequest` of the config, for a request whose user is known. */
|
|
6046
|
+
async applyOnRequest(headers, url, user) {
|
|
6047
|
+
const result = await this.config.onRequest?.({
|
|
6048
|
+
headers,
|
|
6049
|
+
url,
|
|
6050
|
+
user,
|
|
6051
|
+
cms: this
|
|
6052
|
+
});
|
|
6053
|
+
return {
|
|
6054
|
+
user: result?.user !== void 0 ? result.user : user,
|
|
6055
|
+
context: result?.context ?? NO_CONTEXT
|
|
6056
|
+
};
|
|
6057
|
+
}
|
|
5972
6058
|
async find(collection2, options = {}) {
|
|
5973
6059
|
const config = this.collection(collection2);
|
|
5974
6060
|
const guard = guardOf(options);
|
|
@@ -6222,6 +6308,7 @@ var EasyCMS = class {
|
|
|
6222
6308
|
await this.checkGrant(guard, { collection: MEDIA }, "create");
|
|
6223
6309
|
const allowed2 = await evaluateAccess(this.collection(MEDIA).access?.create, {
|
|
6224
6310
|
user: guard.user,
|
|
6311
|
+
context: guard.context,
|
|
6225
6312
|
data
|
|
6226
6313
|
});
|
|
6227
6314
|
if (allowed2 !== true) throw deny(guard.user);
|
|
@@ -6259,7 +6346,11 @@ var EasyCMS = class {
|
|
|
6259
6346
|
const fromURL = this.config.upload.fromURL;
|
|
6260
6347
|
if (guard.enforce) {
|
|
6261
6348
|
await this.checkGrant(guard, { collection: MEDIA }, "create");
|
|
6262
|
-
const allowed2 = await evaluateAccess(config.access?.create, {
|
|
6349
|
+
const allowed2 = await evaluateAccess(config.access?.create, {
|
|
6350
|
+
user: guard.user,
|
|
6351
|
+
context: guard.context,
|
|
6352
|
+
data
|
|
6353
|
+
});
|
|
6263
6354
|
if (allowed2 !== true) throw deny(guard.user);
|
|
6264
6355
|
if (!fromURL)
|
|
6265
6356
|
throw new ValidationError(MEDIA, [
|
|
@@ -6291,7 +6382,10 @@ var EasyCMS = class {
|
|
|
6291
6382
|
const config = this.collection(API_KEYS);
|
|
6292
6383
|
const guard = guardOf(options);
|
|
6293
6384
|
if (guard.enforce) {
|
|
6294
|
-
const allowed2 = await evaluateAccess(config.access?.create, {
|
|
6385
|
+
const allowed2 = await evaluateAccess(config.access?.create, {
|
|
6386
|
+
user: guard.user,
|
|
6387
|
+
context: guard.context
|
|
6388
|
+
});
|
|
6295
6389
|
if (allowed2 !== true) throw deny(guard.user);
|
|
6296
6390
|
}
|
|
6297
6391
|
const owner = guard.enforce ? guard.user?.id : data.user;
|
|
@@ -6303,7 +6397,12 @@ var EasyCMS = class {
|
|
|
6303
6397
|
config,
|
|
6304
6398
|
{
|
|
6305
6399
|
name: data.name,
|
|
6306
|
-
|
|
6400
|
+
// The context it is created in (e.g. a tenant) stays with the key. Requests can't name
|
|
6401
|
+
// one; trusted calls may.
|
|
6402
|
+
permissions: {
|
|
6403
|
+
...withoutContext(data.permissions),
|
|
6404
|
+
...guard.enforce ? Object.keys(guard.context).length > 0 ? { context: guard.context } : {} : data.permissions?.context ? { context: data.permissions.context } : {}
|
|
6405
|
+
},
|
|
6307
6406
|
expiresAt: expiresAt ?? null,
|
|
6308
6407
|
prefix,
|
|
6309
6408
|
user: owner
|
|
@@ -6564,7 +6663,11 @@ var EasyCMS = class {
|
|
|
6564
6663
|
if (id === null) {
|
|
6565
6664
|
if (guard.enforce) {
|
|
6566
6665
|
await this.checkGrant(guard, { collection: collection2 }, "create");
|
|
6567
|
-
const allowed2 = await evaluateAccess(config.access?.create, {
|
|
6666
|
+
const allowed2 = await evaluateAccess(config.access?.create, {
|
|
6667
|
+
user: guard.user,
|
|
6668
|
+
context: guard.context,
|
|
6669
|
+
data: raw
|
|
6670
|
+
});
|
|
6568
6671
|
if (allowed2 !== true) throw deny(guard.user);
|
|
6569
6672
|
}
|
|
6570
6673
|
} else {
|
|
@@ -6620,8 +6723,9 @@ var EasyCMS = class {
|
|
|
6620
6723
|
raw,
|
|
6621
6724
|
await this.fieldChecker("update", guard, void 0, raw)
|
|
6622
6725
|
);
|
|
6623
|
-
const
|
|
6624
|
-
const
|
|
6726
|
+
const key2 = this.globalKey(config, guard);
|
|
6727
|
+
const saved = (key2 ? await this.db.findGlobal({ slug: key2 }) : null) ?? {};
|
|
6728
|
+
const current = await this.globalDraft(config, saved, key2);
|
|
6625
6729
|
const merged = generateSlugs(
|
|
6626
6730
|
config.fields,
|
|
6627
6731
|
applyDefaults(
|
|
@@ -6719,7 +6823,7 @@ var EasyCMS = class {
|
|
|
6719
6823
|
"update",
|
|
6720
6824
|
parsed,
|
|
6721
6825
|
options,
|
|
6722
|
-
guard
|
|
6826
|
+
guard
|
|
6723
6827
|
);
|
|
6724
6828
|
prepared = await this.transform(
|
|
6725
6829
|
config.hooks?.beforeChange,
|
|
@@ -6853,8 +6957,12 @@ var EasyCMS = class {
|
|
|
6853
6957
|
const config = this.global(slug2);
|
|
6854
6958
|
const guard = guardOf(options);
|
|
6855
6959
|
await this.checkGlobalAccess(config, "read", guard);
|
|
6856
|
-
|
|
6857
|
-
|
|
6960
|
+
return this.readGlobal(config, this.globalKey(config, guard), guard, options);
|
|
6961
|
+
}
|
|
6962
|
+
/** A global as stored under `key` (see `globalKey`); empty when there is no key. */
|
|
6963
|
+
async readGlobal(config, key2, guard, options) {
|
|
6964
|
+
const saved = key2 ? await this.db.findGlobal({ slug: key2 }) : null;
|
|
6965
|
+
const stored = saved && options.draft ? await this.globalDraft(config, saved, key2) : saved;
|
|
6858
6966
|
const data = fillMissing(
|
|
6859
6967
|
config.fields,
|
|
6860
6968
|
applyDefaults(config.fields, stored ?? {}, this.config.localization)
|
|
@@ -6867,6 +6975,27 @@ var EasyCMS = class {
|
|
|
6867
6975
|
const { id: _id, ...doc } = out;
|
|
6868
6976
|
return doc;
|
|
6869
6977
|
}
|
|
6978
|
+
/**
|
|
6979
|
+
* Where a global is kept: its slug, or `<slug>@<scope>` for globals with `scope` (e.g. one per
|
|
6980
|
+
* tenant). `null` when the call has no scope: reads give the global empty. `scope` is given by
|
|
6981
|
+
* scheduled jobs, which have no request.
|
|
6982
|
+
*/
|
|
6983
|
+
globalKey(config, guard, scope) {
|
|
6984
|
+
if (scope !== void 0) return `${config.slug}@${scope}`;
|
|
6985
|
+
if (!config.scope) return config.slug;
|
|
6986
|
+
const own = config.scope({ context: guard.context, user: guard.user });
|
|
6987
|
+
if (own === void 0) return config.slug;
|
|
6988
|
+
return own === null ? null : `${config.slug}@${own}`;
|
|
6989
|
+
}
|
|
6990
|
+
/** `globalKey` for changes, which need a scope. */
|
|
6991
|
+
writeKey(config, guard, scope) {
|
|
6992
|
+
const key2 = this.globalKey(config, guard, scope);
|
|
6993
|
+
if (key2 === null)
|
|
6994
|
+
throw new QueryError(
|
|
6995
|
+
`"${config.slug}" has a value per scope (e.g. per tenant): choose one to change it`
|
|
6996
|
+
);
|
|
6997
|
+
return key2;
|
|
6998
|
+
}
|
|
6870
6999
|
async updateGlobal(slug2, data, options = {}) {
|
|
6871
7000
|
return this.saveGlobal(slug2, asObject(data, slug2), options);
|
|
6872
7001
|
}
|
|
@@ -6880,20 +7009,21 @@ var EasyCMS = class {
|
|
|
6880
7009
|
const config = this.global(slug2);
|
|
6881
7010
|
const guard = guardOf(options);
|
|
6882
7011
|
await this.checkGlobalAccess(config, "update", guard);
|
|
6883
|
-
const
|
|
6884
|
-
|
|
6885
|
-
|
|
7012
|
+
const key2 = this.writeKey(config, guard);
|
|
7013
|
+
const saved = await this.db.findGlobal({ slug: key2 });
|
|
7014
|
+
if (saved && await this.globalDraft(config, saved, key2) !== saved) {
|
|
7015
|
+
await this.saveVersion(config, globalParent(key2), 0, saved, guard);
|
|
6886
7016
|
}
|
|
6887
7017
|
return this.findGlobal(slug2, { ...options, draft: true });
|
|
6888
7018
|
}
|
|
6889
7019
|
/** Saved versions of a global, newest first. Needs update access. */
|
|
6890
7020
|
async findGlobalVersions(slug2, options = {}) {
|
|
6891
|
-
await this.globalVersionTarget(slug2, options);
|
|
6892
|
-
return this.versions.list(globalParent(
|
|
7021
|
+
const { key: key2 } = await this.globalVersionTarget(slug2, options);
|
|
7022
|
+
return this.versions.list(globalParent(key2), 0, options.limit ?? 20, options.page ?? 1);
|
|
6893
7023
|
}
|
|
6894
7024
|
async findGlobalVersion(slug2, versionId, options = {}) {
|
|
6895
|
-
const config = await this.globalVersionTarget(slug2, options);
|
|
6896
|
-
const version = await this.versions.get(globalParent(
|
|
7025
|
+
const { config, key: key2 } = await this.globalVersionTarget(slug2, options);
|
|
7026
|
+
const version = await this.versions.get(globalParent(key2), 0, versionId);
|
|
6897
7027
|
if (!version) return null;
|
|
6898
7028
|
const data = fillMissing(
|
|
6899
7029
|
config.fields,
|
|
@@ -6908,8 +7038,8 @@ var EasyCMS = class {
|
|
|
6908
7038
|
}
|
|
6909
7039
|
/** Makes an old version of a global current (as a draft when the global has drafts). */
|
|
6910
7040
|
async restoreGlobalVersion(slug2, versionId, options = {}) {
|
|
6911
|
-
const config = await this.globalVersionTarget(slug2, options);
|
|
6912
|
-
const version = await this.versions.get(globalParent(
|
|
7041
|
+
const { config, key: key2 } = await this.globalVersionTarget(slug2, options);
|
|
7042
|
+
const version = await this.versions.get(globalParent(key2), 0, versionId);
|
|
6913
7043
|
if (!version) throw new NotFoundError(`${slug2} version`, versionId);
|
|
6914
7044
|
const { status: _status, ...data } = version.data;
|
|
6915
7045
|
return this.saveGlobal(
|
|
@@ -6922,20 +7052,22 @@ var EasyCMS = class {
|
|
|
6922
7052
|
async globalVersionTarget(slug2, options) {
|
|
6923
7053
|
const config = this.global(slug2);
|
|
6924
7054
|
if (!versionLimit(config)) throw new QueryError(`"${slug2}" has no versions`);
|
|
6925
|
-
|
|
6926
|
-
|
|
7055
|
+
const guard = guardOf(options);
|
|
7056
|
+
await this.checkGlobalAccess(config, "update", guard);
|
|
7057
|
+
return { config, key: this.writeKey(config, guard) };
|
|
6927
7058
|
}
|
|
6928
7059
|
/** The global's pending draft when it has one (see `withDrafts`), otherwise `saved`. */
|
|
6929
|
-
async globalDraft(config, saved) {
|
|
6930
|
-
if (!this.separateDrafts(config) || saved.status !== "published") return saved;
|
|
6931
|
-
const version = (await this.versions.latest(globalParent(
|
|
7060
|
+
async globalDraft(config, saved, key2) {
|
|
7061
|
+
if (!key2 || !this.separateDrafts(config) || saved.status !== "published") return saved;
|
|
7062
|
+
const version = (await this.versions.latest(globalParent(key2), [0])).get("0");
|
|
6932
7063
|
if (version?.status !== "draft") return saved;
|
|
6933
7064
|
return { ...version.data, updatedAt: version.createdAt, status: "draft" };
|
|
6934
7065
|
}
|
|
6935
|
-
async saveGlobal(slug2, raw, options, mode = "save") {
|
|
7066
|
+
async saveGlobal(slug2, raw, options, mode = "save", scope) {
|
|
6936
7067
|
const config = this.global(slug2);
|
|
6937
7068
|
const guard = guardOf(options);
|
|
6938
7069
|
await this.checkGlobalAccess(config, "update", guard);
|
|
7070
|
+
const key2 = this.writeKey(config, guard, scope);
|
|
6939
7071
|
if (config.drafts && (mode === "unpublish" || raw.status === "published"))
|
|
6940
7072
|
await this.checkGrant(guard, { global: slug2 }, "publish");
|
|
6941
7073
|
const input = await filterInput(
|
|
@@ -6943,8 +7075,8 @@ var EasyCMS = class {
|
|
|
6943
7075
|
raw,
|
|
6944
7076
|
await this.fieldChecker("update", guard, void 0, raw)
|
|
6945
7077
|
);
|
|
6946
|
-
const existing = await this.db.findGlobal({ slug:
|
|
6947
|
-
const current = await this.globalDraft(config, existing);
|
|
7078
|
+
const existing = await this.db.findGlobal({ slug: key2 }) ?? {};
|
|
7079
|
+
const current = await this.globalDraft(config, existing, key2);
|
|
6948
7080
|
const localized = mode === "restore" ? input : this.toMaps(config, input, current, options);
|
|
6949
7081
|
const merged = generateSlugs(
|
|
6950
7082
|
config.fields,
|
|
@@ -6957,7 +7089,7 @@ var EasyCMS = class {
|
|
|
6957
7089
|
);
|
|
6958
7090
|
if (config.drafts) merged.status = input.status ?? current.status ?? "draft";
|
|
6959
7091
|
const base = this.hookArgs(config, guard);
|
|
6960
|
-
let prepared = await this.prepare(config, merged, "update", void 0, options, guard
|
|
7092
|
+
let prepared = await this.prepare(config, merged, "update", void 0, options, guard);
|
|
6961
7093
|
prepared = await this.transform(
|
|
6962
7094
|
config.hooks?.beforeChange,
|
|
6963
7095
|
"data",
|
|
@@ -6967,7 +7099,7 @@ var EasyCMS = class {
|
|
|
6967
7099
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6968
7100
|
if (mode !== "unpublish" && this.separateDrafts(config) && existing.status === "published" && prepared.status === "draft") {
|
|
6969
7101
|
const draft = { ...prepared, updatedAt: now };
|
|
6970
|
-
await this.saveVersion(config, globalParent(
|
|
7102
|
+
await this.saveVersion(config, globalParent(key2), 0, draft, guard);
|
|
6971
7103
|
await this.notify(config.hooks?.afterChange, "afterChange", slug2, {
|
|
6972
7104
|
...base,
|
|
6973
7105
|
doc: draft,
|
|
@@ -6976,10 +7108,10 @@ var EasyCMS = class {
|
|
|
6976
7108
|
});
|
|
6977
7109
|
this.emit(config, "draft", draft);
|
|
6978
7110
|
await this.audited(config, "draft", draft, current, guard, mode);
|
|
6979
|
-
return this.
|
|
7111
|
+
return this.readGlobal(config, key2, guard, { ...options, draft: true });
|
|
6980
7112
|
}
|
|
6981
|
-
const doc = await this.db.updateGlobal({ slug:
|
|
6982
|
-
await this.saveVersion(config, globalParent(
|
|
7113
|
+
const doc = await this.db.updateGlobal({ slug: key2, data: { ...prepared, updatedAt: now } });
|
|
7114
|
+
await this.saveVersion(config, globalParent(key2), 0, doc, guard);
|
|
6983
7115
|
await this.notify(config.hooks?.afterChange, "afterChange", slug2, {
|
|
6984
7116
|
...base,
|
|
6985
7117
|
doc,
|
|
@@ -6988,17 +7120,17 @@ var EasyCMS = class {
|
|
|
6988
7120
|
});
|
|
6989
7121
|
this.emit(config, "update", doc, existing.status);
|
|
6990
7122
|
await this.audited(config, "update", doc, current, guard, mode);
|
|
6991
|
-
return this.
|
|
7123
|
+
return this.readGlobal(config, key2, guard, { ...options, draft: true });
|
|
6992
7124
|
}
|
|
6993
7125
|
/**
|
|
6994
7126
|
* What `user` may do with one document. Resolves `where`-style access
|
|
6995
7127
|
* against the document, so the admin UI can hide actions precisely.
|
|
6996
7128
|
*/
|
|
6997
|
-
async documentPermissions(collection2, id, user) {
|
|
7129
|
+
async documentPermissions(collection2, id, user, context = NO_CONTEXT) {
|
|
6998
7130
|
const config = this.collection(collection2);
|
|
6999
7131
|
const parsed = parseId(id);
|
|
7000
7132
|
if (parsed === void 0) return { update: false, delete: false };
|
|
7001
|
-
const guard = { enforce: true, user };
|
|
7133
|
+
const guard = { enforce: true, user, context };
|
|
7002
7134
|
const check2 = (operation) => this.checkDocumentAccess(config, operation, guard, parsed, void 0).then(
|
|
7003
7135
|
() => true,
|
|
7004
7136
|
(error) => {
|
|
@@ -7166,9 +7298,10 @@ var EasyCMS = class {
|
|
|
7166
7298
|
const action = row.action;
|
|
7167
7299
|
try {
|
|
7168
7300
|
if (parent.startsWith("global:")) {
|
|
7169
|
-
const slug2 = parent.slice("global:".length);
|
|
7170
|
-
if (action === "publish")
|
|
7171
|
-
|
|
7301
|
+
const { slug: slug2, scope } = splitGlobalKey(parent.slice("global:".length));
|
|
7302
|
+
if (action === "publish")
|
|
7303
|
+
await this.saveGlobal(slug2, { status: "published" }, {}, "save", scope);
|
|
7304
|
+
else await this.saveGlobal(slug2, { status: "draft" }, {}, "unpublish", scope);
|
|
7172
7305
|
} else if (action === "publish") {
|
|
7173
7306
|
await this.updateDocument(parent, row.doc, { status: "published" }, { depth: 0 });
|
|
7174
7307
|
} else {
|
|
@@ -7213,16 +7346,16 @@ var EasyCMS = class {
|
|
|
7213
7346
|
await this.removeJob(collection2, parsed, jobId);
|
|
7214
7347
|
}
|
|
7215
7348
|
async scheduleGlobal(slug2, job, options = {}) {
|
|
7216
|
-
await this.globalScheduleTarget(slug2, options);
|
|
7217
|
-
return this.addJob(`global:${
|
|
7349
|
+
const key2 = await this.globalScheduleTarget(slug2, options);
|
|
7350
|
+
return this.addJob(`global:${key2}`, 0, job, options);
|
|
7218
7351
|
}
|
|
7219
7352
|
async scheduledGlobal(slug2, options = {}) {
|
|
7220
|
-
await this.globalScheduleTarget(slug2, options);
|
|
7221
|
-
return this.pendingJobs(`global:${
|
|
7353
|
+
const key2 = await this.globalScheduleTarget(slug2, options);
|
|
7354
|
+
return this.pendingJobs(`global:${key2}`, 0);
|
|
7222
7355
|
}
|
|
7223
7356
|
async cancelGlobalSchedule(slug2, jobId, options = {}) {
|
|
7224
|
-
await this.globalScheduleTarget(slug2, options);
|
|
7225
|
-
await this.removeJob(`global:${
|
|
7357
|
+
const key2 = await this.globalScheduleTarget(slug2, options);
|
|
7358
|
+
await this.removeJob(`global:${key2}`, 0, jobId);
|
|
7226
7359
|
}
|
|
7227
7360
|
async scheduleTarget(collection2, id, options) {
|
|
7228
7361
|
const config = this.collection(collection2);
|
|
@@ -7237,8 +7370,10 @@ var EasyCMS = class {
|
|
|
7237
7370
|
async globalScheduleTarget(slug2, options) {
|
|
7238
7371
|
const config = this.global(slug2);
|
|
7239
7372
|
if (!config.schedule) throw new QueryError(`"${slug2}" has no schedule`);
|
|
7240
|
-
|
|
7241
|
-
await this.
|
|
7373
|
+
const guard = guardOf(options);
|
|
7374
|
+
await this.checkGlobalAccess(config, "update", guard);
|
|
7375
|
+
await this.checkGrant(guard, { global: slug2 }, "publish");
|
|
7376
|
+
return this.writeKey(config, guard);
|
|
7242
7377
|
}
|
|
7243
7378
|
async addJob(parent, doc, job, options) {
|
|
7244
7379
|
const runAt = new Date(job.at);
|
|
@@ -7295,9 +7430,16 @@ var EasyCMS = class {
|
|
|
7295
7430
|
if (!guard.enforce) return true;
|
|
7296
7431
|
const cached = allowed2.get(parent);
|
|
7297
7432
|
if (cached !== void 0) return cached;
|
|
7298
|
-
const
|
|
7299
|
-
|
|
7300
|
-
|
|
7433
|
+
const global2 = parent.startsWith("global:") ? this.config.globals.find(
|
|
7434
|
+
(g) => g.slug === splitGlobalKey(parent.slice("global:".length)).slug
|
|
7435
|
+
) : void 0;
|
|
7436
|
+
if (global2 && this.globalKey(global2, guard) !== parent.slice("global:".length)) {
|
|
7437
|
+
allowed2.set(parent, false);
|
|
7438
|
+
return false;
|
|
7439
|
+
}
|
|
7440
|
+
const config = parent.startsWith("global:") ? global2 : this.config.collections.find((c) => c.slug === parent);
|
|
7441
|
+
const access = config ? await evaluateAccess(config.access?.update, { user: guard.user, context: guard.context }) : false;
|
|
7442
|
+
const target = parent.startsWith("global:") ? { global: splitGlobalKey(parent.slice("global:".length)).slug } : { collection: parent };
|
|
7301
7443
|
const result = access !== false && await this.roles.allows(guard.user, target, "update") !== false;
|
|
7302
7444
|
allowed2.set(parent, result);
|
|
7303
7445
|
return result;
|
|
@@ -7306,7 +7448,7 @@ var EasyCMS = class {
|
|
|
7306
7448
|
for (const row of rows.docs) {
|
|
7307
7449
|
const parent = String(row.parent);
|
|
7308
7450
|
if (!await may2(parent)) continue;
|
|
7309
|
-
const target = parent.startsWith("global:") ? { global: parent.slice("global:".length) } : { collection: parent };
|
|
7451
|
+
const target = parent.startsWith("global:") ? { global: splitGlobalKey(parent.slice("global:".length)).slug } : { collection: parent };
|
|
7310
7452
|
jobs.push({ ...toJob(row), ...target, doc: row.doc });
|
|
7311
7453
|
if (jobs.length === limit) break;
|
|
7312
7454
|
}
|
|
@@ -7418,7 +7560,10 @@ var EasyCMS = class {
|
|
|
7418
7560
|
async readWhere(config, guard, where) {
|
|
7419
7561
|
if (!guard.enforce) return where;
|
|
7420
7562
|
const own = await this.checkGrant(guard, { collection: config.slug }, "read");
|
|
7421
|
-
const access = await evaluateAccess(config.access?.read, {
|
|
7563
|
+
const access = await evaluateAccess(config.access?.read, {
|
|
7564
|
+
user: guard.user,
|
|
7565
|
+
context: guard.context
|
|
7566
|
+
});
|
|
7422
7567
|
if (access === false) throw deny(guard.user);
|
|
7423
7568
|
await this.checkQueryFields(config, guard, where);
|
|
7424
7569
|
return andWhere(andWhere(where, access), own);
|
|
@@ -7428,6 +7573,7 @@ var EasyCMS = class {
|
|
|
7428
7573
|
const own = await this.checkGrant(guard, { collection: config.slug }, operation);
|
|
7429
7574
|
const allowed2 = await evaluateAccess(config.access?.[operation], {
|
|
7430
7575
|
user: guard.user,
|
|
7576
|
+
context: guard.context,
|
|
7431
7577
|
id,
|
|
7432
7578
|
...data ? { data } : {}
|
|
7433
7579
|
});
|
|
@@ -7445,7 +7591,10 @@ var EasyCMS = class {
|
|
|
7445
7591
|
async checkGlobalAccess(config, operation, guard) {
|
|
7446
7592
|
if (!guard.enforce) return;
|
|
7447
7593
|
await this.checkGrant(guard, { global: config.slug }, operation);
|
|
7448
|
-
const access = await evaluateAccess(config.access?.[operation], {
|
|
7594
|
+
const access = await evaluateAccess(config.access?.[operation], {
|
|
7595
|
+
user: guard.user,
|
|
7596
|
+
context: guard.context
|
|
7597
|
+
});
|
|
7449
7598
|
if (typeof access === "object")
|
|
7450
7599
|
throw new QueryError(`${operation} access of global "${config.slug}" must return a boolean`);
|
|
7451
7600
|
if (!access) throw deny(guard.user);
|
|
@@ -7456,6 +7605,7 @@ var EasyCMS = class {
|
|
|
7456
7605
|
kind,
|
|
7457
7606
|
{
|
|
7458
7607
|
user: guard.user,
|
|
7608
|
+
context: guard.context,
|
|
7459
7609
|
...id !== void 0 ? { id } : {},
|
|
7460
7610
|
...data ? { data } : {}
|
|
7461
7611
|
},
|
|
@@ -7536,7 +7686,11 @@ var EasyCMS = class {
|
|
|
7536
7686
|
await this.checkGrant(guard, { collection: collection2 }, "create");
|
|
7537
7687
|
if (config.drafts && raw.status === "published")
|
|
7538
7688
|
await this.checkGrant(guard, { collection: collection2 }, "publish");
|
|
7539
|
-
const allowed2 = await evaluateAccess(config.access?.create, {
|
|
7689
|
+
const allowed2 = await evaluateAccess(config.access?.create, {
|
|
7690
|
+
user: guard.user,
|
|
7691
|
+
context: guard.context,
|
|
7692
|
+
data: raw
|
|
7693
|
+
});
|
|
7540
7694
|
if (typeof allowed2 === "object")
|
|
7541
7695
|
throw new QueryError(`create access of "${collection2}" must return a boolean`);
|
|
7542
7696
|
if (!allowed2) throw deny(guard.user);
|
|
@@ -7568,7 +7722,7 @@ var EasyCMS = class {
|
|
|
7568
7722
|
"create",
|
|
7569
7723
|
void 0,
|
|
7570
7724
|
options,
|
|
7571
|
-
|
|
7725
|
+
hookGuard ?? guard
|
|
7572
7726
|
);
|
|
7573
7727
|
prepared = await this.transform(
|
|
7574
7728
|
config.hooks?.beforeChange,
|
|
@@ -7689,7 +7843,8 @@ var EasyCMS = class {
|
|
|
7689
7843
|
action,
|
|
7690
7844
|
event === "delete" ? void 0 : doc,
|
|
7691
7845
|
event === "create" ? void 0 : previous,
|
|
7692
|
-
guard.user
|
|
7846
|
+
guard.user,
|
|
7847
|
+
guard.context
|
|
7693
7848
|
);
|
|
7694
7849
|
}
|
|
7695
7850
|
/** Records a version after a save, when the collection or global keeps versions. */
|
|
@@ -7706,7 +7861,12 @@ var EasyCMS = class {
|
|
|
7706
7861
|
});
|
|
7707
7862
|
}
|
|
7708
7863
|
hookArgs(config, guard) {
|
|
7709
|
-
return {
|
|
7864
|
+
return {
|
|
7865
|
+
user: guard.user,
|
|
7866
|
+
cms: this,
|
|
7867
|
+
slug: config.slug,
|
|
7868
|
+
context: guard.context
|
|
7869
|
+
};
|
|
7710
7870
|
}
|
|
7711
7871
|
/**
|
|
7712
7872
|
* Runs hooks that may replace a value, passed as `data` (before hooks) or `doc` (afterRead).
|
|
@@ -7768,7 +7928,7 @@ var EasyCMS = class {
|
|
|
7768
7928
|
return result;
|
|
7769
7929
|
}
|
|
7770
7930
|
/** Validates, checks uniqueness and references. Returns clean data or throws `ValidationError`. */
|
|
7771
|
-
async prepare(config, data, operation, selfId, options = {},
|
|
7931
|
+
async prepare(config, data, operation, selfId, options = {}, guard = { user: null, context: NO_CONTEXT }) {
|
|
7772
7932
|
const isDraft = config.drafts === true && (data.status ?? "draft") === "draft";
|
|
7773
7933
|
const locale = this.localeOf(options);
|
|
7774
7934
|
const result = await validateFields(config.fields, data, {
|
|
@@ -7794,9 +7954,9 @@ var EasyCMS = class {
|
|
|
7794
7954
|
await this.makeSlugsUnique(config, clean2, selfId);
|
|
7795
7955
|
errors.push(...await this.checkUnique(config, clean2, selfId));
|
|
7796
7956
|
}
|
|
7797
|
-
errors.push(...await this.checkReferences(result.references));
|
|
7957
|
+
errors.push(...await this.checkReferences(result.references, guard));
|
|
7798
7958
|
if (errors.length === 0)
|
|
7799
|
-
errors.push(...await this.checkFilterOptions(result.references, selfId,
|
|
7959
|
+
errors.push(...await this.checkFilterOptions(result.references, selfId, guard));
|
|
7800
7960
|
if (errors.length > 0) throw new ValidationError(config.slug, errors);
|
|
7801
7961
|
return clean2;
|
|
7802
7962
|
}
|
|
@@ -7824,7 +7984,11 @@ var EasyCMS = class {
|
|
|
7824
7984
|
async makeSlugsUnique(config, data, selfId) {
|
|
7825
7985
|
for (const field of config.fields) {
|
|
7826
7986
|
if (field.type !== "slug") continue;
|
|
7827
|
-
const scope = field.uniqueWithin === void 0 ? void 0 : {
|
|
7987
|
+
const scope = field.uniqueWithin === void 0 ? void 0 : {
|
|
7988
|
+
and: uniqueWithinOf(field).map((name) => ({
|
|
7989
|
+
[name]: { equals: data[name] ?? null }
|
|
7990
|
+
}))
|
|
7991
|
+
};
|
|
7828
7992
|
const unique = async (path, base) => {
|
|
7829
7993
|
let candidate = base;
|
|
7830
7994
|
for (let n = 2; await this.isTaken(config.slug, path, candidate, selfId, scope); n++) {
|
|
@@ -7855,9 +8019,14 @@ var EasyCMS = class {
|
|
|
7855
8019
|
locale === this.config.localization?.defaultLocale ? field.name : `${field.name}.${locale}`,
|
|
7856
8020
|
v
|
|
7857
8021
|
]) : [[field.name, field.name, value]];
|
|
8022
|
+
const scope = field.uniqueWithin === void 0 ? void 0 : {
|
|
8023
|
+
and: uniqueWithinOf(field).map((name) => ({
|
|
8024
|
+
[name]: { equals: data[name] ?? null }
|
|
8025
|
+
}))
|
|
8026
|
+
};
|
|
7858
8027
|
for (const [path, errorField, v] of checks) {
|
|
7859
8028
|
if (v === null || v === void 0) continue;
|
|
7860
|
-
if (await this.isTaken(config.slug, path, v, selfId)) {
|
|
8029
|
+
if (await this.isTaken(config.slug, path, v, selfId, scope)) {
|
|
7861
8030
|
errors.push({ field: errorField, message: "must be unique" });
|
|
7862
8031
|
}
|
|
7863
8032
|
}
|
|
@@ -7874,15 +8043,57 @@ var EasyCMS = class {
|
|
|
7874
8043
|
};
|
|
7875
8044
|
return await this.db.count({ collection: collection2, where }) > 0;
|
|
7876
8045
|
}
|
|
8046
|
+
/**
|
|
8047
|
+
* What folder keys are unique within for a call (`uniqueWithin` of the `key` field of media
|
|
8048
|
+
* folders, e.g. the tenant): the value a folder made by this call would get from its hooks.
|
|
8049
|
+
*/
|
|
8050
|
+
async folderScope(guard) {
|
|
8051
|
+
return this.uniqueScope(MEDIA_FOLDERS, "key", guard);
|
|
8052
|
+
}
|
|
8053
|
+
/**
|
|
8054
|
+
* Where a unique field's values must differ for a call (`uniqueWithin`, e.g. the tenant): the
|
|
8055
|
+
* value a document created by this call would get there from its hooks, as `{ [field]: value }`;
|
|
8056
|
+
* `{}` when the field is unique everywhere. For finding documents by such a field, e.g. a page
|
|
8057
|
+
* by its path in the current tenant.
|
|
8058
|
+
*/
|
|
8059
|
+
async uniqueScope(collection2, field, options = {}) {
|
|
8060
|
+
const config = this.config.collections.find((c) => c.slug === collection2);
|
|
8061
|
+
const target = config?.fields.find((f) => f.name === field);
|
|
8062
|
+
const within = target ? uniqueWithinOf(target) : [];
|
|
8063
|
+
if (!config || within.length === 0) return {};
|
|
8064
|
+
const guard = {
|
|
8065
|
+
enforce: false,
|
|
8066
|
+
user: options.user ?? null,
|
|
8067
|
+
context: options.context ?? NO_CONTEXT
|
|
8068
|
+
};
|
|
8069
|
+
const data = await this.transform(
|
|
8070
|
+
config.hooks?.beforeValidate,
|
|
8071
|
+
"data",
|
|
8072
|
+
{ ...this.hookArgs(config, guard), operation: "create" },
|
|
8073
|
+
{}
|
|
8074
|
+
).catch(() => ({}));
|
|
8075
|
+
return Object.fromEntries(within.map((name) => [name, data[name] ?? null]));
|
|
8076
|
+
}
|
|
8077
|
+
/**
|
|
8078
|
+
* @internal The id of the media folder with this key (`folder` of upload fields), made when
|
|
8079
|
+
* needed; `null` when the call has no scope to make it in (e.g. all tenants) and none exists.
|
|
8080
|
+
*/
|
|
8081
|
+
async keyedFolder(key2, options = {}) {
|
|
8082
|
+
const scope = await this.folderScope(guardOf(options));
|
|
8083
|
+
const complete = Object.values(scope).every((v) => v !== null && v !== void 0);
|
|
8084
|
+
return this.folders.keyed(key2, scope, complete);
|
|
8085
|
+
}
|
|
7877
8086
|
/** Each reference is one its relationship's `filterOptions` allow. */
|
|
7878
|
-
async checkFilterOptions(references2, selfId,
|
|
8087
|
+
async checkFilterOptions(references2, selfId, guard) {
|
|
7879
8088
|
const errors = [];
|
|
7880
8089
|
const cms = this;
|
|
8090
|
+
const { user, context } = guard;
|
|
7881
8091
|
const allowed2 = /* @__PURE__ */ new Map();
|
|
7882
8092
|
for (const ref of references2) {
|
|
7883
8093
|
const filter = ref.filterOptions;
|
|
7884
8094
|
if (!filter) continue;
|
|
7885
|
-
if (!allowed2.has(filter))
|
|
8095
|
+
if (!allowed2.has(filter))
|
|
8096
|
+
allowed2.set(filter, await filter({ id: selfId, user, cms, context }));
|
|
7886
8097
|
const where = allowed2.get(filter);
|
|
7887
8098
|
if (where === true) continue;
|
|
7888
8099
|
const target = this.config.collections.find((c) => c.slug === ref.collection);
|
|
@@ -7897,7 +8108,7 @@ var EasyCMS = class {
|
|
|
7897
8108
|
}
|
|
7898
8109
|
return errors;
|
|
7899
8110
|
}
|
|
7900
|
-
async checkReferences(references2) {
|
|
8111
|
+
async checkReferences(references2, guard) {
|
|
7901
8112
|
const byCollection = /* @__PURE__ */ new Map();
|
|
7902
8113
|
for (const ref of references2) {
|
|
7903
8114
|
if (!this.config.collections.some((c) => c.slug === ref.collection)) continue;
|
|
@@ -7926,7 +8137,8 @@ var EasyCMS = class {
|
|
|
7926
8137
|
}
|
|
7927
8138
|
if (ref.folderOnly && this.folders.enabled && !await this.folders.inKeyed(
|
|
7928
8139
|
ref.folderOnly,
|
|
7929
|
-
doc.folder ?? null
|
|
8140
|
+
doc.folder ?? null,
|
|
8141
|
+
await this.folderScope(guard)
|
|
7930
8142
|
))
|
|
7931
8143
|
errors.push({
|
|
7932
8144
|
field: ref.field,
|
|
@@ -7968,8 +8180,21 @@ function wherePaths(where) {
|
|
|
7968
8180
|
}
|
|
7969
8181
|
return out;
|
|
7970
8182
|
}
|
|
8183
|
+
var NO_CONTEXT = Object.freeze({});
|
|
8184
|
+
function withoutContext(permissions) {
|
|
8185
|
+
const { context: _context, ...rest } = permissions ?? {};
|
|
8186
|
+
return rest;
|
|
8187
|
+
}
|
|
8188
|
+
function splitGlobalKey(key2) {
|
|
8189
|
+
const at = key2.indexOf("@");
|
|
8190
|
+
return at < 0 ? { slug: key2 } : { slug: key2.slice(0, at), scope: key2.slice(at + 1) };
|
|
8191
|
+
}
|
|
7971
8192
|
function guardOf(options) {
|
|
7972
|
-
return {
|
|
8193
|
+
return {
|
|
8194
|
+
enforce: options.overrideAccess === false,
|
|
8195
|
+
user: options.user ?? null,
|
|
8196
|
+
context: options.context ?? NO_CONTEXT
|
|
8197
|
+
};
|
|
7973
8198
|
}
|
|
7974
8199
|
function deny(user) {
|
|
7975
8200
|
return user ? new ForbiddenError() : new UnauthorizedError();
|
|
@@ -8256,10 +8481,10 @@ function componentRef(component) {
|
|
|
8256
8481
|
if (typeof component === "string") return { tag: component };
|
|
8257
8482
|
return component.props ? { tag: component.tag, props: JSON.parse(JSON.stringify(component.props)) } : { tag: component.tag };
|
|
8258
8483
|
}
|
|
8259
|
-
async function allowed(access, user) {
|
|
8260
|
-
return await evaluateAccess(access, { user }) !== false;
|
|
8484
|
+
async function allowed(access, user, context) {
|
|
8485
|
+
return await evaluateAccess(access, { user, context }) !== false;
|
|
8261
8486
|
}
|
|
8262
|
-
async function serializeFields(fields, checks, localized, folderId) {
|
|
8487
|
+
async function serializeFields(fields, checks, localized, folderId, who) {
|
|
8263
8488
|
const { update: update2 } = checks;
|
|
8264
8489
|
const out = [];
|
|
8265
8490
|
for (const field of fields) {
|
|
@@ -8269,6 +8494,13 @@ async function serializeFields(fields, checks, localized, folderId) {
|
|
|
8269
8494
|
if (field.required) f.required = true;
|
|
8270
8495
|
if (field.unique) f.unique = true;
|
|
8271
8496
|
if (field.defaultValue !== void 0) f.defaultValue = field.defaultValue;
|
|
8497
|
+
if (field.admin?.defaultValue) {
|
|
8498
|
+
const value = await field.admin.defaultValue(who);
|
|
8499
|
+
if (value !== void 0) f.defaultValue = value;
|
|
8500
|
+
}
|
|
8501
|
+
const column = field.admin?.column;
|
|
8502
|
+
if (column === true || typeof column === "function" && await column(who)) f.column = true;
|
|
8503
|
+
if (field.admin?.allowCreate === false) f.noCreate = true;
|
|
8272
8504
|
if (!await update2.allows(field)) f.readOnly = true;
|
|
8273
8505
|
if (field.localized && localized) f.localized = true;
|
|
8274
8506
|
if (field.position === "sidebar") f.position = "sidebar";
|
|
@@ -8309,16 +8541,19 @@ async function serializeFields(fields, checks, localized, folderId) {
|
|
|
8309
8541
|
case "upload":
|
|
8310
8542
|
f.to = "media";
|
|
8311
8543
|
if (field.hasMany) f.hasMany = true;
|
|
8544
|
+
if (field.filterOptions) f.filtered = true;
|
|
8312
8545
|
if (field.minRows !== void 0) f.minRows = field.minRows;
|
|
8313
8546
|
if (field.maxRows !== void 0) f.maxRows = field.maxRows;
|
|
8314
8547
|
if (field.mimeTypes) f.mimeTypes = expandMimeTypes(field.mimeTypes);
|
|
8315
|
-
if (field.folder && folderId)
|
|
8316
|
-
|
|
8548
|
+
if (field.folder && folderId) {
|
|
8549
|
+
const id = await folderId(field.folder);
|
|
8550
|
+
if (id !== null) f.folder = { id, only: field.folderOnly === true };
|
|
8551
|
+
}
|
|
8317
8552
|
break;
|
|
8318
8553
|
case "array":
|
|
8319
8554
|
if (field.minRows !== void 0) f.minRows = field.minRows;
|
|
8320
8555
|
if (field.maxRows !== void 0) f.maxRows = field.maxRows;
|
|
8321
|
-
f.fields = await serializeFields(field.fields, checks, localized, folderId);
|
|
8556
|
+
f.fields = await serializeFields(field.fields, checks, localized, folderId, who);
|
|
8322
8557
|
break;
|
|
8323
8558
|
case "blocks":
|
|
8324
8559
|
if (field.minRows !== void 0) f.minRows = field.minRows;
|
|
@@ -8327,35 +8562,36 @@ async function serializeFields(fields, checks, localized, folderId) {
|
|
|
8327
8562
|
field.blocks.map(async (block) => ({
|
|
8328
8563
|
slug: block.slug,
|
|
8329
8564
|
...block.labels ? { labels: block.labels } : {},
|
|
8330
|
-
fields: await serializeFields(block.fields, checks, localized, folderId)
|
|
8565
|
+
fields: await serializeFields(block.fields, checks, localized, folderId, who)
|
|
8331
8566
|
}))
|
|
8332
8567
|
);
|
|
8333
8568
|
break;
|
|
8334
8569
|
case "group":
|
|
8335
|
-
f.fields = await serializeFields(field.fields, checks, localized, folderId);
|
|
8570
|
+
f.fields = await serializeFields(field.fields, checks, localized, folderId, who);
|
|
8336
8571
|
break;
|
|
8337
8572
|
}
|
|
8338
8573
|
out.push(f);
|
|
8339
8574
|
}
|
|
8340
8575
|
return out;
|
|
8341
8576
|
}
|
|
8342
|
-
async function checkers(cms, user) {
|
|
8577
|
+
async function checkers(cms, user, context) {
|
|
8343
8578
|
const rules = await cms.roles.fieldRules(user);
|
|
8344
8579
|
return {
|
|
8345
|
-
update: new FieldAccessChecker("update", { user }, rules),
|
|
8346
|
-
read: new FieldAccessChecker("read", { user }, rules)
|
|
8580
|
+
update: new FieldAccessChecker("update", { user, context }, rules),
|
|
8581
|
+
read: new FieldAccessChecker("read", { user, context }, rules)
|
|
8347
8582
|
};
|
|
8348
8583
|
}
|
|
8349
|
-
async function may(cms, access, user, target, operation) {
|
|
8350
|
-
return await allowed(access, user) && await cms.roles.allows(user, target, operation) !== false;
|
|
8584
|
+
async function may(cms, access, user, target, operation, context) {
|
|
8585
|
+
return await allowed(access, user, context) && await cms.roles.allows(user, target, operation) !== false;
|
|
8351
8586
|
}
|
|
8352
|
-
async function collection(cms, config, user, localized, roles) {
|
|
8587
|
+
async function collection(cms, config, user, localized, roles, context) {
|
|
8353
8588
|
const target = { collection: config.slug };
|
|
8354
8589
|
const fields = await serializeFields(
|
|
8355
8590
|
config.fields,
|
|
8356
|
-
await checkers(cms, user),
|
|
8591
|
+
await checkers(cms, user, context),
|
|
8357
8592
|
localized,
|
|
8358
|
-
folderIds(cms)
|
|
8593
|
+
folderIds(cms, user, context),
|
|
8594
|
+
{ user, context }
|
|
8359
8595
|
);
|
|
8360
8596
|
if (config.slug === USERS && cms.roles.enabled) {
|
|
8361
8597
|
const role = fields.find((f) => f.name === "role");
|
|
@@ -8372,11 +8608,11 @@ async function collection(cms, config, user, localized, roles) {
|
|
|
8372
8608
|
schedule: config.schedule === true,
|
|
8373
8609
|
fields,
|
|
8374
8610
|
permissions: {
|
|
8375
|
-
read: await may(cms, config.access?.read, user, target, "read"),
|
|
8376
|
-
create: await may(cms, config.access?.create, user, target, "create"),
|
|
8377
|
-
update: await may(cms, config.access?.update, user, target, "update"),
|
|
8378
|
-
delete: await may(cms, config.access?.delete, user, target, "delete"),
|
|
8379
|
-
publish: config.drafts === true && await may(cms, config.access?.update, user, target, "publish")
|
|
8611
|
+
read: await may(cms, config.access?.read, user, target, "read", context),
|
|
8612
|
+
create: await may(cms, config.access?.create, user, target, "create", context),
|
|
8613
|
+
update: await may(cms, config.access?.update, user, target, "update", context),
|
|
8614
|
+
delete: await may(cms, config.access?.delete, user, target, "delete", context),
|
|
8615
|
+
publish: config.drafts === true && await may(cms, config.access?.update, user, target, "publish", context)
|
|
8380
8616
|
}
|
|
8381
8617
|
};
|
|
8382
8618
|
if (config.labels) result.labels = config.labels;
|
|
@@ -8386,26 +8622,30 @@ async function collection(cms, config, user, localized, roles) {
|
|
|
8386
8622
|
if (config.admin?.group === "settings" || SETTINGS.has(config.slug)) result.group = "settings";
|
|
8387
8623
|
if (config.admin?.list?.tree) result.tree = config.admin.list.tree;
|
|
8388
8624
|
if (config.admin?.list?.sort) result.defaultSort = config.admin.list.sort;
|
|
8625
|
+
if (config.admin?.confirmDelete) result.confirmDelete = { ...config.admin.confirmDelete };
|
|
8389
8626
|
const owner = cms.roles.ownerOf(config.slug);
|
|
8390
8627
|
if (owner && fields.some((f) => f.name === owner)) result.owner = owner;
|
|
8391
8628
|
if (config.editIn === "drawer" && !result.drafts && !result.versions && !result.preview)
|
|
8392
8629
|
result.editIn = "drawer";
|
|
8393
8630
|
return result;
|
|
8394
8631
|
}
|
|
8395
|
-
async function global(cms, config, user, localized) {
|
|
8632
|
+
async function global(cms, config, user, localized, context) {
|
|
8396
8633
|
const target = { global: config.slug };
|
|
8397
|
-
const checks = await checkers(cms, user);
|
|
8634
|
+
const checks = await checkers(cms, user, context);
|
|
8398
8635
|
const result = {
|
|
8399
8636
|
slug: config.slug,
|
|
8400
8637
|
drafts: config.drafts === true,
|
|
8401
8638
|
versions: Boolean(config.versions),
|
|
8402
8639
|
preview: typeof config.preview === "function",
|
|
8403
8640
|
schedule: config.schedule === true,
|
|
8404
|
-
fields: await serializeFields(config.fields, checks, localized, folderIds(cms)
|
|
8641
|
+
fields: await serializeFields(config.fields, checks, localized, folderIds(cms, user, context), {
|
|
8642
|
+
user,
|
|
8643
|
+
context
|
|
8644
|
+
}),
|
|
8405
8645
|
permissions: {
|
|
8406
|
-
read: await may(cms, config.access?.read, user, target, "read"),
|
|
8407
|
-
update: await may(cms, config.access?.update, user, target, "update"),
|
|
8408
|
-
publish: config.drafts === true && await may(cms, config.access?.update, user, target, "publish")
|
|
8646
|
+
read: await may(cms, config.access?.read, user, target, "read", context),
|
|
8647
|
+
update: await may(cms, config.access?.update, user, target, "update", context),
|
|
8648
|
+
publish: config.drafts === true && await may(cms, config.access?.update, user, target, "publish", context)
|
|
8409
8649
|
}
|
|
8410
8650
|
};
|
|
8411
8651
|
if (config.label !== void 0) result.label = config.label;
|
|
@@ -8413,11 +8653,11 @@ async function global(cms, config, user, localized) {
|
|
|
8413
8653
|
if (config.admin?.sidebar?.length) result.sidebar = config.admin.sidebar.map(componentRef);
|
|
8414
8654
|
return result;
|
|
8415
8655
|
}
|
|
8416
|
-
async function adminSchema(cms, user, origin) {
|
|
8656
|
+
async function adminSchema(cms, user, origin, context = {}) {
|
|
8417
8657
|
const collections = cms.config.collections.filter((c) => !INTERNAL_COLLECTIONS.has(c.slug));
|
|
8418
8658
|
const localization = cms.config.localization;
|
|
8419
8659
|
const localized = localization !== null;
|
|
8420
|
-
const admin = user
|
|
8660
|
+
const admin = isSystemAdmin(user);
|
|
8421
8661
|
const deliveries = cms.config.collections.some(
|
|
8422
8662
|
(c) => c.slug === WEBHOOK_DELIVERIES || c.slug === EMAIL_DELIVERIES
|
|
8423
8663
|
);
|
|
@@ -8433,13 +8673,16 @@ async function adminSchema(cms, user, origin) {
|
|
|
8433
8673
|
const roles = await cms.roles.options();
|
|
8434
8674
|
return {
|
|
8435
8675
|
locale: cms.config.admin.locale,
|
|
8676
|
+
switcher: cms.config.admin.switcher,
|
|
8436
8677
|
passwordLinks: cms.auth.canSendPasswordLinks(origin),
|
|
8437
8678
|
menu: [...cms.config.admin.menu],
|
|
8438
8679
|
localization: localization ? { locales: [...localization.locales], defaultLocale: localization.defaultLocale } : null,
|
|
8439
8680
|
collections: await Promise.all(
|
|
8440
|
-
collections.map((c) => collection(cms, c, user, localized, roles))
|
|
8681
|
+
collections.map((c) => collection(cms, c, user, localized, roles, context))
|
|
8682
|
+
),
|
|
8683
|
+
globals: await Promise.all(
|
|
8684
|
+
cms.config.globals.map((g) => global(cms, g, user, localized, context))
|
|
8441
8685
|
),
|
|
8442
|
-
globals: await Promise.all(cms.config.globals.map((g) => global(cms, g, user, localized))),
|
|
8443
8686
|
modules: adminModuleUrls(cms),
|
|
8444
8687
|
uploadFromURL: cms.config.upload.fromURL !== void 0,
|
|
8445
8688
|
upload: {
|
|
@@ -8499,12 +8742,12 @@ async function widgets(cms, user) {
|
|
|
8499
8742
|
}
|
|
8500
8743
|
return out;
|
|
8501
8744
|
}
|
|
8502
|
-
function folderIds(cms) {
|
|
8503
|
-
return cms.folders.enabled ? (key2) => cms.
|
|
8745
|
+
function folderIds(cms, user, context) {
|
|
8746
|
+
return cms.folders.enabled ? (key2) => cms.keyedFolder(key2, { user, context }) : void 0;
|
|
8504
8747
|
}
|
|
8505
8748
|
|
|
8506
8749
|
// src/version.ts
|
|
8507
|
-
var VERSION = true ? "0.
|
|
8750
|
+
var VERSION = true ? "0.45.0" : "0.0.0-dev";
|
|
8508
8751
|
|
|
8509
8752
|
// src/rest/admin-status.ts
|
|
8510
8753
|
var DAY2 = 864e5;
|
|
@@ -8592,7 +8835,7 @@ function fieldAt(fields, path) {
|
|
|
8592
8835
|
}
|
|
8593
8836
|
return found;
|
|
8594
8837
|
}
|
|
8595
|
-
async function pickerFilter(cms, url, target, user) {
|
|
8838
|
+
async function pickerFilter(cms, url, target, user, context = {}) {
|
|
8596
8839
|
const forCollection = url.searchParams.get("filterFor");
|
|
8597
8840
|
const forGlobal = url.searchParams.get("filterForGlobal");
|
|
8598
8841
|
const spec = forCollection ?? forGlobal;
|
|
@@ -8601,12 +8844,13 @@ async function pickerFilter(cms, url, target, user) {
|
|
|
8601
8844
|
const slug2 = dot > 0 ? spec.slice(0, dot) : "";
|
|
8602
8845
|
const owner = forCollection !== null ? cms.config.collections.find((c) => c.slug === slug2) : cms.config.globals.find((g) => g.slug === slug2);
|
|
8603
8846
|
const field = owner ? fieldAt(owner.fields, spec.slice(dot + 1)) : void 0;
|
|
8604
|
-
|
|
8847
|
+
const to = field?.type === "upload" ? "media" : field?.type === "relationship" ? field.to : null;
|
|
8848
|
+
if (!field || field.type !== "relationship" && field.type !== "upload" || to !== target)
|
|
8605
8849
|
throw new QueryError(`filterFor: no relationship to "${target}" at "${spec}"`);
|
|
8606
8850
|
if (!field.filterOptions) return void 0;
|
|
8607
8851
|
const rawId = url.searchParams.get("filterId");
|
|
8608
8852
|
const id = rawId === null || rawId === "" ? void 0 : /^\d+$/.test(rawId) ? Number(rawId) : rawId;
|
|
8609
|
-
const where = await field.filterOptions({ id, user, cms });
|
|
8853
|
+
const where = await field.filterOptions({ id, user, cms, context });
|
|
8610
8854
|
return where === true ? void 0 : where;
|
|
8611
8855
|
}
|
|
8612
8856
|
|
|
@@ -8766,14 +9010,24 @@ async function respond(cms, request, url, path, root, options, production) {
|
|
|
8766
9010
|
if (path === null) throw new HttpError("Not found", 404);
|
|
8767
9011
|
const segments = pathSegments(path);
|
|
8768
9012
|
const { token, via } = readToken(request);
|
|
8769
|
-
const
|
|
8770
|
-
if (!
|
|
9013
|
+
const signedIn = token ? await cms.auth.verify(token) : null;
|
|
9014
|
+
if (!signedIn && via === "bearer" && token?.startsWith("ecms_"))
|
|
8771
9015
|
throw new UnauthorizedError("Invalid or expired API key");
|
|
8772
|
-
const
|
|
9016
|
+
const { user, context } = await cms.applyOnRequest(request.headers, url, signedIn);
|
|
9017
|
+
const ctx = {
|
|
9018
|
+
request,
|
|
9019
|
+
url,
|
|
9020
|
+
user,
|
|
9021
|
+
context,
|
|
9022
|
+
via: signedIn ? via : null,
|
|
9023
|
+
token,
|
|
9024
|
+
headers
|
|
9025
|
+
};
|
|
8773
9026
|
const method = request.method.toUpperCase();
|
|
8774
9027
|
if (method !== "GET" && method !== "HEAD") checkCsrf(cms, ctx);
|
|
8775
9028
|
const audit = {
|
|
8776
9029
|
user,
|
|
9030
|
+
context,
|
|
8777
9031
|
ip: options.getClientIp?.(request),
|
|
8778
9032
|
userAgent: request.headers.get("user-agent")
|
|
8779
9033
|
};
|
|
@@ -8833,6 +9087,7 @@ async function customEndpoint(cms, ctx, method, segments, root, options) {
|
|
|
8833
9087
|
url: ctx.url,
|
|
8834
9088
|
params,
|
|
8835
9089
|
user: ctx.user,
|
|
9090
|
+
context: ctx.context,
|
|
8836
9091
|
ip: options.getClientIp?.(ctx.request),
|
|
8837
9092
|
cms,
|
|
8838
9093
|
json: () => readJson(ctx.request)
|
|
@@ -8859,7 +9114,12 @@ function matchPath(path, segments) {
|
|
|
8859
9114
|
}
|
|
8860
9115
|
async function route(cms, ctx, method, segments, options) {
|
|
8861
9116
|
const [first, second, third] = segments;
|
|
8862
|
-
const access = {
|
|
9117
|
+
const access = {
|
|
9118
|
+
overrideAccess: false,
|
|
9119
|
+
user: ctx.user,
|
|
9120
|
+
context: ctx.context,
|
|
9121
|
+
...parseLocale(ctx.url)
|
|
9122
|
+
};
|
|
8863
9123
|
const draft = ctx.user !== null && ctx.url.searchParams.get("draft") === "true";
|
|
8864
9124
|
if (segments.length === 0) throw new HttpError("Not found", 404);
|
|
8865
9125
|
if (first === "auth") return ssoRoute(cms, ctx, method, segments);
|
|
@@ -8957,14 +9217,14 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
8957
9217
|
const secret = cms.config.cronSecret ?? process.env.CRON_SECRET;
|
|
8958
9218
|
const header = ctx.request.headers.get("authorization") ?? "";
|
|
8959
9219
|
const byCron = !!secret && safeEqual(header, `Bearer ${secret}`);
|
|
8960
|
-
if (!byCron && ctx.user
|
|
9220
|
+
if (!byCron && !isSystemAdmin(ctx.user)) {
|
|
8961
9221
|
throw ctx.user ? new HttpError("Forbidden", 403) : new UnauthorizedError();
|
|
8962
9222
|
}
|
|
8963
9223
|
return { body: await cms.runJobs() };
|
|
8964
9224
|
}
|
|
8965
9225
|
if (first === "admin" && second === "backups") {
|
|
8966
9226
|
if (!ctx.user) throw new UnauthorizedError();
|
|
8967
|
-
if (ctx.user
|
|
9227
|
+
if (!isSystemAdmin(ctx.user)) throw new ForbiddenError();
|
|
8968
9228
|
const [, , id2, action, extra] = segments;
|
|
8969
9229
|
if (extra !== void 0) throw new HttpError("Not found", 404);
|
|
8970
9230
|
if (id2 === void 0) {
|
|
@@ -9006,7 +9266,7 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9006
9266
|
}
|
|
9007
9267
|
if (first === "admin" && second === "email") {
|
|
9008
9268
|
if (!ctx.user) throw new UnauthorizedError();
|
|
9009
|
-
if (ctx.user
|
|
9269
|
+
if (!isSystemAdmin(ctx.user)) throw new ForbiddenError();
|
|
9010
9270
|
const [, , action, extra] = segments;
|
|
9011
9271
|
if (extra !== void 0) throw new HttpError("Not found", 404);
|
|
9012
9272
|
if (action === void 0) {
|
|
@@ -9025,7 +9285,7 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9025
9285
|
}
|
|
9026
9286
|
if (first === "admin" && second === "sso" && third === void 0) {
|
|
9027
9287
|
if (!ctx.user) throw new UnauthorizedError();
|
|
9028
|
-
if (ctx.user
|
|
9288
|
+
if (!isSystemAdmin(ctx.user)) throw new ForbiddenError();
|
|
9029
9289
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
9030
9290
|
return { body: cms.auth.sso.settings(ctx.url.origin) };
|
|
9031
9291
|
}
|
|
@@ -9040,8 +9300,11 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9040
9300
|
doc: q.get("doc"),
|
|
9041
9301
|
actor: q.get("actor"),
|
|
9042
9302
|
from: q.get("from"),
|
|
9043
|
-
to: q.get("to")
|
|
9303
|
+
to: q.get("to"),
|
|
9304
|
+
// Users of a part of the site (e.g. one tenant) see its entries; others the chosen part's.
|
|
9305
|
+
scope: cms.audit.scopeOf(ctx.context)
|
|
9044
9306
|
};
|
|
9307
|
+
if (ctx.user.scoped && !filter.scope) throw new ForbiddenError();
|
|
9045
9308
|
if (second === "audit.csv" && third === void 0) {
|
|
9046
9309
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
9047
9310
|
return {
|
|
@@ -9066,7 +9329,7 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9066
9329
|
}
|
|
9067
9330
|
if (first === "admin" && second === "owned" && third !== void 0 && segments.length === 3) {
|
|
9068
9331
|
if (!ctx.user) throw new UnauthorizedError();
|
|
9069
|
-
if (ctx.user
|
|
9332
|
+
if (!isSystemAdmin(ctx.user)) throw new ForbiddenError();
|
|
9070
9333
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
9071
9334
|
const parsed = parseId(third);
|
|
9072
9335
|
if (parsed === void 0) throw new HttpError("Not found", 404);
|
|
@@ -9080,7 +9343,7 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9080
9343
|
}
|
|
9081
9344
|
if (first === "admin" && second === "roles") {
|
|
9082
9345
|
if (!ctx.user) throw new UnauthorizedError();
|
|
9083
|
-
if (ctx.user
|
|
9346
|
+
if (!isSystemAdmin(ctx.user)) throw new ForbiddenError();
|
|
9084
9347
|
if (!cms.roles.enabled) throw new HttpError("Not found", 404);
|
|
9085
9348
|
const [, , id2, action, extra] = segments;
|
|
9086
9349
|
if (extra !== void 0) throw new HttpError("Not found", 404);
|
|
@@ -9166,7 +9429,7 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9166
9429
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
9167
9430
|
if (!ctx.user) throw new UnauthorizedError();
|
|
9168
9431
|
if (second === "schema" && third === void 0)
|
|
9169
|
-
return { body: await adminSchema(cms, ctx.user, ctx.url.origin) };
|
|
9432
|
+
return { body: await adminSchema(cms, ctx.user, ctx.url.origin, ctx.context) };
|
|
9170
9433
|
if (second === "modules" && third !== void 0 && segments.length === 3) {
|
|
9171
9434
|
const index = /^(\d+)\.js$/.exec(third)?.[1];
|
|
9172
9435
|
const file = index === void 0 ? void 0 : await readAdminModule(cms, Number(index));
|
|
@@ -9186,13 +9449,19 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9186
9449
|
return { body: await adminStatus(cms) };
|
|
9187
9450
|
}
|
|
9188
9451
|
if (second === "scheduled" && third === void 0)
|
|
9189
|
-
return {
|
|
9452
|
+
return {
|
|
9453
|
+
body: await cms.upcomingJobs({
|
|
9454
|
+
user: ctx.user,
|
|
9455
|
+
context: ctx.context,
|
|
9456
|
+
overrideAccess: false
|
|
9457
|
+
})
|
|
9458
|
+
};
|
|
9190
9459
|
const [, , collection3, id2, extra] = segments;
|
|
9191
9460
|
if (second === "access" && collection3 && id2 && extra === void 0) {
|
|
9192
9461
|
if (INTERNAL_COLLECTIONS.has(collection3) || !cms.config.collections.some((c) => c.slug === collection3)) {
|
|
9193
9462
|
throw new HttpError(`Unknown collection "${collection3}"`, 404);
|
|
9194
9463
|
}
|
|
9195
|
-
return { body: await cms.documentPermissions(collection3, id2, ctx.user) };
|
|
9464
|
+
return { body: await cms.documentPermissions(collection3, id2, ctx.user, ctx.context) };
|
|
9196
9465
|
}
|
|
9197
9466
|
throw new HttpError("Not found", 404);
|
|
9198
9467
|
}
|
|
@@ -9297,7 +9566,7 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9297
9566
|
if (second === void 0) {
|
|
9298
9567
|
if (method === "GET") {
|
|
9299
9568
|
const query = parseListQuery(ctx.url);
|
|
9300
|
-
const filter = await pickerFilter(cms, ctx.url, collection2, ctx.user);
|
|
9569
|
+
const filter = await pickerFilter(cms, ctx.url, collection2, ctx.user, ctx.context);
|
|
9301
9570
|
if (filter) query.where = query.where ? { and: [query.where, filter] } : filter;
|
|
9302
9571
|
return { body: await cms.find(collection2, { ...access, ...query, draft }) };
|
|
9303
9572
|
}
|
|
@@ -9342,12 +9611,17 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9342
9611
|
throw methodNotAllowed(ctx, "GET, PATCH, DELETE");
|
|
9343
9612
|
}
|
|
9344
9613
|
async function documentAction(cms, ctx, method, collection2, id, path) {
|
|
9345
|
-
const access = {
|
|
9614
|
+
const access = {
|
|
9615
|
+
overrideAccess: false,
|
|
9616
|
+
user: ctx.user,
|
|
9617
|
+
context: ctx.context,
|
|
9618
|
+
...parseLocale(ctx.url)
|
|
9619
|
+
};
|
|
9346
9620
|
const depth = parseDepth(ctx.url);
|
|
9347
9621
|
const [action, versionId, extra] = path;
|
|
9348
9622
|
if (collection2 === USERS && action === "password-link" && path.length === 1) {
|
|
9349
9623
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
9350
|
-
if (ctx.user
|
|
9624
|
+
if (!isSystemAdmin(ctx.user)) throw new ForbiddenError();
|
|
9351
9625
|
const body = await readJson(ctx.request).catch(() => ({}));
|
|
9352
9626
|
const sent = await cms.auth.sendPasswordLink(/^\d+$/.test(id) ? Number(id) : id, {
|
|
9353
9627
|
origin: ctx.url.origin,
|
|
@@ -9400,7 +9674,12 @@ async function documentAction(cms, ctx, method, collection2, id, path) {
|
|
|
9400
9674
|
throw new HttpError("Not found", 404);
|
|
9401
9675
|
}
|
|
9402
9676
|
async function globalAction(cms, ctx, method, slug2, path) {
|
|
9403
|
-
const access = {
|
|
9677
|
+
const access = {
|
|
9678
|
+
overrideAccess: false,
|
|
9679
|
+
user: ctx.user,
|
|
9680
|
+
context: ctx.context,
|
|
9681
|
+
...parseLocale(ctx.url)
|
|
9682
|
+
};
|
|
9404
9683
|
const depth = parseDepth(ctx.url);
|
|
9405
9684
|
const [action, versionId, extra] = path;
|
|
9406
9685
|
if (action === "versions" && versionId === void 0) {
|
|
@@ -9563,6 +9842,7 @@ async function servePrivateFile(cms, ctx, key2, head) {
|
|
|
9563
9842
|
limit: 5,
|
|
9564
9843
|
depth: 0,
|
|
9565
9844
|
user: ctx.user,
|
|
9845
|
+
context: ctx.context,
|
|
9566
9846
|
overrideAccess: false
|
|
9567
9847
|
});
|
|
9568
9848
|
const owns = found.docs.some(
|
|
@@ -9663,7 +9943,7 @@ async function ssoRoute(cms, ctx, method, segments) {
|
|
|
9663
9943
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
9664
9944
|
const asked = ctx.url.searchParams.get("user");
|
|
9665
9945
|
const target = asked === null ? ctx.user.id : parseId(asked) ?? asked;
|
|
9666
|
-
if (String(target) !== String(ctx.user.id) && (ctx.user
|
|
9946
|
+
if (String(target) !== String(ctx.user.id) && !isSystemAdmin(ctx.user))
|
|
9667
9947
|
throw new ForbiddenError();
|
|
9668
9948
|
return { body: await sso.identities(target) };
|
|
9669
9949
|
}
|
|
@@ -10061,6 +10341,7 @@ export {
|
|
|
10061
10341
|
isAdmin,
|
|
10062
10342
|
isHasMany,
|
|
10063
10343
|
isLoggedIn,
|
|
10344
|
+
isSystemAdmin,
|
|
10064
10345
|
keyAllows,
|
|
10065
10346
|
loadConfig,
|
|
10066
10347
|
localStorage,
|
|
@@ -10078,6 +10359,7 @@ export {
|
|
|
10078
10359
|
singularize,
|
|
10079
10360
|
slugify,
|
|
10080
10361
|
sniffMimeType,
|
|
10362
|
+
uniqueWithinOf,
|
|
10081
10363
|
validateConfig,
|
|
10082
10364
|
validateFields,
|
|
10083
10365
|
verifyPassword,
|