@easy-cms/core 0.32.0 → 0.33.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 +161 -5
- package/dist/index.js +652 -195
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -326,6 +326,7 @@ var SCHEDULED_JOBS = "scheduled-jobs";
|
|
|
326
326
|
var WEBHOOK_DELIVERIES = "webhook-deliveries";
|
|
327
327
|
var EMAIL_DELIVERIES = "email-deliveries";
|
|
328
328
|
var DATABASE_BACKUPS = "database-backups";
|
|
329
|
+
var ROLES = "user-roles";
|
|
329
330
|
var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
330
331
|
SESSIONS,
|
|
331
332
|
LOGIN_ATTEMPTS,
|
|
@@ -333,7 +334,8 @@ var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
|
333
334
|
SCHEDULED_JOBS,
|
|
334
335
|
WEBHOOK_DELIVERIES,
|
|
335
336
|
EMAIL_DELIVERIES,
|
|
336
|
-
DATABASE_BACKUPS
|
|
337
|
+
DATABASE_BACKUPS,
|
|
338
|
+
ROLES
|
|
337
339
|
]);
|
|
338
340
|
var DEFAULT_ROLES = ["admin", "editor"];
|
|
339
341
|
var adminOrSelf = ({ user }) => {
|
|
@@ -344,7 +346,8 @@ var adminOrSelf = ({ user }) => {
|
|
|
344
346
|
var adminOnly = {
|
|
345
347
|
update: ({ user }) => user?.role === "admin"
|
|
346
348
|
};
|
|
347
|
-
function userFields(roles) {
|
|
349
|
+
function userFields(roles, rbac) {
|
|
350
|
+
const defaultRole = roles.includes("editor") ? "editor" : roles[roles.length - 1];
|
|
348
351
|
return [
|
|
349
352
|
{
|
|
350
353
|
name: "email",
|
|
@@ -354,12 +357,20 @@ function userFields(roles) {
|
|
|
354
357
|
label: { en: "Email", th: "\u0E2D\u0E35\u0E40\u0E21\u0E25" }
|
|
355
358
|
},
|
|
356
359
|
{ name: "name", type: "text", label: { en: "Name", th: "\u0E0A\u0E37\u0E48\u0E2D" } },
|
|
357
|
-
|
|
360
|
+
// With roles from the admin (`auth.rbac`), any role in Settings → Roles: checked on save.
|
|
361
|
+
rbac ? {
|
|
362
|
+
name: "role",
|
|
363
|
+
type: "text",
|
|
364
|
+
required: true,
|
|
365
|
+
defaultValue: defaultRole,
|
|
366
|
+
access: adminOnly,
|
|
367
|
+
label: { en: "Role", th: "\u0E1A\u0E17\u0E1A\u0E32\u0E17" }
|
|
368
|
+
} : {
|
|
358
369
|
name: "role",
|
|
359
370
|
type: "select",
|
|
360
371
|
options: roles,
|
|
361
372
|
required: true,
|
|
362
|
-
defaultValue:
|
|
373
|
+
defaultValue: defaultRole,
|
|
363
374
|
access: adminOnly,
|
|
364
375
|
label: { en: "Role", th: "\u0E1A\u0E17\u0E1A\u0E32\u0E17" }
|
|
365
376
|
},
|
|
@@ -382,7 +393,7 @@ function withUsers(config) {
|
|
|
382
393
|
useAsTitle: "email",
|
|
383
394
|
icon: "users",
|
|
384
395
|
...custom,
|
|
385
|
-
fields: [...userFields(roles), ...custom?.fields ?? []],
|
|
396
|
+
fields: [...userFields(roles, config.auth?.rbac === true), ...custom?.fields ?? []],
|
|
386
397
|
access: {
|
|
387
398
|
read: isLoggedIn,
|
|
388
399
|
create: isAdmin,
|
|
@@ -1690,7 +1701,7 @@ function parseId(value) {
|
|
|
1690
1701
|
async function validateFields(fields, input, options, prefix = "") {
|
|
1691
1702
|
const data = {};
|
|
1692
1703
|
const errors = [];
|
|
1693
|
-
const
|
|
1704
|
+
const references2 = [];
|
|
1694
1705
|
for (const field of fields) {
|
|
1695
1706
|
const path = prefix + field.name;
|
|
1696
1707
|
const raw = input[field.name];
|
|
@@ -1708,7 +1719,7 @@ async function validateFields(fields, input, options, prefix = "") {
|
|
|
1708
1719
|
);
|
|
1709
1720
|
data[field.name] = sub.data;
|
|
1710
1721
|
errors.push(...sub.errors);
|
|
1711
|
-
|
|
1722
|
+
references2.push(...sub.references);
|
|
1712
1723
|
continue;
|
|
1713
1724
|
}
|
|
1714
1725
|
const localization = options.localization;
|
|
@@ -1738,7 +1749,7 @@ async function validateFields(fields, input, options, prefix = "") {
|
|
|
1738
1749
|
options,
|
|
1739
1750
|
failLocale,
|
|
1740
1751
|
errors,
|
|
1741
|
-
|
|
1752
|
+
references2
|
|
1742
1753
|
);
|
|
1743
1754
|
if (normalized === void 0) continue;
|
|
1744
1755
|
out[locale] = normalized;
|
|
@@ -1755,12 +1766,12 @@ async function validateFields(fields, input, options, prefix = "") {
|
|
|
1755
1766
|
if (!field.required) await runCustom(field, null, options, fail);
|
|
1756
1767
|
continue;
|
|
1757
1768
|
}
|
|
1758
|
-
const value = await normalizeValue(field, raw, path, options, fail, errors,
|
|
1769
|
+
const value = await normalizeValue(field, raw, path, options, fail, errors, references2);
|
|
1759
1770
|
if (value === void 0) continue;
|
|
1760
1771
|
data[field.name] = value;
|
|
1761
1772
|
await runCustom(field, value, options, fail);
|
|
1762
1773
|
}
|
|
1763
|
-
return { data, errors, references };
|
|
1774
|
+
return { data, errors, references: references2 };
|
|
1764
1775
|
}
|
|
1765
1776
|
async function runCustom(field, value, options, fail) {
|
|
1766
1777
|
if (!field.validate) return;
|
|
@@ -1768,7 +1779,7 @@ async function runCustom(field, value, options, fail) {
|
|
|
1768
1779
|
const result = await validate(value, { data: options.root, operation: options.operation });
|
|
1769
1780
|
if (result !== true) fail(typeof result === "string" ? result : "is invalid");
|
|
1770
1781
|
}
|
|
1771
|
-
async function normalizeValue(field, raw, path, options, fail, errors,
|
|
1782
|
+
async function normalizeValue(field, raw, path, options, fail, errors, references2) {
|
|
1772
1783
|
switch (field.type) {
|
|
1773
1784
|
case "text":
|
|
1774
1785
|
case "textarea":
|
|
@@ -1830,7 +1841,7 @@ async function normalizeValue(field, raw, path, options, fail, errors, reference
|
|
|
1830
1841
|
if (id === void 0)
|
|
1831
1842
|
return fail(`must be ${many ? "ids" : "an id"} of "${target}" documents`);
|
|
1832
1843
|
ids.push(id);
|
|
1833
|
-
|
|
1844
|
+
references2.push({
|
|
1834
1845
|
field: path,
|
|
1835
1846
|
collection: target,
|
|
1836
1847
|
id,
|
|
@@ -1854,19 +1865,19 @@ async function normalizeValue(field, raw, path, options, fail, errors, reference
|
|
|
1854
1865
|
}
|
|
1855
1866
|
const sub = await validateFields(field.fields, row, options, `${path}.${i}.`);
|
|
1856
1867
|
errors.push(...sub.errors);
|
|
1857
|
-
|
|
1868
|
+
references2.push(...sub.references);
|
|
1858
1869
|
const id = typeof row.id === "string" && row.id !== "" ? row.id : randomUUID();
|
|
1859
1870
|
rows.push({ id, ...sub.data });
|
|
1860
1871
|
}
|
|
1861
1872
|
return rows;
|
|
1862
1873
|
}
|
|
1863
1874
|
case "blocks":
|
|
1864
|
-
return normalizeBlocks(field, raw, path, options, fail, errors,
|
|
1875
|
+
return normalizeBlocks(field, raw, path, options, fail, errors, references2);
|
|
1865
1876
|
case "group":
|
|
1866
1877
|
return raw;
|
|
1867
1878
|
}
|
|
1868
1879
|
}
|
|
1869
|
-
async function normalizeBlocks(field, raw, path, options, fail, errors,
|
|
1880
|
+
async function normalizeBlocks(field, raw, path, options, fail, errors, references2) {
|
|
1870
1881
|
if (!Array.isArray(raw)) return fail("must be an array of blocks");
|
|
1871
1882
|
if (field.minRows !== void 0 && raw.length < field.minRows)
|
|
1872
1883
|
return fail(`must have at least ${field.minRows} blocks`);
|
|
@@ -1884,7 +1895,7 @@ async function normalizeBlocks(field, raw, path, options, fail, errors, referenc
|
|
|
1884
1895
|
}
|
|
1885
1896
|
const sub = await validateFields(fields, row, options, `${path}.${i}.`);
|
|
1886
1897
|
errors.push(...sub.errors);
|
|
1887
|
-
|
|
1898
|
+
references2.push(...sub.references);
|
|
1888
1899
|
const id = typeof row.id === "string" && row.id !== "" ? row.id : randomUUID();
|
|
1889
1900
|
rows.push({ id, blockType: row.blockType, ...sub.data });
|
|
1890
1901
|
}
|
|
@@ -2168,6 +2179,487 @@ import { existsSync } from "fs";
|
|
|
2168
2179
|
import { resolve as resolve2 } from "path";
|
|
2169
2180
|
import { createJiti } from "jiti";
|
|
2170
2181
|
|
|
2182
|
+
// src/versions.ts
|
|
2183
|
+
var DEFAULT_MAX_VERSIONS = 50;
|
|
2184
|
+
function versionLimit(config) {
|
|
2185
|
+
const versions = config.versions;
|
|
2186
|
+
if (!versions) return void 0;
|
|
2187
|
+
return versions === true ? DEFAULT_MAX_VERSIONS : versions.max ?? DEFAULT_MAX_VERSIONS;
|
|
2188
|
+
}
|
|
2189
|
+
var collectionParent = (slug2) => slug2;
|
|
2190
|
+
var globalParent = (slug2) => `global:${slug2}`;
|
|
2191
|
+
function snapshotOf(fields, doc) {
|
|
2192
|
+
const hidden = new Set(fields.filter((f) => f.hidden).map((f) => f.name));
|
|
2193
|
+
const snapshot = {};
|
|
2194
|
+
for (const [key2, value] of Object.entries(doc)) {
|
|
2195
|
+
if (key2 === "id" || key2 === "createdAt" || key2 === "updatedAt" || hidden.has(key2)) continue;
|
|
2196
|
+
snapshot[key2] = value;
|
|
2197
|
+
}
|
|
2198
|
+
return snapshot;
|
|
2199
|
+
}
|
|
2200
|
+
var toSummary = (row) => ({
|
|
2201
|
+
id: row.id,
|
|
2202
|
+
status: row.status ?? null,
|
|
2203
|
+
latest: row.latest === true,
|
|
2204
|
+
author: row.author ?? null,
|
|
2205
|
+
createdAt: String(row.createdAt)
|
|
2206
|
+
});
|
|
2207
|
+
var VersionStore = class {
|
|
2208
|
+
constructor(db) {
|
|
2209
|
+
this.db = db;
|
|
2210
|
+
}
|
|
2211
|
+
db;
|
|
2212
|
+
/** Saves a new latest version and deletes the oldest beyond `max`. */
|
|
2213
|
+
async save(args) {
|
|
2214
|
+
const { parent, doc } = args;
|
|
2215
|
+
const previous = await this.db.find({
|
|
2216
|
+
collection: VERSIONS,
|
|
2217
|
+
where: { and: [...this.of(parent, doc), { latest: { equals: true } }] },
|
|
2218
|
+
sort: [],
|
|
2219
|
+
limit: 0,
|
|
2220
|
+
page: 1
|
|
2221
|
+
});
|
|
2222
|
+
for (const row of previous.docs) {
|
|
2223
|
+
await this.db.update({ collection: VERSIONS, id: row.id, data: { ...row, latest: false } });
|
|
2224
|
+
}
|
|
2225
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2226
|
+
const created = await this.db.create({
|
|
2227
|
+
collection: VERSIONS,
|
|
2228
|
+
data: {
|
|
2229
|
+
parent,
|
|
2230
|
+
doc,
|
|
2231
|
+
status: args.status,
|
|
2232
|
+
latest: true,
|
|
2233
|
+
author: args.author,
|
|
2234
|
+
snapshot: args.snapshot,
|
|
2235
|
+
createdAt: now,
|
|
2236
|
+
updatedAt: now
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
await this.prune(parent, doc, args.max);
|
|
2240
|
+
return toSummary(created);
|
|
2241
|
+
}
|
|
2242
|
+
/** The latest version of each document, keyed by document id. */
|
|
2243
|
+
async latest(parent, docs) {
|
|
2244
|
+
const result = /* @__PURE__ */ new Map();
|
|
2245
|
+
if (docs.length === 0) return result;
|
|
2246
|
+
const rows = await this.db.find({
|
|
2247
|
+
collection: VERSIONS,
|
|
2248
|
+
where: {
|
|
2249
|
+
and: [
|
|
2250
|
+
{ parent: { equals: parent } },
|
|
2251
|
+
{ doc: { in: [...docs] } },
|
|
2252
|
+
{ latest: { equals: true } }
|
|
2253
|
+
]
|
|
2254
|
+
},
|
|
2255
|
+
sort: [],
|
|
2256
|
+
limit: 0,
|
|
2257
|
+
page: 1
|
|
2258
|
+
});
|
|
2259
|
+
for (const row of rows.docs) result.set(String(row.doc), this.toVersion(row));
|
|
2260
|
+
return result;
|
|
2261
|
+
}
|
|
2262
|
+
async list(parent, doc, limit, page) {
|
|
2263
|
+
const found = await this.db.find({
|
|
2264
|
+
collection: VERSIONS,
|
|
2265
|
+
where: { and: this.of(parent, doc) },
|
|
2266
|
+
sort: ["-id"],
|
|
2267
|
+
limit,
|
|
2268
|
+
page
|
|
2269
|
+
});
|
|
2270
|
+
return { ...found, docs: found.docs.map(toSummary) };
|
|
2271
|
+
}
|
|
2272
|
+
async get(parent, doc, id) {
|
|
2273
|
+
const found = await this.db.find({
|
|
2274
|
+
collection: VERSIONS,
|
|
2275
|
+
where: { and: [...this.of(parent, doc), { id: { equals: id } }] },
|
|
2276
|
+
sort: [],
|
|
2277
|
+
limit: 1,
|
|
2278
|
+
page: 1
|
|
2279
|
+
});
|
|
2280
|
+
const row = found.docs[0];
|
|
2281
|
+
return row ? this.toVersion(row) : null;
|
|
2282
|
+
}
|
|
2283
|
+
async deleteAll(parent, doc) {
|
|
2284
|
+
const rows = await this.db.find({
|
|
2285
|
+
collection: VERSIONS,
|
|
2286
|
+
where: { and: this.of(parent, doc) },
|
|
2287
|
+
sort: [],
|
|
2288
|
+
limit: 0,
|
|
2289
|
+
page: 1
|
|
2290
|
+
});
|
|
2291
|
+
for (const row of rows.docs) await this.db.delete({ collection: VERSIONS, id: row.id });
|
|
2292
|
+
}
|
|
2293
|
+
async prune(parent, doc, max) {
|
|
2294
|
+
const rows = await this.db.find({
|
|
2295
|
+
collection: VERSIONS,
|
|
2296
|
+
where: { and: this.of(parent, doc) },
|
|
2297
|
+
sort: ["-id"],
|
|
2298
|
+
limit: 0,
|
|
2299
|
+
page: 1
|
|
2300
|
+
});
|
|
2301
|
+
for (const row of rows.docs.slice(max)) {
|
|
2302
|
+
await this.db.delete({ collection: VERSIONS, id: row.id });
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
of(parent, doc) {
|
|
2306
|
+
return [{ parent: { equals: parent } }, { doc: { equals: doc } }];
|
|
2307
|
+
}
|
|
2308
|
+
toVersion(row) {
|
|
2309
|
+
return { ...toSummary(row), data: row.snapshot ?? {} };
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
|
|
2313
|
+
// src/roles.ts
|
|
2314
|
+
var ROLE_OPERATIONS = ["read", "create", "update", "delete", "publish"];
|
|
2315
|
+
var GLOBAL_OPERATIONS = ["read", "update", "publish"];
|
|
2316
|
+
var KEY = /^[a-z][a-z0-9-]{0,31}$/;
|
|
2317
|
+
var HISTORY = 50;
|
|
2318
|
+
var TTL = 3e4;
|
|
2319
|
+
var rolesCollection = {
|
|
2320
|
+
slug: ROLES,
|
|
2321
|
+
access: { read: () => false, create: () => false, update: () => false, delete: () => false },
|
|
2322
|
+
fields: [
|
|
2323
|
+
{ name: "key", type: "text", required: true, unique: true },
|
|
2324
|
+
{ name: "name", type: "text" },
|
|
2325
|
+
{ name: "permissions", type: "json" }
|
|
2326
|
+
]
|
|
2327
|
+
};
|
|
2328
|
+
function governed(slug2) {
|
|
2329
|
+
return !INTERNAL_COLLECTIONS.has(slug2) && slug2 !== API_KEYS;
|
|
2330
|
+
}
|
|
2331
|
+
function references(fields, out = /* @__PURE__ */ new Set()) {
|
|
2332
|
+
for (const field of fields) {
|
|
2333
|
+
if (field.type === "relationship") out.add(field.to);
|
|
2334
|
+
else if (field.type === "upload") out.add("media");
|
|
2335
|
+
else if (field.type === "group" || field.type === "array") references(field.fields, out);
|
|
2336
|
+
else if (field.type === "blocks") for (const b of field.blocks) references(b.fields, out);
|
|
2337
|
+
}
|
|
2338
|
+
return out;
|
|
2339
|
+
}
|
|
2340
|
+
var viewId = {
|
|
2341
|
+
page: (path) => `page:${path}`,
|
|
2342
|
+
widget: (tag) => `widget:${tag}`
|
|
2343
|
+
};
|
|
2344
|
+
var tagOf = (component) => typeof component === "string" ? component : component.tag;
|
|
2345
|
+
var Roles = class {
|
|
2346
|
+
constructor(config, db) {
|
|
2347
|
+
this.config = config;
|
|
2348
|
+
this.db = db;
|
|
2349
|
+
this.enabled = config.auth.rbac;
|
|
2350
|
+
this.versions = new VersionStore(db);
|
|
2351
|
+
}
|
|
2352
|
+
config;
|
|
2353
|
+
db;
|
|
2354
|
+
enabled;
|
|
2355
|
+
versions;
|
|
2356
|
+
cache;
|
|
2357
|
+
synced;
|
|
2358
|
+
/**
|
|
2359
|
+
* Whether the user's role allows an operation: `true`, `false`, or for their own account
|
|
2360
|
+
* (users without read or update on `users`) a constraint to it.
|
|
2361
|
+
*/
|
|
2362
|
+
async allows(user, target, operation) {
|
|
2363
|
+
if (!this.enabled || !user || user.role === "admin") return true;
|
|
2364
|
+
if ("collection" in target && !governed(target.collection)) return true;
|
|
2365
|
+
const permissions = await this.permissionsOf(user.role);
|
|
2366
|
+
const ops = "collection" in target ? permissions.collections?.[target.collection] : permissions.globals?.[target.global];
|
|
2367
|
+
if (ops?.includes(operation)) return true;
|
|
2368
|
+
if ("collection" in target && target.collection === USERS) {
|
|
2369
|
+
if (operation === "read" || operation === "update") return { id: { equals: user.id } };
|
|
2370
|
+
}
|
|
2371
|
+
return false;
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Whether the user may open an admin page: admins always; with roles, those whose role has
|
|
2375
|
+
* it; without roles, nobody else (`status`, `deliveries`) or everybody (plugin pages).
|
|
2376
|
+
*/
|
|
2377
|
+
async canView(user, id) {
|
|
2378
|
+
if (user.role === "admin" && !user.apiKey) return true;
|
|
2379
|
+
if (user.apiKey) return false;
|
|
2380
|
+
if (!this.enabled) return id.startsWith("page:") || id.startsWith("widget:");
|
|
2381
|
+
return (await this.permissionsOf(user.role)).admin?.includes(id) ?? false;
|
|
2382
|
+
}
|
|
2383
|
+
/** Forgets cached permissions, after a role changed. */
|
|
2384
|
+
invalidate() {
|
|
2385
|
+
this.cache = void 0;
|
|
2386
|
+
}
|
|
2387
|
+
/**
|
|
2388
|
+
* Adds the roles of `auth.roles` that are missing; on the first start, gives them everything.
|
|
2389
|
+
* Runs once, on first use (not on startup: `easy-cms migrate` opens the CMS before the table exists).
|
|
2390
|
+
*/
|
|
2391
|
+
sync() {
|
|
2392
|
+
this.synced ??= this.addMissing().catch((error) => {
|
|
2393
|
+
this.synced = void 0;
|
|
2394
|
+
throw error;
|
|
2395
|
+
});
|
|
2396
|
+
return this.synced;
|
|
2397
|
+
}
|
|
2398
|
+
async addMissing() {
|
|
2399
|
+
if (!this.enabled) return;
|
|
2400
|
+
const rows = await this.rows();
|
|
2401
|
+
const first = rows.length === 0;
|
|
2402
|
+
const known = new Set(rows.map((r) => String(r.key)));
|
|
2403
|
+
for (const key2 of this.config.auth.roles) {
|
|
2404
|
+
if (known.has(key2)) continue;
|
|
2405
|
+
try {
|
|
2406
|
+
await this.insert(key2, "", first && key2 !== "admin" ? this.everything() : {}, null);
|
|
2407
|
+
} catch (error) {
|
|
2408
|
+
if (!(await this.rows()).some((r) => r.key === key2)) throw error;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
this.invalidate();
|
|
2412
|
+
}
|
|
2413
|
+
/** Whether a role exists (users can only be given existing roles). */
|
|
2414
|
+
async exists(key2) {
|
|
2415
|
+
if (!this.enabled) return this.config.auth.roles.includes(key2);
|
|
2416
|
+
return (await this.load()).has(key2);
|
|
2417
|
+
}
|
|
2418
|
+
/** Roles a user can be given, those of `auth.roles` first; `name` may be empty. */
|
|
2419
|
+
async options() {
|
|
2420
|
+
if (!this.enabled) return this.config.auth.roles.map((key2) => ({ key: key2, name: "" }));
|
|
2421
|
+
await this.sync();
|
|
2422
|
+
return (await this.rows()).map((row) => ({ key: String(row.key), name: typeof row.name === "string" ? row.name : "" })).sort((a, b) => rank(a, this.config) - rank(b, this.config) || a.key.localeCompare(b.key));
|
|
2423
|
+
}
|
|
2424
|
+
// --- Settings → Roles ----------------------------------------------------
|
|
2425
|
+
async list() {
|
|
2426
|
+
await this.sync();
|
|
2427
|
+
const rows = await this.rows();
|
|
2428
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2429
|
+
for (const row of rows) {
|
|
2430
|
+
counts.set(
|
|
2431
|
+
String(row.key),
|
|
2432
|
+
await this.db.count({ collection: USERS, where: { role: { equals: row.key } } })
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
const roles = rows.map((row) => this.toRole(row, counts.get(String(row.key)) ?? 0)).sort((a, b) => rank(a, this.config) - rank(b, this.config) || a.key.localeCompare(b.key));
|
|
2436
|
+
const decided = roles.filter((r) => r.key !== "admin").map((r) => r.permissions);
|
|
2437
|
+
const seen = (group, slug2) => decided.some((p) => p[group]?.[slug2] !== void 0);
|
|
2438
|
+
return {
|
|
2439
|
+
roles,
|
|
2440
|
+
collections: this.config.collections.filter((c) => governed(c.slug)).map((c) => ({
|
|
2441
|
+
slug: c.slug,
|
|
2442
|
+
ops: ROLE_OPERATIONS.filter((op) => op !== "publish" || c.drafts === true),
|
|
2443
|
+
new: !seen("collections", c.slug),
|
|
2444
|
+
references: [...references(c.fields)].filter((s) => s !== c.slug && governed(s))
|
|
2445
|
+
})),
|
|
2446
|
+
globals: this.config.globals.map((g) => ({
|
|
2447
|
+
slug: g.slug,
|
|
2448
|
+
ops: GLOBAL_OPERATIONS.filter((op) => op !== "publish" || g.drafts === true),
|
|
2449
|
+
new: !seen("globals", g.slug)
|
|
2450
|
+
})),
|
|
2451
|
+
views: this.views()
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
async create(input, author) {
|
|
2455
|
+
await this.sync();
|
|
2456
|
+
const key2 = typeof input.key === "string" ? input.key.trim() : "";
|
|
2457
|
+
if (!KEY.test(key2)) {
|
|
2458
|
+
throw new ValidationError(ROLES, [
|
|
2459
|
+
{ field: "key", message: 'lowercase letters, digits and "-", starting with a letter' }
|
|
2460
|
+
]);
|
|
2461
|
+
}
|
|
2462
|
+
if ((await this.rows()).some((r) => r.key === key2))
|
|
2463
|
+
throw new ValidationError(ROLES, [{ field: "key", message: "is already used" }]);
|
|
2464
|
+
const row = await this.insert(
|
|
2465
|
+
key2,
|
|
2466
|
+
this.cleanName(input.name),
|
|
2467
|
+
this.clean(input.permissions),
|
|
2468
|
+
author.id
|
|
2469
|
+
);
|
|
2470
|
+
return this.toRole(row, 0);
|
|
2471
|
+
}
|
|
2472
|
+
async update(id, input, author) {
|
|
2473
|
+
const row = await this.row(id);
|
|
2474
|
+
const data = { ...row, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2475
|
+
if (input.name !== void 0) data.name = this.cleanName(input.name);
|
|
2476
|
+
if (input.permissions !== void 0) {
|
|
2477
|
+
if (row.key === "admin")
|
|
2478
|
+
throw new EasyCMSError("Admins may always do everything; their role cannot be changed", 400);
|
|
2479
|
+
data.permissions = this.clean(input.permissions);
|
|
2480
|
+
}
|
|
2481
|
+
const { id: _id, ...rest } = data;
|
|
2482
|
+
const saved = await this.db.update({ collection: ROLES, id: row.id, data: rest });
|
|
2483
|
+
await this.record(saved, author.id);
|
|
2484
|
+
this.invalidate();
|
|
2485
|
+
return this.toRole(saved, await this.users(String(row.key)));
|
|
2486
|
+
}
|
|
2487
|
+
async delete(id) {
|
|
2488
|
+
const row = await this.row(id);
|
|
2489
|
+
const key2 = String(row.key);
|
|
2490
|
+
if (this.config.auth.roles.includes(key2))
|
|
2491
|
+
throw new EasyCMSError(`"${key2}" is in auth.roles and cannot be deleted`, 400);
|
|
2492
|
+
const users = await this.users(key2);
|
|
2493
|
+
if (users > 0)
|
|
2494
|
+
throw new EasyCMSError(`${users} user(s) have this role; give them another role first`, 409);
|
|
2495
|
+
await this.db.delete({ collection: ROLES, id: row.id });
|
|
2496
|
+
await this.versions.deleteAll(ROLES, row.id);
|
|
2497
|
+
this.invalidate();
|
|
2498
|
+
}
|
|
2499
|
+
async history(id) {
|
|
2500
|
+
const row = await this.row(id);
|
|
2501
|
+
const list = await this.versions.list(ROLES, row.id, HISTORY, 1);
|
|
2502
|
+
const emails = /* @__PURE__ */ new Map();
|
|
2503
|
+
const out = [];
|
|
2504
|
+
for (const summary of list.docs) {
|
|
2505
|
+
const version = await this.versions.get(ROLES, row.id, summary.id);
|
|
2506
|
+
if (!version) continue;
|
|
2507
|
+
let author = null;
|
|
2508
|
+
if (summary.author !== null) {
|
|
2509
|
+
const k = String(summary.author);
|
|
2510
|
+
if (!emails.has(k)) {
|
|
2511
|
+
const user = await this.db.findById({ collection: USERS, id: summary.author });
|
|
2512
|
+
emails.set(k, typeof user?.email === "string" ? user.email : null);
|
|
2513
|
+
}
|
|
2514
|
+
author = emails.get(k) ?? null;
|
|
2515
|
+
}
|
|
2516
|
+
out.push({
|
|
2517
|
+
id: summary.id,
|
|
2518
|
+
author,
|
|
2519
|
+
createdAt: summary.createdAt,
|
|
2520
|
+
name: String(version.data.name ?? ""),
|
|
2521
|
+
permissions: version.data.permissions ?? {}
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
return out;
|
|
2525
|
+
}
|
|
2526
|
+
// -------------------------------------------------------------------------
|
|
2527
|
+
/** Pages and panels roles can be given. */
|
|
2528
|
+
views() {
|
|
2529
|
+
return [
|
|
2530
|
+
{ id: "status" },
|
|
2531
|
+
...this.config.collections.some(
|
|
2532
|
+
(c) => c.slug === WEBHOOK_DELIVERIES || c.slug === EMAIL_DELIVERIES
|
|
2533
|
+
) ? [{ id: "deliveries" }] : [],
|
|
2534
|
+
...this.config.admin.pages.map((page) => ({ id: viewId.page(page.path), label: page.label })),
|
|
2535
|
+
...this.config.admin.dashboard.map((widget) => ({
|
|
2536
|
+
id: viewId.widget(tagOf(widget.component)),
|
|
2537
|
+
...widget.label !== void 0 ? { label: widget.label } : {}
|
|
2538
|
+
}))
|
|
2539
|
+
];
|
|
2540
|
+
}
|
|
2541
|
+
/** Every collection and global, and the plugin pages and panels: what roles had before roles. */
|
|
2542
|
+
everything() {
|
|
2543
|
+
const collections = {};
|
|
2544
|
+
for (const c of this.config.collections) {
|
|
2545
|
+
if (governed(c.slug))
|
|
2546
|
+
collections[c.slug] = ROLE_OPERATIONS.filter((op) => op !== "publish" || c.drafts === true);
|
|
2547
|
+
}
|
|
2548
|
+
const globals = {};
|
|
2549
|
+
for (const g of this.config.globals)
|
|
2550
|
+
globals[g.slug] = GLOBAL_OPERATIONS.filter((op) => op !== "publish" || g.drafts === true);
|
|
2551
|
+
const admin = this.views().map((v) => v.id).filter((id) => id.startsWith("page:") || id.startsWith("widget:"));
|
|
2552
|
+
return { collections, globals, admin };
|
|
2553
|
+
}
|
|
2554
|
+
/** Keeps only known collections, globals, operations and pages; `read` comes with the others. */
|
|
2555
|
+
clean(value) {
|
|
2556
|
+
const input = typeof value === "object" && value !== null ? value : {};
|
|
2557
|
+
const pick = (group, allowed2) => {
|
|
2558
|
+
const out = {};
|
|
2559
|
+
if (typeof group !== "object" || group === null) return out;
|
|
2560
|
+
for (const [slug2, ops] of Object.entries(group)) {
|
|
2561
|
+
const valid = allowed2.get(slug2);
|
|
2562
|
+
if (!valid || !Array.isArray(ops)) continue;
|
|
2563
|
+
const picked = valid.filter((op) => ops.includes(op));
|
|
2564
|
+
if (picked.length > 0 && !picked.includes("read")) picked.unshift("read");
|
|
2565
|
+
out[slug2] = picked;
|
|
2566
|
+
}
|
|
2567
|
+
return out;
|
|
2568
|
+
};
|
|
2569
|
+
const collections = new Map(
|
|
2570
|
+
this.config.collections.filter((c) => governed(c.slug)).map((c) => [
|
|
2571
|
+
c.slug,
|
|
2572
|
+
ROLE_OPERATIONS.filter((op) => op !== "publish" || c.drafts === true)
|
|
2573
|
+
])
|
|
2574
|
+
);
|
|
2575
|
+
const globals = new Map(
|
|
2576
|
+
this.config.globals.map((g) => [
|
|
2577
|
+
g.slug,
|
|
2578
|
+
GLOBAL_OPERATIONS.filter((op) => op !== "publish" || g.drafts === true)
|
|
2579
|
+
])
|
|
2580
|
+
);
|
|
2581
|
+
const views = new Set(this.views().map((v) => v.id));
|
|
2582
|
+
const admin = Array.isArray(input.admin) ? [
|
|
2583
|
+
...new Set(
|
|
2584
|
+
input.admin.filter((id) => typeof id === "string" && views.has(id))
|
|
2585
|
+
)
|
|
2586
|
+
] : [];
|
|
2587
|
+
return {
|
|
2588
|
+
collections: pick(input.collections, collections),
|
|
2589
|
+
globals: pick(input.globals, globals),
|
|
2590
|
+
admin
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
cleanName(value) {
|
|
2594
|
+
if (value === void 0 || value === null) return "";
|
|
2595
|
+
if (typeof value !== "string" || value.trim().length > 60)
|
|
2596
|
+
throw new ValidationError(ROLES, [
|
|
2597
|
+
{ field: "name", message: "must be at most 60 characters" }
|
|
2598
|
+
]);
|
|
2599
|
+
return value.trim();
|
|
2600
|
+
}
|
|
2601
|
+
async permissionsOf(key2) {
|
|
2602
|
+
return (await this.load()).get(key2) ?? {};
|
|
2603
|
+
}
|
|
2604
|
+
async load() {
|
|
2605
|
+
if (this.cache && Date.now() - this.cache.at < TTL) return this.cache.byKey;
|
|
2606
|
+
await this.sync();
|
|
2607
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2608
|
+
for (const row of await this.rows())
|
|
2609
|
+
byKey.set(String(row.key), row.permissions ?? {});
|
|
2610
|
+
this.cache = { at: Date.now(), byKey };
|
|
2611
|
+
return byKey;
|
|
2612
|
+
}
|
|
2613
|
+
async rows() {
|
|
2614
|
+
return (await this.db.find({ collection: ROLES, sort: ["id"], limit: 0, page: 1 })).docs;
|
|
2615
|
+
}
|
|
2616
|
+
async row(id) {
|
|
2617
|
+
const row = await this.db.findById({ collection: ROLES, id });
|
|
2618
|
+
if (!row) throw new NotFoundError(ROLES, id);
|
|
2619
|
+
return row;
|
|
2620
|
+
}
|
|
2621
|
+
users(key2) {
|
|
2622
|
+
return this.db.count({ collection: USERS, where: { role: { equals: key2 } } });
|
|
2623
|
+
}
|
|
2624
|
+
async insert(key2, name, permissions, author) {
|
|
2625
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2626
|
+
const row = await this.db.create({
|
|
2627
|
+
collection: ROLES,
|
|
2628
|
+
data: { key: key2, name, permissions, createdAt: now, updatedAt: now }
|
|
2629
|
+
});
|
|
2630
|
+
await this.record(row, author);
|
|
2631
|
+
this.invalidate();
|
|
2632
|
+
return row;
|
|
2633
|
+
}
|
|
2634
|
+
/** Keeps the role as saved, for its history. */
|
|
2635
|
+
async record(row, author) {
|
|
2636
|
+
await this.versions.save({
|
|
2637
|
+
parent: ROLES,
|
|
2638
|
+
doc: row.id,
|
|
2639
|
+
status: null,
|
|
2640
|
+
snapshot: { name: row.name ?? "", permissions: row.permissions ?? {} },
|
|
2641
|
+
author,
|
|
2642
|
+
max: HISTORY
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
toRole(row, users) {
|
|
2646
|
+
const key2 = String(row.key);
|
|
2647
|
+
return {
|
|
2648
|
+
id: row.id,
|
|
2649
|
+
key: key2,
|
|
2650
|
+
name: typeof row.name === "string" ? row.name : "",
|
|
2651
|
+
system: this.config.auth.roles.includes(key2),
|
|
2652
|
+
users,
|
|
2653
|
+
permissions: row.permissions ?? {},
|
|
2654
|
+
updatedAt: String(row.updatedAt)
|
|
2655
|
+
};
|
|
2656
|
+
}
|
|
2657
|
+
};
|
|
2658
|
+
function rank(role, config) {
|
|
2659
|
+
const index = config.auth.roles.indexOf(role.key);
|
|
2660
|
+
return index === -1 ? config.auth.roles.length : index;
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2171
2663
|
// src/webhooks.ts
|
|
2172
2664
|
import { createHmac as createHmac2, randomUUID as randomUUID2 } from "crypto";
|
|
2173
2665
|
var WEBHOOK_EVENTS = [
|
|
@@ -2359,6 +2851,7 @@ var RESERVED_SLUGS = /* @__PURE__ */ new Set([
|
|
|
2359
2851
|
"webhook-deliveries",
|
|
2360
2852
|
"email-deliveries",
|
|
2361
2853
|
"database-backups",
|
|
2854
|
+
"user-roles",
|
|
2362
2855
|
"api-keys",
|
|
2363
2856
|
"jobs",
|
|
2364
2857
|
"migrations",
|
|
@@ -2563,6 +3056,7 @@ function validateAdminViews(config, add) {
|
|
|
2563
3056
|
"e.g. dashboard: [{ component: 'ecms-stats-widget' }]"
|
|
2564
3057
|
);
|
|
2565
3058
|
} else {
|
|
3059
|
+
const tags = /* @__PURE__ */ new Set();
|
|
2566
3060
|
widgets2.forEach((widget, i) => {
|
|
2567
3061
|
const path = `admin.dashboard[${i}]`;
|
|
2568
3062
|
if (typeof widget !== "object" || widget === null) {
|
|
@@ -2573,6 +3067,19 @@ function validateAdminViews(config, add) {
|
|
|
2573
3067
|
validateComponent(w.component, `${path}.component`, add);
|
|
2574
3068
|
if (w.width !== void 0 && w.width !== "half" && w.width !== "full")
|
|
2575
3069
|
add(`${path}.width`, 'must be "half" or "full"');
|
|
3070
|
+
if (w.label !== void 0 && !(typeof w.label === "string" && w.label.trim() !== "") && !(typeof w.label === "object" && w.label !== null && !Array.isArray(w.label)))
|
|
3071
|
+
add(`${path}.label`, "must be a string or { en, th }");
|
|
3072
|
+
const component = w.component;
|
|
3073
|
+
const tag = typeof component === "string" ? component : component?.tag;
|
|
3074
|
+
if (config.auth?.rbac === true && typeof tag === "string") {
|
|
3075
|
+
if (tags.has(tag))
|
|
3076
|
+
add(
|
|
3077
|
+
`${path}.component`,
|
|
3078
|
+
`another panel uses "${tag}"`,
|
|
3079
|
+
"with auth.rbac, roles are given panels by their tag: use one tag per panel"
|
|
3080
|
+
);
|
|
3081
|
+
tags.add(tag);
|
|
3082
|
+
}
|
|
2576
3083
|
access(w.access, path);
|
|
2577
3084
|
});
|
|
2578
3085
|
}
|
|
@@ -2586,6 +3093,8 @@ function validateIcon(container, path, add) {
|
|
|
2586
3093
|
function validateAuth(config, add) {
|
|
2587
3094
|
const auth = config.auth;
|
|
2588
3095
|
if (auth === void 0) return;
|
|
3096
|
+
if (auth.rbac !== void 0 && typeof auth.rbac !== "boolean")
|
|
3097
|
+
add("auth.rbac", "must be true or false");
|
|
2589
3098
|
if (auth.roles !== void 0) {
|
|
2590
3099
|
const roles = auth.roles;
|
|
2591
3100
|
if (!Array.isArray(roles) || roles.some((r) => typeof r !== "string" || r === "")) {
|
|
@@ -3280,6 +3789,7 @@ async function resolveConfig(input) {
|
|
|
3280
3789
|
},
|
|
3281
3790
|
auth: {
|
|
3282
3791
|
roles: config.auth?.roles ?? DEFAULT_ROLES,
|
|
3792
|
+
rbac: config.auth?.rbac === true,
|
|
3283
3793
|
tokenExpiration: config.auth?.tokenExpiration ?? DEFAULT_TOKEN_EXPIRATION,
|
|
3284
3794
|
maxLoginAttempts: config.auth?.maxLoginAttempts ?? 5,
|
|
3285
3795
|
lockWindow: config.auth?.lockWindow ?? 15 * 60,
|
|
@@ -3291,12 +3801,14 @@ async function resolveConfig(input) {
|
|
|
3291
3801
|
collections: [
|
|
3292
3802
|
...config.collections ?? [],
|
|
3293
3803
|
...internalCollections,
|
|
3294
|
-
|
|
3804
|
+
// Roles keep their history as versions.
|
|
3805
|
+
...usesVersions(config) || config.auth?.rbac ? [versionsCollection] : [],
|
|
3295
3806
|
...usesSchedule(config) ? [scheduledJobsCollection] : [],
|
|
3296
3807
|
...(config.webhooks?.length ?? 0) > 0 ? [webhookDeliveriesCollection] : [],
|
|
3297
3808
|
...config.apiKeys ? [apiKeysCollection] : [],
|
|
3298
3809
|
...config.email ? [emailDeliveriesCollection] : [],
|
|
3299
|
-
databaseBackupsCollection
|
|
3810
|
+
databaseBackupsCollection,
|
|
3811
|
+
...config.auth?.rbac ? [rolesCollection] : []
|
|
3300
3812
|
],
|
|
3301
3813
|
globals: config.globals ?? [],
|
|
3302
3814
|
endpoints: config.endpoints ?? [],
|
|
@@ -3841,137 +4353,6 @@ function read(response, maxBytes, deadline) {
|
|
|
3841
4353
|
});
|
|
3842
4354
|
}
|
|
3843
4355
|
|
|
3844
|
-
// src/versions.ts
|
|
3845
|
-
var DEFAULT_MAX_VERSIONS = 50;
|
|
3846
|
-
function versionLimit(config) {
|
|
3847
|
-
const versions = config.versions;
|
|
3848
|
-
if (!versions) return void 0;
|
|
3849
|
-
return versions === true ? DEFAULT_MAX_VERSIONS : versions.max ?? DEFAULT_MAX_VERSIONS;
|
|
3850
|
-
}
|
|
3851
|
-
var collectionParent = (slug2) => slug2;
|
|
3852
|
-
var globalParent = (slug2) => `global:${slug2}`;
|
|
3853
|
-
function snapshotOf(fields, doc) {
|
|
3854
|
-
const hidden = new Set(fields.filter((f) => f.hidden).map((f) => f.name));
|
|
3855
|
-
const snapshot = {};
|
|
3856
|
-
for (const [key2, value] of Object.entries(doc)) {
|
|
3857
|
-
if (key2 === "id" || key2 === "createdAt" || key2 === "updatedAt" || hidden.has(key2)) continue;
|
|
3858
|
-
snapshot[key2] = value;
|
|
3859
|
-
}
|
|
3860
|
-
return snapshot;
|
|
3861
|
-
}
|
|
3862
|
-
var toSummary = (row) => ({
|
|
3863
|
-
id: row.id,
|
|
3864
|
-
status: row.status ?? null,
|
|
3865
|
-
latest: row.latest === true,
|
|
3866
|
-
author: row.author ?? null,
|
|
3867
|
-
createdAt: String(row.createdAt)
|
|
3868
|
-
});
|
|
3869
|
-
var VersionStore = class {
|
|
3870
|
-
constructor(db) {
|
|
3871
|
-
this.db = db;
|
|
3872
|
-
}
|
|
3873
|
-
db;
|
|
3874
|
-
/** Saves a new latest version and deletes the oldest beyond `max`. */
|
|
3875
|
-
async save(args) {
|
|
3876
|
-
const { parent, doc } = args;
|
|
3877
|
-
const previous = await this.db.find({
|
|
3878
|
-
collection: VERSIONS,
|
|
3879
|
-
where: { and: [...this.of(parent, doc), { latest: { equals: true } }] },
|
|
3880
|
-
sort: [],
|
|
3881
|
-
limit: 0,
|
|
3882
|
-
page: 1
|
|
3883
|
-
});
|
|
3884
|
-
for (const row of previous.docs) {
|
|
3885
|
-
await this.db.update({ collection: VERSIONS, id: row.id, data: { ...row, latest: false } });
|
|
3886
|
-
}
|
|
3887
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3888
|
-
const created = await this.db.create({
|
|
3889
|
-
collection: VERSIONS,
|
|
3890
|
-
data: {
|
|
3891
|
-
parent,
|
|
3892
|
-
doc,
|
|
3893
|
-
status: args.status,
|
|
3894
|
-
latest: true,
|
|
3895
|
-
author: args.author,
|
|
3896
|
-
snapshot: args.snapshot,
|
|
3897
|
-
createdAt: now,
|
|
3898
|
-
updatedAt: now
|
|
3899
|
-
}
|
|
3900
|
-
});
|
|
3901
|
-
await this.prune(parent, doc, args.max);
|
|
3902
|
-
return toSummary(created);
|
|
3903
|
-
}
|
|
3904
|
-
/** The latest version of each document, keyed by document id. */
|
|
3905
|
-
async latest(parent, docs) {
|
|
3906
|
-
const result = /* @__PURE__ */ new Map();
|
|
3907
|
-
if (docs.length === 0) return result;
|
|
3908
|
-
const rows = await this.db.find({
|
|
3909
|
-
collection: VERSIONS,
|
|
3910
|
-
where: {
|
|
3911
|
-
and: [
|
|
3912
|
-
{ parent: { equals: parent } },
|
|
3913
|
-
{ doc: { in: [...docs] } },
|
|
3914
|
-
{ latest: { equals: true } }
|
|
3915
|
-
]
|
|
3916
|
-
},
|
|
3917
|
-
sort: [],
|
|
3918
|
-
limit: 0,
|
|
3919
|
-
page: 1
|
|
3920
|
-
});
|
|
3921
|
-
for (const row of rows.docs) result.set(String(row.doc), this.toVersion(row));
|
|
3922
|
-
return result;
|
|
3923
|
-
}
|
|
3924
|
-
async list(parent, doc, limit, page) {
|
|
3925
|
-
const found = await this.db.find({
|
|
3926
|
-
collection: VERSIONS,
|
|
3927
|
-
where: { and: this.of(parent, doc) },
|
|
3928
|
-
sort: ["-id"],
|
|
3929
|
-
limit,
|
|
3930
|
-
page
|
|
3931
|
-
});
|
|
3932
|
-
return { ...found, docs: found.docs.map(toSummary) };
|
|
3933
|
-
}
|
|
3934
|
-
async get(parent, doc, id) {
|
|
3935
|
-
const found = await this.db.find({
|
|
3936
|
-
collection: VERSIONS,
|
|
3937
|
-
where: { and: [...this.of(parent, doc), { id: { equals: id } }] },
|
|
3938
|
-
sort: [],
|
|
3939
|
-
limit: 1,
|
|
3940
|
-
page: 1
|
|
3941
|
-
});
|
|
3942
|
-
const row = found.docs[0];
|
|
3943
|
-
return row ? this.toVersion(row) : null;
|
|
3944
|
-
}
|
|
3945
|
-
async deleteAll(parent, doc) {
|
|
3946
|
-
const rows = await this.db.find({
|
|
3947
|
-
collection: VERSIONS,
|
|
3948
|
-
where: { and: this.of(parent, doc) },
|
|
3949
|
-
sort: [],
|
|
3950
|
-
limit: 0,
|
|
3951
|
-
page: 1
|
|
3952
|
-
});
|
|
3953
|
-
for (const row of rows.docs) await this.db.delete({ collection: VERSIONS, id: row.id });
|
|
3954
|
-
}
|
|
3955
|
-
async prune(parent, doc, max) {
|
|
3956
|
-
const rows = await this.db.find({
|
|
3957
|
-
collection: VERSIONS,
|
|
3958
|
-
where: { and: this.of(parent, doc) },
|
|
3959
|
-
sort: ["-id"],
|
|
3960
|
-
limit: 0,
|
|
3961
|
-
page: 1
|
|
3962
|
-
});
|
|
3963
|
-
for (const row of rows.docs.slice(max)) {
|
|
3964
|
-
await this.db.delete({ collection: VERSIONS, id: row.id });
|
|
3965
|
-
}
|
|
3966
|
-
}
|
|
3967
|
-
of(parent, doc) {
|
|
3968
|
-
return [{ parent: { equals: parent } }, { doc: { equals: doc } }];
|
|
3969
|
-
}
|
|
3970
|
-
toVersion(row) {
|
|
3971
|
-
return { ...toSummary(row), data: row.snapshot ?? {} };
|
|
3972
|
-
}
|
|
3973
|
-
};
|
|
3974
|
-
|
|
3975
4356
|
// src/local-api.ts
|
|
3976
4357
|
async function createEasyCMS(config, options = {}) {
|
|
3977
4358
|
const resolved3 = await resolveConfig(config);
|
|
@@ -4009,6 +4390,8 @@ var EasyCMS = class {
|
|
|
4009
4390
|
storage;
|
|
4010
4391
|
/** Project root: relative paths in the config (e.g. `admin.modules`) start here. */
|
|
4011
4392
|
cwd;
|
|
4393
|
+
/** Roles and their permissions (`auth.rbac`, Settings → Roles). */
|
|
4394
|
+
roles;
|
|
4012
4395
|
versions;
|
|
4013
4396
|
webhooks;
|
|
4014
4397
|
mailer;
|
|
@@ -4019,6 +4402,7 @@ var EasyCMS = class {
|
|
|
4019
4402
|
this.storage = storage;
|
|
4020
4403
|
this.cwd = cwd;
|
|
4021
4404
|
this.versions = new VersionStore(db);
|
|
4405
|
+
this.roles = new Roles(config, db);
|
|
4022
4406
|
this.webhooks = new Webhooks(
|
|
4023
4407
|
config.webhooks ?? [],
|
|
4024
4408
|
logger,
|
|
@@ -4109,7 +4493,7 @@ var EasyCMS = class {
|
|
|
4109
4493
|
const config = this.collection(MEDIA);
|
|
4110
4494
|
const guard = guardOf(options);
|
|
4111
4495
|
if (guard.enforce) {
|
|
4112
|
-
this.
|
|
4496
|
+
await this.checkGrant(guard, { collection: MEDIA }, "create");
|
|
4113
4497
|
const allowed2 = await evaluateAccess(config.access?.create, { user: guard.user, data });
|
|
4114
4498
|
if (allowed2 !== true) throw deny(guard.user);
|
|
4115
4499
|
}
|
|
@@ -4169,7 +4553,7 @@ var EasyCMS = class {
|
|
|
4169
4553
|
const guard = guardOf(options);
|
|
4170
4554
|
const fromURL = this.config.upload.fromURL;
|
|
4171
4555
|
if (guard.enforce) {
|
|
4172
|
-
this.
|
|
4556
|
+
await this.checkGrant(guard, { collection: MEDIA }, "create");
|
|
4173
4557
|
const allowed2 = await evaluateAccess(config.access?.create, { user: guard.user, data });
|
|
4174
4558
|
if (allowed2 !== true) throw deny(guard.user);
|
|
4175
4559
|
if (!fromURL)
|
|
@@ -4324,7 +4708,7 @@ var EasyCMS = class {
|
|
|
4324
4708
|
let parsed;
|
|
4325
4709
|
if (id === null) {
|
|
4326
4710
|
if (guard.enforce) {
|
|
4327
|
-
this.
|
|
4711
|
+
await this.checkGrant(guard, { collection: collection2 }, "create");
|
|
4328
4712
|
const allowed2 = await evaluateAccess(config.access?.create, { user: guard.user, data: raw });
|
|
4329
4713
|
if (allowed2 !== true) throw deny(guard.user);
|
|
4330
4714
|
}
|
|
@@ -4444,7 +4828,7 @@ var EasyCMS = class {
|
|
|
4444
4828
|
if (!existing || parsed === void 0) throw new NotFoundError(collection2, id);
|
|
4445
4829
|
await this.checkDocumentAccess(config, "update", guard, parsed, raw);
|
|
4446
4830
|
if (config.drafts && (mode === "unpublish" || raw.status === "published"))
|
|
4447
|
-
this.
|
|
4831
|
+
await this.checkGrant(guard, { collection: collection2 }, "publish");
|
|
4448
4832
|
const [drafted] = await this.withDrafts(config, [existing]);
|
|
4449
4833
|
const live = options.live === true && mode === "save";
|
|
4450
4834
|
const current = live ? existing : drafted;
|
|
@@ -4639,7 +5023,7 @@ var EasyCMS = class {
|
|
|
4639
5023
|
const guard = guardOf(options);
|
|
4640
5024
|
await this.checkGlobalAccess(config, "update", guard);
|
|
4641
5025
|
if (config.drafts && (mode === "unpublish" || raw.status === "published"))
|
|
4642
|
-
this.
|
|
5026
|
+
await this.checkGrant(guard, { global: slug2 }, "publish");
|
|
4643
5027
|
const input = await filterInput(
|
|
4644
5028
|
config.fields,
|
|
4645
5029
|
raw,
|
|
@@ -4928,14 +5312,14 @@ var EasyCMS = class {
|
|
|
4928
5312
|
const existing = parsed === void 0 ? null : await this.db.findById({ collection: collection2, id: parsed });
|
|
4929
5313
|
if (!existing || parsed === void 0) throw new NotFoundError(collection2, id);
|
|
4930
5314
|
await this.checkDocumentAccess(config, "update", guardOf(options), parsed, void 0);
|
|
4931
|
-
this.
|
|
5315
|
+
await this.checkGrant(guardOf(options), { collection: collection2 }, "publish");
|
|
4932
5316
|
return { config, parsed };
|
|
4933
5317
|
}
|
|
4934
5318
|
async globalScheduleTarget(slug2, options) {
|
|
4935
5319
|
const config = this.global(slug2);
|
|
4936
5320
|
if (!config.schedule) throw new QueryError(`"${slug2}" has no schedule`);
|
|
4937
5321
|
await this.checkGlobalAccess(config, "update", guardOf(options));
|
|
4938
|
-
this.
|
|
5322
|
+
await this.checkGrant(guardOf(options), { global: slug2 }, "publish");
|
|
4939
5323
|
}
|
|
4940
5324
|
async addJob(parent, doc, job, options) {
|
|
4941
5325
|
const runAt = new Date(job.at);
|
|
@@ -4981,20 +5365,21 @@ var EasyCMS = class {
|
|
|
4981
5365
|
page: 1
|
|
4982
5366
|
});
|
|
4983
5367
|
const allowed2 = /* @__PURE__ */ new Map();
|
|
4984
|
-
const
|
|
5368
|
+
const may2 = async (parent) => {
|
|
4985
5369
|
if (!guard.enforce) return true;
|
|
4986
5370
|
const cached = allowed2.get(parent);
|
|
4987
5371
|
if (cached !== void 0) return cached;
|
|
4988
5372
|
const config = parent.startsWith("global:") ? this.config.globals.find((g) => g.slug === parent.slice("global:".length)) : this.config.collections.find((c) => c.slug === parent);
|
|
4989
5373
|
const access = config ? await evaluateAccess(config.access?.update, { user: guard.user }) : false;
|
|
4990
|
-
const
|
|
5374
|
+
const target = parent.startsWith("global:") ? { global: parent.slice("global:".length) } : { collection: parent };
|
|
5375
|
+
const result = access !== false && await this.roles.allows(guard.user, target, "update") !== false;
|
|
4991
5376
|
allowed2.set(parent, result);
|
|
4992
5377
|
return result;
|
|
4993
5378
|
};
|
|
4994
5379
|
const jobs = [];
|
|
4995
5380
|
for (const row of rows.docs) {
|
|
4996
5381
|
const parent = String(row.parent);
|
|
4997
|
-
if (!await
|
|
5382
|
+
if (!await may2(parent)) continue;
|
|
4998
5383
|
const target = parent.startsWith("global:") ? { global: parent.slice("global:".length) } : { collection: parent };
|
|
4999
5384
|
jobs.push({ ...toJob(row), ...target, doc: row.doc });
|
|
5000
5385
|
if (jobs.length === limit) break;
|
|
@@ -5079,28 +5464,36 @@ var EasyCMS = class {
|
|
|
5079
5464
|
if (!config) throw new QueryError(`Unknown global "${slug2}"`);
|
|
5080
5465
|
return config;
|
|
5081
5466
|
}
|
|
5082
|
-
/**
|
|
5083
|
-
|
|
5084
|
-
|
|
5467
|
+
/**
|
|
5468
|
+
* Refuses what a request's API key (see `ApiKeyPermissions`) or the user's role (`auth.rbac`)
|
|
5469
|
+
* does not allow. Returns a constraint when the role allows only the user's own account.
|
|
5470
|
+
*/
|
|
5471
|
+
async checkGrant(guard, target, operation) {
|
|
5472
|
+
if (!guard.enforce) return true;
|
|
5085
5473
|
const name = "collection" in target ? target.collection : target.global;
|
|
5086
|
-
|
|
5474
|
+
if (!keyAllows(guard.user, target, operation))
|
|
5475
|
+
throw new ForbiddenError(`This API key may not ${operation} "${name}"`);
|
|
5476
|
+
const role = await this.roles.allows(guard.user, target, operation);
|
|
5477
|
+
if (role === false) throw deny(guard.user);
|
|
5478
|
+
return role;
|
|
5087
5479
|
}
|
|
5088
5480
|
/** The query constraint read access adds, or `undefined` when there is none. Throws when denied. */
|
|
5089
5481
|
async readWhere(config, guard, where) {
|
|
5090
5482
|
if (!guard.enforce) return where;
|
|
5091
|
-
this.
|
|
5483
|
+
const own = await this.checkGrant(guard, { collection: config.slug }, "read");
|
|
5092
5484
|
const access = await evaluateAccess(config.access?.read, { user: guard.user });
|
|
5093
5485
|
if (access === false) throw deny(guard.user);
|
|
5094
|
-
return andWhere(where, access);
|
|
5486
|
+
return andWhere(andWhere(where, access), own);
|
|
5095
5487
|
}
|
|
5096
5488
|
async checkDocumentAccess(config, operation, guard, id, data) {
|
|
5097
5489
|
if (!guard.enforce) return;
|
|
5098
|
-
this.
|
|
5099
|
-
const
|
|
5490
|
+
const own = await this.checkGrant(guard, { collection: config.slug }, operation);
|
|
5491
|
+
const allowed2 = await evaluateAccess(config.access?.[operation], {
|
|
5100
5492
|
user: guard.user,
|
|
5101
5493
|
id,
|
|
5102
5494
|
...data ? { data } : {}
|
|
5103
5495
|
});
|
|
5496
|
+
const access = allowed2 === false ? false : andWhere(own === true ? void 0 : own, allowed2) ?? true;
|
|
5104
5497
|
if (access === true) return;
|
|
5105
5498
|
if (access !== false) {
|
|
5106
5499
|
const matches2 = await this.db.count({
|
|
@@ -5113,7 +5506,7 @@ var EasyCMS = class {
|
|
|
5113
5506
|
}
|
|
5114
5507
|
async checkGlobalAccess(config, operation, guard) {
|
|
5115
5508
|
if (!guard.enforce) return;
|
|
5116
|
-
this.
|
|
5509
|
+
await this.checkGrant(guard, { global: config.slug }, operation);
|
|
5117
5510
|
const access = await evaluateAccess(config.access?.[operation], { user: guard.user });
|
|
5118
5511
|
if (typeof access === "object")
|
|
5119
5512
|
throw new QueryError(`${operation} access of global "${config.slug}" must return a boolean`);
|
|
@@ -5167,9 +5560,9 @@ var EasyCMS = class {
|
|
|
5167
5560
|
const guard = guardOf(options);
|
|
5168
5561
|
const collection2 = config.slug;
|
|
5169
5562
|
if (guard.enforce) {
|
|
5170
|
-
this.
|
|
5563
|
+
await this.checkGrant(guard, { collection: collection2 }, "create");
|
|
5171
5564
|
if (config.drafts && raw.status === "published")
|
|
5172
|
-
this.
|
|
5565
|
+
await this.checkGrant(guard, { collection: collection2 }, "publish");
|
|
5173
5566
|
const allowed2 = await evaluateAccess(config.access?.create, { user: guard.user, data: raw });
|
|
5174
5567
|
if (typeof allowed2 === "object")
|
|
5175
5568
|
throw new QueryError(`create access of "${collection2}" must return a boolean`);
|
|
@@ -5388,6 +5781,8 @@ var EasyCMS = class {
|
|
|
5388
5781
|
}
|
|
5389
5782
|
clean2.status = status;
|
|
5390
5783
|
}
|
|
5784
|
+
if (config.slug === USERS && this.roles.enabled && typeof clean2.role === "string" && !await this.roles.exists(clean2.role))
|
|
5785
|
+
errors.push({ field: "role", message: `there is no role "${clean2.role}"` });
|
|
5391
5786
|
const isCollection = this.config.collections.includes(config);
|
|
5392
5787
|
if (isCollection && errors.length === 0) {
|
|
5393
5788
|
await this.makeSlugsUnique(config, clean2, selfId);
|
|
@@ -5474,11 +5869,11 @@ var EasyCMS = class {
|
|
|
5474
5869
|
return await this.db.count({ collection: collection2, where }) > 0;
|
|
5475
5870
|
}
|
|
5476
5871
|
/** Each reference is one its relationship's `filterOptions` allow. */
|
|
5477
|
-
async checkFilterOptions(
|
|
5872
|
+
async checkFilterOptions(references2, selfId, user) {
|
|
5478
5873
|
const errors = [];
|
|
5479
5874
|
const cms = this;
|
|
5480
5875
|
const allowed2 = /* @__PURE__ */ new Map();
|
|
5481
|
-
for (const ref of
|
|
5876
|
+
for (const ref of references2) {
|
|
5482
5877
|
const filter = ref.filterOptions;
|
|
5483
5878
|
if (!filter) continue;
|
|
5484
5879
|
if (!allowed2.has(filter)) allowed2.set(filter, await filter({ id: selfId, user, cms }));
|
|
@@ -5496,9 +5891,9 @@ var EasyCMS = class {
|
|
|
5496
5891
|
}
|
|
5497
5892
|
return errors;
|
|
5498
5893
|
}
|
|
5499
|
-
async checkReferences(
|
|
5894
|
+
async checkReferences(references2) {
|
|
5500
5895
|
const byCollection = /* @__PURE__ */ new Map();
|
|
5501
|
-
for (const ref of
|
|
5896
|
+
for (const ref of references2) {
|
|
5502
5897
|
if (!this.config.collections.some((c) => c.slug === ref.collection)) continue;
|
|
5503
5898
|
byCollection.set(ref.collection, [...byCollection.get(ref.collection) ?? [], ref]);
|
|
5504
5899
|
}
|
|
@@ -5897,23 +6292,36 @@ async function serializeFields(fields, update2, localized) {
|
|
|
5897
6292
|
}
|
|
5898
6293
|
return out;
|
|
5899
6294
|
}
|
|
5900
|
-
async function
|
|
6295
|
+
async function may(cms, access, user, target, operation) {
|
|
6296
|
+
return await allowed(access, user) && await cms.roles.allows(user, target, operation) !== false;
|
|
6297
|
+
}
|
|
6298
|
+
async function collection(cms, config, user, localized, roles) {
|
|
6299
|
+
const target = { collection: config.slug };
|
|
6300
|
+
const fields = await serializeFields(
|
|
6301
|
+
config.fields,
|
|
6302
|
+
new FieldAccessChecker("update", { user }),
|
|
6303
|
+
localized
|
|
6304
|
+
);
|
|
6305
|
+
if (config.slug === USERS && cms.roles.enabled) {
|
|
6306
|
+
const role = fields.find((f) => f.name === "role");
|
|
6307
|
+
if (role) {
|
|
6308
|
+
role.type = "select";
|
|
6309
|
+
role.options = roles.map((r) => ({ label: r.name || r.key, value: r.key }));
|
|
6310
|
+
}
|
|
6311
|
+
}
|
|
5901
6312
|
const result = {
|
|
5902
6313
|
slug: config.slug,
|
|
5903
6314
|
drafts: config.drafts === true,
|
|
5904
6315
|
versions: Boolean(config.versions),
|
|
5905
6316
|
preview: typeof config.preview === "function",
|
|
5906
6317
|
schedule: config.schedule === true,
|
|
5907
|
-
fields
|
|
5908
|
-
config.fields,
|
|
5909
|
-
new FieldAccessChecker("update", { user }),
|
|
5910
|
-
localized
|
|
5911
|
-
),
|
|
6318
|
+
fields,
|
|
5912
6319
|
permissions: {
|
|
5913
|
-
read: await
|
|
5914
|
-
create: await
|
|
5915
|
-
update: await
|
|
5916
|
-
delete: await
|
|
6320
|
+
read: await may(cms, config.access?.read, user, target, "read"),
|
|
6321
|
+
create: await may(cms, config.access?.create, user, target, "create"),
|
|
6322
|
+
update: await may(cms, config.access?.update, user, target, "update"),
|
|
6323
|
+
delete: await may(cms, config.access?.delete, user, target, "delete"),
|
|
6324
|
+
publish: config.drafts === true && await may(cms, config.access?.update, user, target, "publish")
|
|
5917
6325
|
}
|
|
5918
6326
|
};
|
|
5919
6327
|
if (config.labels) result.labels = config.labels;
|
|
@@ -5927,7 +6335,8 @@ async function collection(config, user, localized) {
|
|
|
5927
6335
|
result.editIn = "drawer";
|
|
5928
6336
|
return result;
|
|
5929
6337
|
}
|
|
5930
|
-
async function global(config, user, localized) {
|
|
6338
|
+
async function global(cms, config, user, localized) {
|
|
6339
|
+
const target = { global: config.slug };
|
|
5931
6340
|
const result = {
|
|
5932
6341
|
slug: config.slug,
|
|
5933
6342
|
drafts: config.drafts === true,
|
|
@@ -5940,8 +6349,9 @@ async function global(config, user, localized) {
|
|
|
5940
6349
|
localized
|
|
5941
6350
|
),
|
|
5942
6351
|
permissions: {
|
|
5943
|
-
read: await
|
|
5944
|
-
update: await
|
|
6352
|
+
read: await may(cms, config.access?.read, user, target, "read"),
|
|
6353
|
+
update: await may(cms, config.access?.update, user, target, "update"),
|
|
6354
|
+
publish: config.drafts === true && await may(cms, config.access?.update, user, target, "publish")
|
|
5945
6355
|
}
|
|
5946
6356
|
};
|
|
5947
6357
|
if (config.label !== void 0) result.label = config.label;
|
|
@@ -5953,16 +6363,31 @@ async function adminSchema(cms, user, origin) {
|
|
|
5953
6363
|
const collections = cms.config.collections.filter((c) => !INTERNAL_COLLECTIONS.has(c.slug));
|
|
5954
6364
|
const localization = cms.config.localization;
|
|
5955
6365
|
const localized = localization !== null;
|
|
6366
|
+
const admin = user.role === "admin" && !user.apiKey;
|
|
6367
|
+
const deliveries = cms.config.collections.some(
|
|
6368
|
+
(c) => c.slug === WEBHOOK_DELIVERIES || c.slug === EMAIL_DELIVERIES
|
|
6369
|
+
);
|
|
6370
|
+
const views = {
|
|
6371
|
+
status: await cms.roles.canView(user, "status"),
|
|
6372
|
+
deliveries: deliveries && await cms.roles.canView(user, "deliveries"),
|
|
6373
|
+
backups: admin,
|
|
6374
|
+
email: admin,
|
|
6375
|
+
roles: admin && cms.roles.enabled
|
|
6376
|
+
};
|
|
6377
|
+
const roles = await cms.roles.options();
|
|
5956
6378
|
return {
|
|
5957
6379
|
locale: cms.config.admin.locale,
|
|
5958
6380
|
passwordLinks: cms.auth.canSendPasswordLinks(origin),
|
|
5959
6381
|
menu: [...cms.config.admin.menu],
|
|
5960
6382
|
localization: localization ? { locales: [...localization.locales], defaultLocale: localization.defaultLocale } : null,
|
|
5961
|
-
collections: await Promise.all(
|
|
5962
|
-
|
|
6383
|
+
collections: await Promise.all(
|
|
6384
|
+
collections.map((c) => collection(cms, c, user, localized, roles))
|
|
6385
|
+
),
|
|
6386
|
+
globals: await Promise.all(cms.config.globals.map((g) => global(cms, g, user, localized))),
|
|
5963
6387
|
modules: adminModuleUrls(cms),
|
|
5964
6388
|
uploadFromURL: cms.config.upload.fromURL !== void 0,
|
|
5965
|
-
|
|
6389
|
+
views,
|
|
6390
|
+
...views.deliveries ? {
|
|
5966
6391
|
deliveries: {
|
|
5967
6392
|
webhook: cms.config.collections.some((c) => c.slug === WEBHOOK_DELIVERIES),
|
|
5968
6393
|
email: cms.config.collections.some((c) => c.slug === EMAIL_DELIVERIES)
|
|
@@ -5983,7 +6408,8 @@ async function shown(access, user) {
|
|
|
5983
6408
|
async function pages(cms, user) {
|
|
5984
6409
|
const out = [];
|
|
5985
6410
|
for (const page of cms.config.admin.pages) {
|
|
5986
|
-
if (!await shown(page.access, user))
|
|
6411
|
+
if (!await shown(page.access, user) || !await cms.roles.canView(user, `page:${page.path}`))
|
|
6412
|
+
continue;
|
|
5987
6413
|
out.push({
|
|
5988
6414
|
path: page.path,
|
|
5989
6415
|
label: page.label,
|
|
@@ -5997,14 +6423,16 @@ async function pages(cms, user) {
|
|
|
5997
6423
|
async function widgets(cms, user) {
|
|
5998
6424
|
const out = [];
|
|
5999
6425
|
for (const widget of cms.config.admin.dashboard) {
|
|
6000
|
-
|
|
6426
|
+
const tag = typeof widget.component === "string" ? widget.component : widget.component.tag;
|
|
6427
|
+
if (!await shown(widget.access, user) || !await cms.roles.canView(user, `widget:${tag}`))
|
|
6428
|
+
continue;
|
|
6001
6429
|
out.push({ component: componentRef(widget.component), width: widget.width ?? "half" });
|
|
6002
6430
|
}
|
|
6003
6431
|
return out;
|
|
6004
6432
|
}
|
|
6005
6433
|
|
|
6006
6434
|
// src/version.ts
|
|
6007
|
-
var VERSION = true ? "0.
|
|
6435
|
+
var VERSION = true ? "0.33.0" : "0.0.0-dev";
|
|
6008
6436
|
|
|
6009
6437
|
// src/rest/admin-status.ts
|
|
6010
6438
|
var DAY = 864e5;
|
|
@@ -6495,9 +6923,36 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
6495
6923
|
}
|
|
6496
6924
|
throw new HttpError("Not found", 404);
|
|
6497
6925
|
}
|
|
6498
|
-
if (first === "admin" && second === "
|
|
6926
|
+
if (first === "admin" && second === "roles") {
|
|
6499
6927
|
if (!ctx.user) throw new UnauthorizedError();
|
|
6500
6928
|
if (ctx.user.role !== "admin" || ctx.user.apiKey) throw new ForbiddenError();
|
|
6929
|
+
if (!cms.roles.enabled) throw new HttpError("Not found", 404);
|
|
6930
|
+
const [, , id2, action, extra] = segments;
|
|
6931
|
+
if (extra !== void 0) throw new HttpError("Not found", 404);
|
|
6932
|
+
if (id2 === void 0) {
|
|
6933
|
+
if (method === "GET") return { body: await cms.roles.list() };
|
|
6934
|
+
if (method === "POST")
|
|
6935
|
+
return { status: 201, body: await cms.roles.create(await readJson(ctx.request), ctx.user) };
|
|
6936
|
+
throw methodNotAllowed(ctx, "GET, POST");
|
|
6937
|
+
}
|
|
6938
|
+
const parsed = parseId(id2);
|
|
6939
|
+
if (parsed === void 0) throw new HttpError("Not found", 404);
|
|
6940
|
+
if (action === "history") {
|
|
6941
|
+
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
6942
|
+
return { body: await cms.roles.history(parsed) };
|
|
6943
|
+
}
|
|
6944
|
+
if (action !== void 0) throw new HttpError("Not found", 404);
|
|
6945
|
+
if (method === "PATCH")
|
|
6946
|
+
return { body: await cms.roles.update(parsed, await readJson(ctx.request), ctx.user) };
|
|
6947
|
+
if (method === "DELETE") {
|
|
6948
|
+
await cms.roles.delete(parsed);
|
|
6949
|
+
return { body: { deleted: 1 } };
|
|
6950
|
+
}
|
|
6951
|
+
throw methodNotAllowed(ctx, "PATCH, DELETE");
|
|
6952
|
+
}
|
|
6953
|
+
if (first === "admin" && second === "deliveries") {
|
|
6954
|
+
if (!ctx.user) throw new UnauthorizedError();
|
|
6955
|
+
if (!await cms.roles.canView(ctx.user, "deliveries")) throw new ForbiddenError();
|
|
6501
6956
|
const [, , kind, id2, action, extra] = segments;
|
|
6502
6957
|
if (kind === void 0) {
|
|
6503
6958
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
@@ -6547,7 +7002,7 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
6547
7002
|
return { body: new Response(file.body, { headers }) };
|
|
6548
7003
|
}
|
|
6549
7004
|
if (second === "status" && third === void 0) {
|
|
6550
|
-
if (ctx.user
|
|
7005
|
+
if (!await cms.roles.canView(ctx.user, "status")) throw new ForbiddenError();
|
|
6551
7006
|
return { body: await adminStatus(cms) };
|
|
6552
7007
|
}
|
|
6553
7008
|
if (second === "scheduled" && third === void 0)
|
|
@@ -7157,6 +7612,8 @@ export {
|
|
|
7157
7612
|
NotFoundError,
|
|
7158
7613
|
PayloadTooLargeError,
|
|
7159
7614
|
QueryError,
|
|
7615
|
+
ROLE_OPERATIONS,
|
|
7616
|
+
Roles,
|
|
7160
7617
|
SESSION_COOKIE,
|
|
7161
7618
|
SchemaError,
|
|
7162
7619
|
TooManyRequestsError,
|