@easy-cms/core 0.14.0 → 0.16.1
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 +980 -881
- package/dist/index.js +285 -17
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,6 +3,132 @@ var anyone = () => true;
|
|
|
3
3
|
var isLoggedIn = ({ user }) => user !== null;
|
|
4
4
|
var isAdmin = ({ user }) => user?.role === "admin";
|
|
5
5
|
|
|
6
|
+
// src/api-keys.ts
|
|
7
|
+
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
8
|
+
var API_KEYS = "api-keys";
|
|
9
|
+
var API_KEY_OPERATIONS = ["read", "create", "update", "delete", "publish"];
|
|
10
|
+
var API_KEY_GLOBAL_OPERATIONS = ["read", "update", "publish"];
|
|
11
|
+
var API_KEY_EXCLUDED = /* @__PURE__ */ new Set(["users", API_KEYS]);
|
|
12
|
+
var KEY_PATTERN = /^ecms_([0-9a-f]{8})_([A-Za-z0-9_-]{32,})$/;
|
|
13
|
+
function isApiKey(token) {
|
|
14
|
+
return token.startsWith("ecms_");
|
|
15
|
+
}
|
|
16
|
+
function newApiKey() {
|
|
17
|
+
const prefix = randomBytes(4).toString("hex");
|
|
18
|
+
const secret = randomBytes(32).toString("base64url");
|
|
19
|
+
return { key: `ecms_${prefix}_${secret}`, prefix, hash: hashSecret(secret) };
|
|
20
|
+
}
|
|
21
|
+
function hashSecret(secret) {
|
|
22
|
+
return createHash("sha256").update(secret).digest("hex");
|
|
23
|
+
}
|
|
24
|
+
function parseApiKey(token) {
|
|
25
|
+
const match = KEY_PATTERN.exec(token);
|
|
26
|
+
return match ? { prefix: match[1], secret: match[2] } : null;
|
|
27
|
+
}
|
|
28
|
+
function secretMatches(secret, hash) {
|
|
29
|
+
if (typeof hash !== "string") return false;
|
|
30
|
+
const a = Buffer.from(hashSecret(secret), "hex");
|
|
31
|
+
const b = Buffer.from(hash, "hex");
|
|
32
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
33
|
+
}
|
|
34
|
+
function keyAllows(user, target, operation) {
|
|
35
|
+
const key2 = user?.apiKey;
|
|
36
|
+
if (!key2) return true;
|
|
37
|
+
if ("collection" in target) {
|
|
38
|
+
if (API_KEY_EXCLUDED.has(target.collection)) return false;
|
|
39
|
+
return key2.permissions.collections?.[target.collection]?.includes(operation) ?? false;
|
|
40
|
+
}
|
|
41
|
+
const allowed2 = key2.permissions.globals?.[target.global];
|
|
42
|
+
return allowed2?.includes(operation) ?? false;
|
|
43
|
+
}
|
|
44
|
+
var isAdmin2 = (user) => user?.role === "admin";
|
|
45
|
+
var ownKeys = ({ user }) => !user || user.apiKey ? false : isAdmin2(user) ? true : { user: { equals: user.id } };
|
|
46
|
+
var system = { update: () => false };
|
|
47
|
+
var apiKeysCollection = {
|
|
48
|
+
slug: API_KEYS,
|
|
49
|
+
labels: {
|
|
50
|
+
singular: { en: "API key", th: "API key" },
|
|
51
|
+
plural: { en: "API keys", th: "API keys" }
|
|
52
|
+
},
|
|
53
|
+
icon: "key",
|
|
54
|
+
useAsTitle: "name",
|
|
55
|
+
access: {
|
|
56
|
+
read: ownKeys,
|
|
57
|
+
// Through `createApiKey` (POST <api>/api-keys), which generates the secret.
|
|
58
|
+
create: ({ user }) => !!user && !user.apiKey,
|
|
59
|
+
update: ownKeys,
|
|
60
|
+
delete: ownKeys
|
|
61
|
+
},
|
|
62
|
+
fields: [
|
|
63
|
+
{ name: "name", type: "text", required: true, label: { en: "Name", th: "\u0E0A\u0E37\u0E48\u0E2D" } },
|
|
64
|
+
{
|
|
65
|
+
name: "permissions",
|
|
66
|
+
type: "json",
|
|
67
|
+
label: { en: "Permissions", th: "\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C" },
|
|
68
|
+
admin: { component: "ecms-api-key-permissions" }
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "expiresAt",
|
|
72
|
+
type: "date",
|
|
73
|
+
label: { en: "Expires", th: "\u0E2B\u0E21\u0E14\u0E2D\u0E32\u0E22\u0E38" },
|
|
74
|
+
position: "sidebar"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "prefix",
|
|
78
|
+
type: "text",
|
|
79
|
+
index: true,
|
|
80
|
+
label: { en: "Key starts with", th: "\u0E02\u0E36\u0E49\u0E19\u0E15\u0E49\u0E19\u0E14\u0E49\u0E27\u0E22" },
|
|
81
|
+
position: "sidebar",
|
|
82
|
+
access: system
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "user",
|
|
86
|
+
type: "relationship",
|
|
87
|
+
to: "users",
|
|
88
|
+
label: { en: "Owner", th: "\u0E40\u0E08\u0E49\u0E32\u0E02\u0E2D\u0E07" },
|
|
89
|
+
position: "sidebar",
|
|
90
|
+
access: system
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "lastUsedAt",
|
|
94
|
+
type: "date",
|
|
95
|
+
label: { en: "Last used", th: "\u0E43\u0E0A\u0E49\u0E25\u0E48\u0E32\u0E2A\u0E38\u0E14" },
|
|
96
|
+
position: "sidebar",
|
|
97
|
+
access: system
|
|
98
|
+
},
|
|
99
|
+
{ name: "keyHash", type: "text", hidden: true }
|
|
100
|
+
],
|
|
101
|
+
hooks: {
|
|
102
|
+
// Only real collections, globals and operations are kept.
|
|
103
|
+
beforeChange: [
|
|
104
|
+
({ data, cms }) => data.permissions === void 0 ? data : {
|
|
105
|
+
...data,
|
|
106
|
+
permissions: cleanPermissions(data.permissions, {
|
|
107
|
+
collections: cms.config.collections.map((c) => c.slug),
|
|
108
|
+
globals: cms.config.globals.map((g) => g.slug)
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
function cleanPermissions(value, known) {
|
|
115
|
+
const input = typeof value === "object" && value !== null ? value : {};
|
|
116
|
+
const pick = (group, slugs, operations) => {
|
|
117
|
+
const out = {};
|
|
118
|
+
if (typeof group !== "object" || group === null) return out;
|
|
119
|
+
for (const [slug, ops] of Object.entries(group)) {
|
|
120
|
+
if (!slugs.includes(slug) || API_KEY_EXCLUDED.has(slug) || !Array.isArray(ops)) continue;
|
|
121
|
+
const valid = operations.filter((op) => ops.includes(op));
|
|
122
|
+
if (valid.length) out[slug] = valid;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
collections: pick(input.collections, known.collections, API_KEY_OPERATIONS),
|
|
128
|
+
globals: pick(input.globals, known.globals, API_KEY_GLOBAL_OPERATIONS)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
6
132
|
// src/errors.ts
|
|
7
133
|
var ConfigError = class _ConfigError extends Error {
|
|
8
134
|
issues;
|
|
@@ -412,7 +538,7 @@ var webhookDeliveriesCollection = {
|
|
|
412
538
|
};
|
|
413
539
|
|
|
414
540
|
// src/auth/password.ts
|
|
415
|
-
import { randomBytes, scrypt, timingSafeEqual } from "crypto";
|
|
541
|
+
import { randomBytes as randomBytes2, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
416
542
|
var MIN_PASSWORD_LENGTH = 8;
|
|
417
543
|
var N = 2 ** 17;
|
|
418
544
|
var R = 8;
|
|
@@ -431,7 +557,7 @@ function derive(password, salt, n, r, p, length) {
|
|
|
431
557
|
});
|
|
432
558
|
}
|
|
433
559
|
async function hashPassword(password) {
|
|
434
|
-
const salt =
|
|
560
|
+
const salt = randomBytes2(16);
|
|
435
561
|
const key2 = await derive(password, salt, N, R, P, KEY_LENGTH);
|
|
436
562
|
return ["scrypt", N, R, P, salt.toString("base64url"), key2.toString("base64url")].join("$");
|
|
437
563
|
}
|
|
@@ -447,7 +573,7 @@ async function verifyPassword(password, stored) {
|
|
|
447
573
|
Number(p),
|
|
448
574
|
expected.length
|
|
449
575
|
);
|
|
450
|
-
return key2.length === expected.length &&
|
|
576
|
+
return key2.length === expected.length && timingSafeEqual2(key2, expected);
|
|
451
577
|
}
|
|
452
578
|
var dummyHash;
|
|
453
579
|
async function fakeVerify(password) {
|
|
@@ -457,12 +583,12 @@ async function fakeVerify(password) {
|
|
|
457
583
|
}
|
|
458
584
|
|
|
459
585
|
// src/auth/tokens.ts
|
|
460
|
-
import { createHash, createHmac, randomBytes as
|
|
586
|
+
import { createHash as createHash2, createHmac, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
461
587
|
function newToken() {
|
|
462
|
-
return
|
|
588
|
+
return randomBytes3(32).toString("base64url");
|
|
463
589
|
}
|
|
464
590
|
function hashToken(token) {
|
|
465
|
-
return
|
|
591
|
+
return createHash2("sha256").update(token).digest("hex");
|
|
466
592
|
}
|
|
467
593
|
function hmac(secret, value) {
|
|
468
594
|
return createHmac("sha256", secret).update(value).digest("base64url");
|
|
@@ -482,7 +608,7 @@ function csrfToken(secret, token) {
|
|
|
482
608
|
function safeEqual(a, b) {
|
|
483
609
|
const left = Buffer.from(a);
|
|
484
610
|
const right = Buffer.from(b);
|
|
485
|
-
return left.length === right.length &&
|
|
611
|
+
return left.length === right.length && timingSafeEqual3(left, right);
|
|
486
612
|
}
|
|
487
613
|
function signPreviewToken(secret, target, expiresAt) {
|
|
488
614
|
const payload = Buffer.from(
|
|
@@ -568,9 +694,13 @@ var Auth = class {
|
|
|
568
694
|
const session = await this.findSession(token);
|
|
569
695
|
if (session) await this.db.delete({ collection: SESSIONS, id: session.id });
|
|
570
696
|
}
|
|
571
|
-
/**
|
|
697
|
+
/**
|
|
698
|
+
* Returns the user for a signed session token, or for an API key (`ecms_…`, with `apiKey` set
|
|
699
|
+
* on the user), or `null` if it is invalid or expired.
|
|
700
|
+
*/
|
|
572
701
|
async verify(signedToken) {
|
|
573
702
|
if (!signedToken) return null;
|
|
703
|
+
if (isApiKey(signedToken)) return this.verifyApiKey(signedToken);
|
|
574
704
|
const token = unsignToken(this.config.secret, signedToken);
|
|
575
705
|
if (!token) return null;
|
|
576
706
|
const session = await this.findSession(token);
|
|
@@ -606,6 +736,40 @@ var Auth = class {
|
|
|
606
736
|
for (const session of sessions.docs)
|
|
607
737
|
await this.db.delete({ collection: SESSIONS, id: session.id });
|
|
608
738
|
}
|
|
739
|
+
/** The owner of an API key, limited by the key; `null` when unknown, expired or disabled. */
|
|
740
|
+
async verifyApiKey(token) {
|
|
741
|
+
if (!this.cms.config.collections.some((c) => c.slug === API_KEYS)) return null;
|
|
742
|
+
const parsed = parseApiKey(token);
|
|
743
|
+
if (!parsed) return null;
|
|
744
|
+
const { docs } = await this.db.find({
|
|
745
|
+
collection: API_KEYS,
|
|
746
|
+
where: { prefix: { equals: parsed.prefix } },
|
|
747
|
+
sort: [],
|
|
748
|
+
limit: 1,
|
|
749
|
+
page: 1
|
|
750
|
+
});
|
|
751
|
+
const key2 = docs[0];
|
|
752
|
+
if (!key2 || !secretMatches(parsed.secret, key2.keyHash)) return null;
|
|
753
|
+
const now = /* @__PURE__ */ new Date();
|
|
754
|
+
if (typeof key2.expiresAt === "string" && key2.expiresAt <= now.toISOString()) return null;
|
|
755
|
+
const owner = await this.db.findById({ collection: USERS, id: key2.user });
|
|
756
|
+
if (!owner || owner.active === false) return null;
|
|
757
|
+
const last = typeof key2.lastUsedAt === "string" ? Date.parse(key2.lastUsedAt) : 0;
|
|
758
|
+
if (now.getTime() - last > 6e4) {
|
|
759
|
+
const { id: _id, ...rest } = key2;
|
|
760
|
+
await this.db.update({
|
|
761
|
+
collection: API_KEYS,
|
|
762
|
+
id: key2.id,
|
|
763
|
+
data: { ...rest, lastUsedAt: now.toISOString() }
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
const apiKey = {
|
|
767
|
+
id: key2.id,
|
|
768
|
+
name: String(key2.name ?? ""),
|
|
769
|
+
permissions: key2.permissions ?? {}
|
|
770
|
+
};
|
|
771
|
+
return { ...await this.toAuthUser(owner), apiKey };
|
|
772
|
+
}
|
|
609
773
|
// -------------------------------------------------------------------------
|
|
610
774
|
async startSession(user) {
|
|
611
775
|
const token = newToken();
|
|
@@ -744,12 +908,38 @@ var ADMIN_ICONS = [
|
|
|
744
908
|
"circle-help",
|
|
745
909
|
"award",
|
|
746
910
|
"ticket",
|
|
747
|
-
"camera"
|
|
911
|
+
"camera",
|
|
912
|
+
"key"
|
|
748
913
|
];
|
|
749
914
|
function defineConfig(config) {
|
|
750
915
|
return config;
|
|
751
916
|
}
|
|
752
917
|
|
|
918
|
+
// src/config-signature.ts
|
|
919
|
+
function configSignature(config) {
|
|
920
|
+
const known = signatures.get(config);
|
|
921
|
+
if (known !== void 0) return known;
|
|
922
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
923
|
+
const signature = JSON.stringify(config, (_key, value) => {
|
|
924
|
+
if (typeof value === "function") return "fn";
|
|
925
|
+
if (typeof value === "object" && value !== null) {
|
|
926
|
+
if (seen.has(value)) return "[circular]";
|
|
927
|
+
seen.add(value);
|
|
928
|
+
if (!Array.isArray(value)) {
|
|
929
|
+
const sorted = {};
|
|
930
|
+
for (const key2 of Object.keys(value).sort())
|
|
931
|
+
sorted[key2] = value[key2];
|
|
932
|
+
seen.add(sorted);
|
|
933
|
+
return sorted;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return value;
|
|
937
|
+
});
|
|
938
|
+
signatures.set(config, signature);
|
|
939
|
+
return signature;
|
|
940
|
+
}
|
|
941
|
+
var signatures = /* @__PURE__ */ new WeakMap();
|
|
942
|
+
|
|
753
943
|
// src/copy.ts
|
|
754
944
|
var BATCH = 200;
|
|
755
945
|
async function copyDatabase(source, target, options = {}) {
|
|
@@ -1285,6 +1475,7 @@ var RESERVED_SLUGS = /* @__PURE__ */ new Set([
|
|
|
1285
1475
|
"document-versions",
|
|
1286
1476
|
"scheduled-jobs",
|
|
1287
1477
|
"webhook-deliveries",
|
|
1478
|
+
"api-keys",
|
|
1288
1479
|
"jobs",
|
|
1289
1480
|
"migrations",
|
|
1290
1481
|
"access"
|
|
@@ -1333,6 +1524,8 @@ function validateConfig(config) {
|
|
|
1333
1524
|
checkSlug(global2, `globals[${i}]`, seenGlobals, add);
|
|
1334
1525
|
});
|
|
1335
1526
|
validateEndpoints(config, collectionSlugs, add);
|
|
1527
|
+
const apiKeys = config.apiKeys;
|
|
1528
|
+
if (apiKeys !== void 0 && typeof apiKeys !== "boolean") add("apiKeys", "must be true or false");
|
|
1336
1529
|
collections.forEach((collection2, i) => {
|
|
1337
1530
|
const path = `collections.${collection2.slug ?? `[${i}]`}`;
|
|
1338
1531
|
validateContainer(collection2, path, collectionSlugs, add);
|
|
@@ -1804,7 +1997,7 @@ function validateComponents(components, path, add) {
|
|
|
1804
1997
|
}
|
|
1805
1998
|
var ENDPOINT_METHODS = ["get", "post", "put", "patch", "delete"];
|
|
1806
1999
|
var ENDPOINT_SEGMENT = /^(:[A-Za-z_][A-Za-z0-9_]*|[A-Za-z0-9._~-]+)$/;
|
|
1807
|
-
var RESERVED_ENDPOINT_ROOTS = /* @__PURE__ */ new Set(["users", "globals", "admin", "jobs", "media"]);
|
|
2000
|
+
var RESERVED_ENDPOINT_ROOTS = /* @__PURE__ */ new Set(["users", "globals", "admin", "jobs", "media", "api-keys"]);
|
|
1808
2001
|
function validateEndpoints(config, collectionSlugs, add) {
|
|
1809
2002
|
const endpoints = config.endpoints;
|
|
1810
2003
|
if (endpoints === void 0) return;
|
|
@@ -1953,7 +2146,8 @@ async function resolveConfig(input) {
|
|
|
1953
2146
|
...internalCollections,
|
|
1954
2147
|
...usesVersions(config) ? [versionsCollection] : [],
|
|
1955
2148
|
...usesSchedule(config) ? [scheduledJobsCollection] : [],
|
|
1956
|
-
...(config.webhooks?.length ?? 0) > 0 ? [webhookDeliveriesCollection] : []
|
|
2149
|
+
...(config.webhooks?.length ?? 0) > 0 ? [webhookDeliveriesCollection] : [],
|
|
2150
|
+
...config.apiKeys ? [apiKeysCollection] : []
|
|
1957
2151
|
],
|
|
1958
2152
|
globals: config.globals ?? [],
|
|
1959
2153
|
endpoints: config.endpoints ?? []
|
|
@@ -2014,7 +2208,7 @@ async function loadConfig(options = {}) {
|
|
|
2014
2208
|
}
|
|
2015
2209
|
|
|
2016
2210
|
// src/local-api.ts
|
|
2017
|
-
import { randomBytes as
|
|
2211
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
2018
2212
|
|
|
2019
2213
|
// src/localization.ts
|
|
2020
2214
|
var ALL_LOCALES = "all";
|
|
@@ -2593,6 +2787,7 @@ var EasyCMS = class {
|
|
|
2593
2787
|
const config = this.collection(MEDIA);
|
|
2594
2788
|
const guard = guardOf(options);
|
|
2595
2789
|
if (guard.enforce) {
|
|
2790
|
+
this.checkKey(guard, { collection: MEDIA }, "create");
|
|
2596
2791
|
const allowed2 = await evaluateAccess(config.access?.create, { user: guard.user, data });
|
|
2597
2792
|
if (allowed2 !== true) throw deny(guard.user);
|
|
2598
2793
|
}
|
|
@@ -2611,7 +2806,7 @@ var EasyCMS = class {
|
|
|
2611
2806
|
}
|
|
2612
2807
|
]);
|
|
2613
2808
|
}
|
|
2614
|
-
const random =
|
|
2809
|
+
const random = randomBytes4(4).toString("hex");
|
|
2615
2810
|
const filename = storageKey(file.name, mimeType, random);
|
|
2616
2811
|
const stored = [];
|
|
2617
2812
|
try {
|
|
@@ -2642,6 +2837,41 @@ var EasyCMS = class {
|
|
|
2642
2837
|
throw error;
|
|
2643
2838
|
}
|
|
2644
2839
|
}
|
|
2840
|
+
/**
|
|
2841
|
+
* Creates an API key (needs `apiKeys: true`) and returns it once: only a hash is stored.
|
|
2842
|
+
* With access enforced the key belongs to `user`; trusted calls name the owner in `data.user`.
|
|
2843
|
+
*/
|
|
2844
|
+
async createApiKey(data, options = {}) {
|
|
2845
|
+
if (!this.config.collections.some((c) => c.slug === API_KEYS))
|
|
2846
|
+
throw new QueryError("API keys are off: set `apiKeys: true` in the config");
|
|
2847
|
+
const config = this.collection(API_KEYS);
|
|
2848
|
+
const guard = guardOf(options);
|
|
2849
|
+
if (guard.enforce) {
|
|
2850
|
+
const allowed2 = await evaluateAccess(config.access?.create, { user: guard.user });
|
|
2851
|
+
if (allowed2 !== true) throw deny(guard.user);
|
|
2852
|
+
}
|
|
2853
|
+
const owner = guard.enforce ? guard.user?.id : data.user;
|
|
2854
|
+
if (owner === void 0 || owner === null)
|
|
2855
|
+
throw new ValidationError(API_KEYS, [{ field: "user", message: "is required" }]);
|
|
2856
|
+
const { key: key2, prefix, hash } = newApiKey();
|
|
2857
|
+
const expiresAt = data.expiresAt instanceof Date ? data.expiresAt.toISOString() : data.expiresAt;
|
|
2858
|
+
const created = await this.createDocument(
|
|
2859
|
+
config,
|
|
2860
|
+
{
|
|
2861
|
+
name: data.name,
|
|
2862
|
+
permissions: data.permissions ?? {},
|
|
2863
|
+
expiresAt: expiresAt ?? null,
|
|
2864
|
+
prefix,
|
|
2865
|
+
user: owner
|
|
2866
|
+
},
|
|
2867
|
+
{ depth: 0 },
|
|
2868
|
+
guard
|
|
2869
|
+
);
|
|
2870
|
+
const raw = await this.db.findById({ collection: API_KEYS, id: created.id });
|
|
2871
|
+
const { id: _id, ...rest } = raw;
|
|
2872
|
+
await this.db.update({ collection: API_KEYS, id: created.id, data: { ...rest, keyHash: hash } });
|
|
2873
|
+
return { key: key2, doc: created };
|
|
2874
|
+
}
|
|
2645
2875
|
/** Public URL of a stored file. */
|
|
2646
2876
|
mediaURL(key2) {
|
|
2647
2877
|
const custom = this.storage.url?.(key2);
|
|
@@ -2739,6 +2969,7 @@ var EasyCMS = class {
|
|
|
2739
2969
|
let parsed;
|
|
2740
2970
|
if (id === null) {
|
|
2741
2971
|
if (guard.enforce) {
|
|
2972
|
+
this.checkKey(guard, { collection: collection2 }, "create");
|
|
2742
2973
|
const allowed2 = await evaluateAccess(config.access?.create, { user: guard.user, data: raw });
|
|
2743
2974
|
if (allowed2 !== true) throw deny(guard.user);
|
|
2744
2975
|
}
|
|
@@ -2813,7 +3044,7 @@ var EasyCMS = class {
|
|
|
2813
3044
|
return { doc: global2, url: this.previewURL(config, global2, options) };
|
|
2814
3045
|
}
|
|
2815
3046
|
/** Coerces preview input like a save would, keeping values that would fail validation. */
|
|
2816
|
-
async previewDoc(config, data,
|
|
3047
|
+
async previewDoc(config, data, system2, options = {}) {
|
|
2817
3048
|
const locale = this.localeOf(options);
|
|
2818
3049
|
const { data: clean } = await validateFields(config.fields, data, {
|
|
2819
3050
|
operation: "update",
|
|
@@ -2826,7 +3057,7 @@ var EasyCMS = class {
|
|
|
2826
3057
|
...data,
|
|
2827
3058
|
...clean,
|
|
2828
3059
|
...config.drafts ? { status: data.status } : {},
|
|
2829
|
-
...
|
|
3060
|
+
...system2,
|
|
2830
3061
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2831
3062
|
};
|
|
2832
3063
|
}
|
|
@@ -2857,6 +3088,8 @@ var EasyCMS = class {
|
|
|
2857
3088
|
const existing = parsed === void 0 ? null : await this.db.findById({ collection: collection2, id: parsed });
|
|
2858
3089
|
if (!existing || parsed === void 0) throw new NotFoundError(collection2, id);
|
|
2859
3090
|
await this.checkDocumentAccess(config, "update", guard, parsed, raw);
|
|
3091
|
+
if (config.drafts && (mode === "unpublish" || raw.status === "published"))
|
|
3092
|
+
this.checkKey(guard, { collection: collection2 }, "publish");
|
|
2860
3093
|
const [current] = await this.withDrafts(config, [existing]);
|
|
2861
3094
|
const { input, password } = splitPassword(config, raw);
|
|
2862
3095
|
const filtered = await filterInput(
|
|
@@ -3043,6 +3276,8 @@ var EasyCMS = class {
|
|
|
3043
3276
|
const config = this.global(slug);
|
|
3044
3277
|
const guard = guardOf(options);
|
|
3045
3278
|
await this.checkGlobalAccess(config, "update", guard);
|
|
3279
|
+
if (config.drafts && (mode === "unpublish" || raw.status === "published"))
|
|
3280
|
+
this.checkKey(guard, { global: slug }, "publish");
|
|
3046
3281
|
const input = await filterInput(
|
|
3047
3282
|
config.fields,
|
|
3048
3283
|
raw,
|
|
@@ -3276,12 +3511,14 @@ var EasyCMS = class {
|
|
|
3276
3511
|
const existing = parsed === void 0 ? null : await this.db.findById({ collection: collection2, id: parsed });
|
|
3277
3512
|
if (!existing || parsed === void 0) throw new NotFoundError(collection2, id);
|
|
3278
3513
|
await this.checkDocumentAccess(config, "update", guardOf(options), parsed, void 0);
|
|
3514
|
+
this.checkKey(guardOf(options), { collection: collection2 }, "publish");
|
|
3279
3515
|
return { config, parsed };
|
|
3280
3516
|
}
|
|
3281
3517
|
async globalScheduleTarget(slug, options) {
|
|
3282
3518
|
const config = this.global(slug);
|
|
3283
3519
|
if (!config.schedule) throw new QueryError(`"${slug}" has no schedule`);
|
|
3284
3520
|
await this.checkGlobalAccess(config, "update", guardOf(options));
|
|
3521
|
+
this.checkKey(guardOf(options), { global: slug }, "publish");
|
|
3285
3522
|
}
|
|
3286
3523
|
async addJob(parent, doc, job, options) {
|
|
3287
3524
|
const runAt = new Date(job.at);
|
|
@@ -3380,15 +3617,23 @@ var EasyCMS = class {
|
|
|
3380
3617
|
if (!config) throw new QueryError(`Unknown global "${slug}"`);
|
|
3381
3618
|
return config;
|
|
3382
3619
|
}
|
|
3620
|
+
/** Refuses what a request's API key does not allow (see `ApiKeyPermissions`). */
|
|
3621
|
+
checkKey(guard, target, operation) {
|
|
3622
|
+
if (!guard.enforce || keyAllows(guard.user, target, operation)) return;
|
|
3623
|
+
const name = "collection" in target ? target.collection : target.global;
|
|
3624
|
+
throw new ForbiddenError(`This API key may not ${operation} "${name}"`);
|
|
3625
|
+
}
|
|
3383
3626
|
/** The query constraint read access adds, or `undefined` when there is none. Throws when denied. */
|
|
3384
3627
|
async readWhere(config, guard, where) {
|
|
3385
3628
|
if (!guard.enforce) return where;
|
|
3629
|
+
this.checkKey(guard, { collection: config.slug }, "read");
|
|
3386
3630
|
const access = await evaluateAccess(config.access?.read, { user: guard.user });
|
|
3387
3631
|
if (access === false) throw deny(guard.user);
|
|
3388
3632
|
return andWhere(where, access);
|
|
3389
3633
|
}
|
|
3390
3634
|
async checkDocumentAccess(config, operation, guard, id, data) {
|
|
3391
3635
|
if (!guard.enforce) return;
|
|
3636
|
+
this.checkKey(guard, { collection: config.slug }, operation);
|
|
3392
3637
|
const access = await evaluateAccess(config.access?.[operation], {
|
|
3393
3638
|
user: guard.user,
|
|
3394
3639
|
id,
|
|
@@ -3406,6 +3651,7 @@ var EasyCMS = class {
|
|
|
3406
3651
|
}
|
|
3407
3652
|
async checkGlobalAccess(config, operation, guard) {
|
|
3408
3653
|
if (!guard.enforce) return;
|
|
3654
|
+
this.checkKey(guard, { global: config.slug }, operation);
|
|
3409
3655
|
const access = await evaluateAccess(config.access?.[operation], { user: guard.user });
|
|
3410
3656
|
if (typeof access === "object")
|
|
3411
3657
|
throw new QueryError(`${operation} access of global "${config.slug}" must return a boolean`);
|
|
@@ -3459,6 +3705,9 @@ var EasyCMS = class {
|
|
|
3459
3705
|
const guard = guardOf(options);
|
|
3460
3706
|
const collection2 = config.slug;
|
|
3461
3707
|
if (guard.enforce) {
|
|
3708
|
+
this.checkKey(guard, { collection: collection2 }, "create");
|
|
3709
|
+
if (config.drafts && raw.status === "published")
|
|
3710
|
+
this.checkKey(guard, { collection: collection2 }, "publish");
|
|
3462
3711
|
const allowed2 = await evaluateAccess(config.access?.create, { user: guard.user, data: raw });
|
|
3463
3712
|
if (typeof allowed2 === "object")
|
|
3464
3713
|
throw new QueryError(`create access of "${collection2}" must return a boolean`);
|
|
@@ -3823,7 +4072,7 @@ function asObject(data, name) {
|
|
|
3823
4072
|
}
|
|
3824
4073
|
|
|
3825
4074
|
// src/rest/admin-modules.ts
|
|
3826
|
-
import { createHash as
|
|
4075
|
+
import { createHash as createHash3 } from "crypto";
|
|
3827
4076
|
import { readFile as readFile2 } from "fs/promises";
|
|
3828
4077
|
import { createRequire } from "module";
|
|
3829
4078
|
import { join as join2 } from "path";
|
|
@@ -3864,7 +4113,7 @@ async function readAdminModule(cms, index) {
|
|
|
3864
4113
|
if (!file) return void 0;
|
|
3865
4114
|
const body = await readFile2(file, "utf8").catch(() => void 0);
|
|
3866
4115
|
if (body === void 0) return void 0;
|
|
3867
|
-
return { body, etag: `"${
|
|
4116
|
+
return { body, etag: `"${createHash3("sha1").update(body).digest("base64url")}"` };
|
|
3868
4117
|
}
|
|
3869
4118
|
|
|
3870
4119
|
// src/rest/admin-schema.ts
|
|
@@ -4130,6 +4379,8 @@ function createRestHandler(instance, options = {}) {
|
|
|
4130
4379
|
const segments = url.pathname.slice(basePath.length).split("/").filter(Boolean).map((s) => decodeURIComponent(s));
|
|
4131
4380
|
const { token, via } = readToken(request);
|
|
4132
4381
|
const user = token ? await cms.auth.verify(token) : null;
|
|
4382
|
+
if (!user && via === "bearer" && token?.startsWith("ecms_"))
|
|
4383
|
+
throw new UnauthorizedError("Invalid or expired API key");
|
|
4133
4384
|
const ctx = { request, url, user, via: user ? via : null, token, headers };
|
|
4134
4385
|
const method = request.method.toUpperCase();
|
|
4135
4386
|
if (method !== "GET" && method !== "HEAD") checkCsrf(cms, ctx);
|
|
@@ -4331,6 +4582,18 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
4331
4582
|
body: await cms.upload(file, data, { ...access, ...parseDepth(ctx.url) })
|
|
4332
4583
|
};
|
|
4333
4584
|
}
|
|
4585
|
+
if (first === API_KEYS && second === void 0 && method === "POST") {
|
|
4586
|
+
const body = await readJson(ctx.request);
|
|
4587
|
+
const { key: key2, doc } = await cms.createApiKey(
|
|
4588
|
+
{
|
|
4589
|
+
name: typeof body.name === "string" ? body.name : "",
|
|
4590
|
+
...body.permissions !== void 0 ? { permissions: body.permissions } : {},
|
|
4591
|
+
...typeof body.expiresAt === "string" ? { expiresAt: body.expiresAt } : {}
|
|
4592
|
+
},
|
|
4593
|
+
access
|
|
4594
|
+
);
|
|
4595
|
+
return { status: 201, body: { ...doc, key: key2 } };
|
|
4596
|
+
}
|
|
4334
4597
|
const collection2 = first;
|
|
4335
4598
|
if (INTERNAL_COLLECTIONS.has(collection2) || !cms.config.collections.some((c) => c.slug === collection2)) {
|
|
4336
4599
|
throw new HttpError(`Unknown collection "${collection2}"`, 404);
|
|
@@ -4817,6 +5080,9 @@ ${map(n.globals)}
|
|
|
4817
5080
|
}
|
|
4818
5081
|
export {
|
|
4819
5082
|
ADMIN_ICONS,
|
|
5083
|
+
API_KEYS,
|
|
5084
|
+
API_KEY_GLOBAL_OPERATIONS,
|
|
5085
|
+
API_KEY_OPERATIONS,
|
|
4820
5086
|
Auth,
|
|
4821
5087
|
BUILTIN_COLLECTIONS,
|
|
4822
5088
|
CONFIG_FILE_NAMES,
|
|
@@ -4851,6 +5117,7 @@ export {
|
|
|
4851
5117
|
anyone,
|
|
4852
5118
|
applyDefaults,
|
|
4853
5119
|
applyPlugins,
|
|
5120
|
+
configSignature,
|
|
4854
5121
|
consoleLogger,
|
|
4855
5122
|
copyDatabase,
|
|
4856
5123
|
createEasyCMS,
|
|
@@ -4866,6 +5133,7 @@ export {
|
|
|
4866
5133
|
importConfig,
|
|
4867
5134
|
isAdmin,
|
|
4868
5135
|
isLoggedIn,
|
|
5136
|
+
keyAllows,
|
|
4869
5137
|
loadConfig,
|
|
4870
5138
|
localStorage,
|
|
4871
5139
|
mergeForUpdate,
|