@easy-cms/core 1.30.1 → 1.32.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/{chunk-W3FVQ2CA.js → chunk-J4NASOMO.js} +1458 -125
- package/dist/{config-DGATdBdP.d.ts → config-Dr_NZGm6.d.ts} +323 -3
- package/dist/{handler-DsA868nd.d.ts → handler-B0m4VOAy.d.ts} +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +1 -1
- package/dist/{plugin-B6lp3qVf.d.ts → plugin-DBHFjM-r.d.ts} +8 -2
- package/dist/plugin.d.ts +2 -2
- package/package.json +1 -1
|
@@ -61,6 +61,7 @@ function hashToken(token) {
|
|
|
61
61
|
function hmac(secret, value) {
|
|
62
62
|
return createHmac("sha256", secret).update(value).digest("base64url");
|
|
63
63
|
}
|
|
64
|
+
var signValue = (secret, value) => hmac(secret, value);
|
|
64
65
|
function secretsOf(config) {
|
|
65
66
|
return [config.secret, ...config.previousSecrets ?? []];
|
|
66
67
|
}
|
|
@@ -161,6 +162,8 @@ var AUDIT_LOGS = "audit-logs";
|
|
|
161
162
|
var OUTBOX = "outbox";
|
|
162
163
|
var RATE_LIMITS = "rate-limits";
|
|
163
164
|
var BULK_OPERATIONS = "bulk-operations";
|
|
165
|
+
var SAVED_VIEWS = "saved-views";
|
|
166
|
+
var CONNECTIONS = "connections";
|
|
164
167
|
var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
165
168
|
SESSIONS,
|
|
166
169
|
LOGIN_ATTEMPTS,
|
|
@@ -175,7 +178,9 @@ var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
|
175
178
|
AUDIT_LOGS,
|
|
176
179
|
OUTBOX,
|
|
177
180
|
RATE_LIMITS,
|
|
178
|
-
BULK_OPERATIONS
|
|
181
|
+
BULK_OPERATIONS,
|
|
182
|
+
SAVED_VIEWS,
|
|
183
|
+
CONNECTIONS
|
|
179
184
|
]);
|
|
180
185
|
var DEFAULT_ROLES = ["admin", "editor"];
|
|
181
186
|
var CREATED_BY_FIELD = {
|
|
@@ -563,6 +568,40 @@ var bulkOperationsCollection = {
|
|
|
563
568
|
{ name: "finishedAt", type: "text" }
|
|
564
569
|
]
|
|
565
570
|
};
|
|
571
|
+
var savedViewsCollection = {
|
|
572
|
+
slug: SAVED_VIEWS,
|
|
573
|
+
access: { read: nobody, create: nobody, update: nobody, delete: nobody },
|
|
574
|
+
fields: [
|
|
575
|
+
{ name: "collection", type: "text", required: true, index: true },
|
|
576
|
+
{ name: "user", type: "text", required: true, index: true },
|
|
577
|
+
// view | default
|
|
578
|
+
{ name: "kind", type: "text", required: true },
|
|
579
|
+
{ name: "name", type: "text" },
|
|
580
|
+
{ name: "shared", type: "boolean" },
|
|
581
|
+
// `{ query, columns?, pageSize?, density? }`: the list's URL query, and what the browser kept.
|
|
582
|
+
{ name: "state", type: "json" },
|
|
583
|
+
// For `default`: a view's id, or `config:<id>` for one of `admin.views`.
|
|
584
|
+
{ name: "target", type: "text" }
|
|
585
|
+
]
|
|
586
|
+
};
|
|
587
|
+
var connectionsCollection = {
|
|
588
|
+
slug: CONNECTIONS,
|
|
589
|
+
access: { read: nobody, create: nobody, update: nobody, delete: nobody },
|
|
590
|
+
fields: [
|
|
591
|
+
// connection | integration | pending
|
|
592
|
+
{ name: "kind", type: "text", required: true },
|
|
593
|
+
{ name: "integration", type: "text", required: true, index: true },
|
|
594
|
+
{ name: "name", type: "text" },
|
|
595
|
+
{ name: "isDefault", type: "boolean" },
|
|
596
|
+
{ name: "values", type: "json" },
|
|
597
|
+
// AES-256-GCM, with a key derived from the secret (see integrations.ts).
|
|
598
|
+
{ name: "secret", type: "text" },
|
|
599
|
+
{ name: "account", type: "text" },
|
|
600
|
+
{ name: "lastUsedAt", type: "text" },
|
|
601
|
+
{ name: "lastError", type: "text" },
|
|
602
|
+
{ name: "enabled", type: "boolean" }
|
|
603
|
+
]
|
|
604
|
+
};
|
|
566
605
|
var webhookDeliveriesCollection = {
|
|
567
606
|
slug: WEBHOOK_DELIVERIES,
|
|
568
607
|
access: { read: nobody, create: nobody, update: nobody, delete: nobody },
|
|
@@ -705,6 +744,15 @@ var emailDeliveriesCollection = {
|
|
|
705
744
|
// src/audit.ts
|
|
706
745
|
import { AsyncLocalStorage } from "async_hooks";
|
|
707
746
|
import { createHmac as createHmac2 } from "crypto";
|
|
747
|
+
|
|
748
|
+
// src/csv.ts
|
|
749
|
+
function csvCell(value) {
|
|
750
|
+
const s = value === null || value === void 0 ? "" : typeof value === "string" ? value : JSON.stringify(value);
|
|
751
|
+
const safe = /^[=+\-@\t\r]/.test(s) ? `'${s}` : s;
|
|
752
|
+
return /[",\n\r]/.test(safe) ? `"${safe.replaceAll('"', '""')}"` : safe;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/audit.ts
|
|
708
756
|
var DEFAULT_AUDIT_KEEP = 365;
|
|
709
757
|
var DEFAULT_FAILED_LOGINS = 20;
|
|
710
758
|
var MAX_VALUE = 500;
|
|
@@ -877,11 +925,7 @@ var AuditLog = class {
|
|
|
877
925
|
limit: CSV_LIMIT,
|
|
878
926
|
page: 1
|
|
879
927
|
});
|
|
880
|
-
const cell =
|
|
881
|
-
const s = value === null || value === void 0 ? "" : typeof value === "string" ? value : JSON.stringify(value);
|
|
882
|
-
const safe = /^[=+\-@\t\r]/.test(s) ? `'${s}` : s;
|
|
883
|
-
return /[",\n\r]/.test(safe) ? `"${safe.replaceAll('"', '""')}"` : safe;
|
|
884
|
-
};
|
|
928
|
+
const cell = csvCell;
|
|
885
929
|
const lines = [
|
|
886
930
|
["at", "action", "target", "doc", "title", "actor", "via", "ip", "changes", "detail"].join(
|
|
887
931
|
","
|
|
@@ -1304,9 +1348,9 @@ var esc = (value) => value.replace(
|
|
|
1304
1348
|
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]
|
|
1305
1349
|
);
|
|
1306
1350
|
function email(subject, lines, button) {
|
|
1307
|
-
const
|
|
1351
|
+
const text4 = [...lines, ...button ? ["", button.url] : []].join("\n");
|
|
1308
1352
|
const html = `<div style="font-family:system-ui,sans-serif;font-size:15px;line-height:1.5;color:#1f2328">${lines.map((line) => `<p>${esc(line)}</p>`).join("")}${button ? `<p><a href="${esc(button.url)}" style="display:inline-block;padding:10px 18px;border-radius:8px;background:#2f6f5e;color:#fff;text-decoration:none;font-weight:600">${esc(button.label)}</a></p><p style="font-size:13px;color:#656d76">${esc(button.url)}</p>` : ""}</div>`;
|
|
1309
|
-
return { subject, text:
|
|
1353
|
+
return { subject, text: text4, html };
|
|
1310
1354
|
}
|
|
1311
1355
|
var hours = (expiresAt) => Math.max(1, Math.round(((expiresAt?.getTime() ?? Date.now()) - Date.now()) / 36e5));
|
|
1312
1356
|
var days = (expiresAt) => Math.max(1, Math.round(hours(expiresAt) / 24));
|
|
@@ -2597,8 +2641,8 @@ function itemsOf(field, n) {
|
|
|
2597
2641
|
return `${n} ${unit}${n === 1 ? "" : "s"}`;
|
|
2598
2642
|
}
|
|
2599
2643
|
var isEmpty = (value) => value === void 0 || value === null || value === "";
|
|
2600
|
-
function slugify(
|
|
2601
|
-
return
|
|
2644
|
+
function slugify(text4) {
|
|
2645
|
+
return text4.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{M}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
|
|
2602
2646
|
}
|
|
2603
2647
|
function applyDefaults(fields, data, localization = null) {
|
|
2604
2648
|
const result = { ...data };
|
|
@@ -2907,7 +2951,7 @@ async function normalizeValue(field, raw, path, options, fail, errors, reference
|
|
|
2907
2951
|
return fail(`must have at least ${field.minRows} rows`);
|
|
2908
2952
|
if (field.maxRows !== void 0 && raw.length > field.maxRows)
|
|
2909
2953
|
return fail(`must have at most ${field.maxRows} rows`);
|
|
2910
|
-
const
|
|
2954
|
+
const rows2 = [];
|
|
2911
2955
|
for (const [i, row] of raw.entries()) {
|
|
2912
2956
|
if (!isPlainObject(row)) {
|
|
2913
2957
|
errors.push({ field: `${path}.${i}`, message: "must be an object" });
|
|
@@ -2917,9 +2961,9 @@ async function normalizeValue(field, raw, path, options, fail, errors, reference
|
|
|
2917
2961
|
errors.push(...sub.errors);
|
|
2918
2962
|
references2.push(...sub.references);
|
|
2919
2963
|
const id = typeof row.id === "string" && row.id !== "" ? row.id : randomUUID();
|
|
2920
|
-
|
|
2964
|
+
rows2.push({ id, ...sub.data });
|
|
2921
2965
|
}
|
|
2922
|
-
return
|
|
2966
|
+
return rows2;
|
|
2923
2967
|
}
|
|
2924
2968
|
case "blocks":
|
|
2925
2969
|
return normalizeBlocks(field, raw, path, options, fail, errors, references2);
|
|
@@ -2933,7 +2977,7 @@ async function normalizeBlocks(field, raw, path, options, fail, errors, referenc
|
|
|
2933
2977
|
return fail(`must have at least ${field.minRows} blocks`);
|
|
2934
2978
|
if (field.maxRows !== void 0 && raw.length > field.maxRows)
|
|
2935
2979
|
return fail(`must have at most ${field.maxRows} blocks`);
|
|
2936
|
-
const
|
|
2980
|
+
const rows2 = [];
|
|
2937
2981
|
for (const [i, row] of raw.entries()) {
|
|
2938
2982
|
const fields = rowFields(field, row);
|
|
2939
2983
|
if (!isPlainObject(row) || !fields) {
|
|
@@ -2947,9 +2991,9 @@ async function normalizeBlocks(field, raw, path, options, fail, errors, referenc
|
|
|
2947
2991
|
errors.push(...sub.errors);
|
|
2948
2992
|
references2.push(...sub.references);
|
|
2949
2993
|
const id = typeof row.id === "string" && row.id !== "" ? row.id : randomUUID();
|
|
2950
|
-
|
|
2994
|
+
rows2.push({ id, blockType: row.blockType, ...sub.data });
|
|
2951
2995
|
}
|
|
2952
|
-
return
|
|
2996
|
+
return rows2;
|
|
2953
2997
|
}
|
|
2954
2998
|
function normalizeSelect(field, raw, fail) {
|
|
2955
2999
|
const allowed2 = field.options.map((o) => typeof o === "string" ? o : o.value);
|
|
@@ -3000,6 +3044,8 @@ var Mailer = class {
|
|
|
3000
3044
|
logger;
|
|
3001
3045
|
queue;
|
|
3002
3046
|
pending = /* @__PURE__ */ new Set();
|
|
3047
|
+
/** Told when an email is given up on (`delivery.failed`). */
|
|
3048
|
+
onGiveUp;
|
|
3003
3049
|
get configured() {
|
|
3004
3050
|
return this.adapter !== void 0;
|
|
3005
3051
|
}
|
|
@@ -3057,6 +3103,8 @@ var Mailer = class {
|
|
|
3057
3103
|
if (delay === void 0) {
|
|
3058
3104
|
failed++;
|
|
3059
3105
|
this.logger.error(`Email gave up after ${attempts} attempts: ${error}`);
|
|
3106
|
+
const sent2 = JSON.parse(queued.message);
|
|
3107
|
+
this.onGiveUp?.({ to: [sent2.to].flat().join(", "), error, attempts });
|
|
3060
3108
|
}
|
|
3061
3109
|
await this.queue.update(id, {
|
|
3062
3110
|
...queued,
|
|
@@ -3198,11 +3246,11 @@ async function copyDatabase(source, target, options = {}) {
|
|
|
3198
3246
|
for (const table of tables) {
|
|
3199
3247
|
let copied = 0;
|
|
3200
3248
|
for (; ; ) {
|
|
3201
|
-
const
|
|
3202
|
-
if (
|
|
3203
|
-
await to.write(table,
|
|
3204
|
-
copied +=
|
|
3205
|
-
if (
|
|
3249
|
+
const rows2 = await from.read(table, copied, BATCH);
|
|
3250
|
+
if (rows2.length === 0) break;
|
|
3251
|
+
await to.write(table, rows2);
|
|
3252
|
+
copied += rows2.length;
|
|
3253
|
+
if (rows2.length < BATCH) break;
|
|
3206
3254
|
}
|
|
3207
3255
|
total += copied;
|
|
3208
3256
|
options.onTable?.({ table, rows: copied });
|
|
@@ -3413,26 +3461,26 @@ async function writeBackupFile(cms, file) {
|
|
|
3413
3461
|
});
|
|
3414
3462
|
}
|
|
3415
3463
|
}
|
|
3416
|
-
var slug = (
|
|
3464
|
+
var slug = (text4) => text4.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "easy-cms";
|
|
3417
3465
|
function filenameFor(cms, date) {
|
|
3418
3466
|
const pad = (n) => String(n).padStart(2, "0");
|
|
3419
3467
|
const stamp = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}`;
|
|
3420
3468
|
return `${slug(cms.config.admin.brand.name || "easy-cms")}-${stamp}.db.gz`;
|
|
3421
3469
|
}
|
|
3422
3470
|
function toAdmin(row) {
|
|
3423
|
-
const
|
|
3471
|
+
const text4 = (value) => typeof value === "string" && value !== "" ? value : null;
|
|
3424
3472
|
return {
|
|
3425
3473
|
id: row.id,
|
|
3426
3474
|
state: ["pending", "running", "done", "failed"].includes(row.state) ? row.state : "failed",
|
|
3427
3475
|
trigger: row.trigger === "scheduled" ? "scheduled" : "manual",
|
|
3428
|
-
filename:
|
|
3476
|
+
filename: text4(row.filename),
|
|
3429
3477
|
size: typeof row.size === "number" ? row.size : null,
|
|
3430
|
-
startedAt:
|
|
3431
|
-
finishedAt:
|
|
3432
|
-
error:
|
|
3433
|
-
author:
|
|
3434
|
-
downloadedBy:
|
|
3435
|
-
downloadedAt:
|
|
3478
|
+
startedAt: text4(row.startedAt),
|
|
3479
|
+
finishedAt: text4(row.finishedAt),
|
|
3480
|
+
error: text4(row.error),
|
|
3481
|
+
author: text4(row.author),
|
|
3482
|
+
downloadedBy: text4(row.downloadedBy),
|
|
3483
|
+
downloadedAt: text4(row.downloadedAt)
|
|
3436
3484
|
};
|
|
3437
3485
|
}
|
|
3438
3486
|
async function update(cms, row, data) {
|
|
@@ -3536,11 +3584,13 @@ async function runBackup(cms, record) {
|
|
|
3536
3584
|
});
|
|
3537
3585
|
cms.logger.info(`Backup ${key} written (${data.byteLength} bytes)`);
|
|
3538
3586
|
} catch (error) {
|
|
3587
|
+
const message2 = (error.message || String(error)).slice(0, 500);
|
|
3539
3588
|
await update(cms, row, {
|
|
3540
3589
|
state: "failed",
|
|
3541
|
-
error:
|
|
3590
|
+
error: message2,
|
|
3542
3591
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3543
3592
|
});
|
|
3593
|
+
cms.systemEvent("backup.failed", { backup: row.id, error: message2 });
|
|
3544
3594
|
throw error;
|
|
3545
3595
|
} finally {
|
|
3546
3596
|
await rm2(dir, { recursive: true, force: true });
|
|
@@ -3771,10 +3821,10 @@ var MediaFolders = class {
|
|
|
3771
3821
|
async rows() {
|
|
3772
3822
|
if (this.cache && Date.now() - this.cache.at < TTL) return this.cache.rows;
|
|
3773
3823
|
const found = await this.db.find({ collection: MEDIA_FOLDERS, sort: [], limit: 0, page: 1 });
|
|
3774
|
-
const
|
|
3824
|
+
const rows2 = /* @__PURE__ */ new Map();
|
|
3775
3825
|
for (const doc of found.docs) {
|
|
3776
3826
|
const permissions = doc.permissions;
|
|
3777
|
-
|
|
3827
|
+
rows2.set(String(doc.id), {
|
|
3778
3828
|
id: doc.id,
|
|
3779
3829
|
parent: doc.parent ?? null,
|
|
3780
3830
|
name: String(doc.name ?? ""),
|
|
@@ -3784,31 +3834,31 @@ var MediaFolders = class {
|
|
|
3784
3834
|
doc
|
|
3785
3835
|
});
|
|
3786
3836
|
}
|
|
3787
|
-
this.cache = { at: Date.now(), rows };
|
|
3788
|
-
return
|
|
3837
|
+
this.cache = { at: Date.now(), rows: rows2 };
|
|
3838
|
+
return rows2;
|
|
3789
3839
|
}
|
|
3790
3840
|
/** A role's level in a folder (`null`: the top level): the nearest permissions up the tree. */
|
|
3791
|
-
levelIn(
|
|
3841
|
+
levelIn(rows2, role, id) {
|
|
3792
3842
|
const seen = /* @__PURE__ */ new Set();
|
|
3793
|
-
let current = id === null ? void 0 :
|
|
3843
|
+
let current = id === null ? void 0 : rows2.get(String(id));
|
|
3794
3844
|
while (current && !seen.has(String(current.id))) {
|
|
3795
3845
|
seen.add(String(current.id));
|
|
3796
3846
|
if (current.permissions) {
|
|
3797
3847
|
const level = current.permissions[role];
|
|
3798
3848
|
return level && level in RANK ? level : "none";
|
|
3799
3849
|
}
|
|
3800
|
-
current = current.parent === null ? void 0 :
|
|
3850
|
+
current = current.parent === null ? void 0 : rows2.get(String(current.parent));
|
|
3801
3851
|
}
|
|
3802
3852
|
return "manage";
|
|
3803
3853
|
}
|
|
3804
3854
|
/** Whether files in a folder (`null`: the top level) are private: it or a folder above is. */
|
|
3805
|
-
privateIn(
|
|
3855
|
+
privateIn(rows2, id) {
|
|
3806
3856
|
const seen = /* @__PURE__ */ new Set();
|
|
3807
|
-
let current = id === null ? void 0 :
|
|
3857
|
+
let current = id === null ? void 0 : rows2.get(String(id));
|
|
3808
3858
|
while (current && !seen.has(String(current.id))) {
|
|
3809
3859
|
if (current.private) return true;
|
|
3810
3860
|
seen.add(String(current.id));
|
|
3811
|
-
current = current.parent === null ? void 0 :
|
|
3861
|
+
current = current.parent === null ? void 0 : rows2.get(String(current.parent));
|
|
3812
3862
|
}
|
|
3813
3863
|
return false;
|
|
3814
3864
|
}
|
|
@@ -3818,26 +3868,26 @@ var MediaFolders = class {
|
|
|
3818
3868
|
return this.privateIn(await this.rows(), id);
|
|
3819
3869
|
}
|
|
3820
3870
|
/** The folder and every folder inside it. */
|
|
3821
|
-
within(
|
|
3871
|
+
within(rows2, ids) {
|
|
3822
3872
|
const out = new Set(ids.map(String));
|
|
3823
3873
|
let grew = true;
|
|
3824
3874
|
while (grew) {
|
|
3825
3875
|
grew = false;
|
|
3826
|
-
for (const row of
|
|
3876
|
+
for (const row of rows2.values())
|
|
3827
3877
|
if (row.parent !== null && out.has(String(row.parent)) && !out.has(String(row.id))) {
|
|
3828
3878
|
out.add(String(row.id));
|
|
3829
3879
|
grew = true;
|
|
3830
3880
|
}
|
|
3831
3881
|
}
|
|
3832
|
-
return [...
|
|
3882
|
+
return [...rows2.values()].filter((r) => out.has(String(r.id))).map((r) => r.id);
|
|
3833
3883
|
}
|
|
3834
3884
|
/**
|
|
3835
3885
|
* Folders whose privacy follows this one: it and those inside, with their privacy now.
|
|
3836
3886
|
* @internal
|
|
3837
3887
|
*/
|
|
3838
3888
|
async subtree(id) {
|
|
3839
|
-
const
|
|
3840
|
-
return this.within(
|
|
3889
|
+
const rows2 = await this.fresh();
|
|
3890
|
+
return this.within(rows2, [id]).map((f) => ({ id: f, private: this.privateIn(rows2, f) }));
|
|
3841
3891
|
}
|
|
3842
3892
|
/**
|
|
3843
3893
|
* The privacy each folder of a subtree would have after a change to its top folder: made
|
|
@@ -3845,15 +3895,15 @@ var MediaFolders = class {
|
|
|
3845
3895
|
* @internal
|
|
3846
3896
|
*/
|
|
3847
3897
|
async privacyAfter(id, change) {
|
|
3848
|
-
const
|
|
3849
|
-
const row =
|
|
3898
|
+
const rows2 = new Map(await this.fresh());
|
|
3899
|
+
const row = rows2.get(String(id));
|
|
3850
3900
|
if (!row) return [];
|
|
3851
|
-
|
|
3901
|
+
rows2.set(String(id), {
|
|
3852
3902
|
...row,
|
|
3853
3903
|
...change.private !== void 0 ? { private: change.private } : {},
|
|
3854
3904
|
...change.parent !== void 0 ? { parent: change.parent } : {}
|
|
3855
3905
|
});
|
|
3856
|
-
return this.within(
|
|
3906
|
+
return this.within(rows2, [id]).map((f) => ({ id: f, private: this.privateIn(rows2, f) }));
|
|
3857
3907
|
}
|
|
3858
3908
|
async keyed(key, scope = {}, create = true) {
|
|
3859
3909
|
const find = async () => [...(await this.rows()).values()].find((r) => r.key === key && inScope(r.doc, scope));
|
|
@@ -3891,9 +3941,9 @@ var MediaFolders = class {
|
|
|
3891
3941
|
/** Whether a file in `folder` is in the folder with this key, or below it. @internal */
|
|
3892
3942
|
async inKeyed(key, folder, scope = {}) {
|
|
3893
3943
|
if (folder === null) return false;
|
|
3894
|
-
const
|
|
3895
|
-
const root = [...
|
|
3896
|
-
return !!root && this.within(
|
|
3944
|
+
const rows2 = await this.rows();
|
|
3945
|
+
const root = [...rows2.values()].find((r) => r.key === key && inScope(r.doc, scope));
|
|
3946
|
+
return !!root && this.within(rows2, [root.id]).some((id) => String(id) === String(folder));
|
|
3897
3947
|
}
|
|
3898
3948
|
/** The user's level in a folder (`null`: the top level). */
|
|
3899
3949
|
async level(user, id) {
|
|
@@ -3916,11 +3966,11 @@ var MediaFolders = class {
|
|
|
3916
3966
|
if (!this.applies(user)) return true;
|
|
3917
3967
|
if (operation === "create") return true;
|
|
3918
3968
|
const need = RANK[needed(collection2, operation)];
|
|
3919
|
-
const
|
|
3969
|
+
const rows2 = await this.rows();
|
|
3920
3970
|
const allowed2 = [];
|
|
3921
3971
|
let all = true;
|
|
3922
|
-
for (const row of
|
|
3923
|
-
if (RANK[this.levelIn(
|
|
3972
|
+
for (const row of rows2.values()) {
|
|
3973
|
+
if (RANK[this.levelIn(rows2, user.role, row.id)] >= need) allowed2.push(row.id);
|
|
3924
3974
|
else all = false;
|
|
3925
3975
|
}
|
|
3926
3976
|
if (all) return true;
|
|
@@ -3948,12 +3998,12 @@ var MediaFolders = class {
|
|
|
3948
3998
|
* @internal
|
|
3949
3999
|
*/
|
|
3950
4000
|
async validate(data, self) {
|
|
3951
|
-
const
|
|
4001
|
+
const rows2 = await this.fresh();
|
|
3952
4002
|
const name = typeof data.name === "string" ? data.name.trim() : "";
|
|
3953
4003
|
const parent = data.parent ?? null;
|
|
3954
4004
|
const errors = [];
|
|
3955
4005
|
if (parent !== null && self !== void 0) {
|
|
3956
|
-
let current =
|
|
4006
|
+
let current = rows2.get(String(parent));
|
|
3957
4007
|
const seen = /* @__PURE__ */ new Set();
|
|
3958
4008
|
while (current && !seen.has(String(current.id))) {
|
|
3959
4009
|
if (String(current.id) === String(self)) {
|
|
@@ -3961,10 +4011,10 @@ var MediaFolders = class {
|
|
|
3961
4011
|
break;
|
|
3962
4012
|
}
|
|
3963
4013
|
seen.add(String(current.id));
|
|
3964
|
-
current = current.parent === null ? void 0 :
|
|
4014
|
+
current = current.parent === null ? void 0 : rows2.get(String(current.parent));
|
|
3965
4015
|
}
|
|
3966
4016
|
}
|
|
3967
|
-
const taken = [...
|
|
4017
|
+
const taken = [...rows2.values()].some(
|
|
3968
4018
|
(row) => String(row.id) !== String(self) && String(row.parent) === String(parent) && row.name.trim().toLocaleLowerCase() === name.toLocaleLowerCase()
|
|
3969
4019
|
);
|
|
3970
4020
|
if (name && taken) errors.push({ field: "name", message: "is already used in this folder" });
|
|
@@ -4218,7 +4268,7 @@ var VersionStore = class {
|
|
|
4218
4268
|
async latest(parent, docs) {
|
|
4219
4269
|
const result = /* @__PURE__ */ new Map();
|
|
4220
4270
|
if (docs.length === 0) return result;
|
|
4221
|
-
const
|
|
4271
|
+
const rows2 = await this.db.find({
|
|
4222
4272
|
collection: VERSIONS,
|
|
4223
4273
|
where: {
|
|
4224
4274
|
and: [
|
|
@@ -4231,7 +4281,7 @@ var VersionStore = class {
|
|
|
4231
4281
|
limit: 0,
|
|
4232
4282
|
page: 1
|
|
4233
4283
|
});
|
|
4234
|
-
for (const row of
|
|
4284
|
+
for (const row of rows2.docs) result.set(String(row.doc), this.toVersion(row));
|
|
4235
4285
|
return result;
|
|
4236
4286
|
}
|
|
4237
4287
|
async list(parent, doc, limit, page) {
|
|
@@ -4256,24 +4306,24 @@ var VersionStore = class {
|
|
|
4256
4306
|
return row ? this.toVersion(row) : null;
|
|
4257
4307
|
}
|
|
4258
4308
|
async deleteAll(parent, doc) {
|
|
4259
|
-
const
|
|
4309
|
+
const rows2 = await this.db.find({
|
|
4260
4310
|
collection: VERSIONS,
|
|
4261
4311
|
where: { and: this.of(parent, doc) },
|
|
4262
4312
|
sort: [],
|
|
4263
4313
|
limit: 0,
|
|
4264
4314
|
page: 1
|
|
4265
4315
|
});
|
|
4266
|
-
for (const row of
|
|
4316
|
+
for (const row of rows2.docs) await this.db.delete({ collection: VERSIONS, id: row.id });
|
|
4267
4317
|
}
|
|
4268
4318
|
async prune(parent, doc, max) {
|
|
4269
|
-
const
|
|
4319
|
+
const rows2 = await this.db.find({
|
|
4270
4320
|
collection: VERSIONS,
|
|
4271
4321
|
where: { and: this.of(parent, doc) },
|
|
4272
4322
|
sort: ["-id"],
|
|
4273
4323
|
limit: 0,
|
|
4274
4324
|
page: 1
|
|
4275
4325
|
});
|
|
4276
|
-
for (const row of
|
|
4326
|
+
for (const row of rows2.docs.slice(max)) {
|
|
4277
4327
|
await this.db.delete({ collection: VERSIONS, id: row.id });
|
|
4278
4328
|
}
|
|
4279
4329
|
}
|
|
@@ -4286,7 +4336,7 @@ var VersionStore = class {
|
|
|
4286
4336
|
};
|
|
4287
4337
|
|
|
4288
4338
|
// src/local-api.ts
|
|
4289
|
-
import { createHash as
|
|
4339
|
+
import { createHash as createHash6, createHmac as createHmac8, randomBytes as randomBytes7, randomUUID as randomUUID3 } from "crypto";
|
|
4290
4340
|
|
|
4291
4341
|
// src/auth/common-passwords.ts
|
|
4292
4342
|
var LIST = `
|
|
@@ -4803,6 +4853,649 @@ function toOperation(row) {
|
|
|
4803
4853
|
};
|
|
4804
4854
|
}
|
|
4805
4855
|
|
|
4856
|
+
// src/integrations.ts
|
|
4857
|
+
import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, createHash as createHash4, hkdfSync, randomBytes as randomBytes6 } from "crypto";
|
|
4858
|
+
var ENV_NAME = "Environment";
|
|
4859
|
+
var PENDING_MS = 10 * 60 * 1e3;
|
|
4860
|
+
var keyOf2 = (secret) => Buffer.from(hkdfSync("sha256", secret, "", "easy-cms:connections", 32));
|
|
4861
|
+
var keyId = (key) => createHash4("sha256").update(key).digest("hex").slice(0, 12);
|
|
4862
|
+
function encryptValues(secret, values) {
|
|
4863
|
+
const key = keyOf2(secret);
|
|
4864
|
+
const iv = randomBytes6(12);
|
|
4865
|
+
const cipher = createCipheriv2("aes-256-gcm", key, iv);
|
|
4866
|
+
const data = Buffer.concat([cipher.update(JSON.stringify(values), "utf8"), cipher.final()]);
|
|
4867
|
+
const box = {
|
|
4868
|
+
k: keyId(key),
|
|
4869
|
+
iv: iv.toString("base64"),
|
|
4870
|
+
t: cipher.getAuthTag().toString("base64"),
|
|
4871
|
+
d: data.toString("base64")
|
|
4872
|
+
};
|
|
4873
|
+
return Buffer.from(JSON.stringify(box)).toString("base64url");
|
|
4874
|
+
}
|
|
4875
|
+
function decryptValues(secrets, sealed) {
|
|
4876
|
+
const box = JSON.parse(Buffer.from(sealed, "base64url").toString("utf8"));
|
|
4877
|
+
for (const [i, secret] of secrets.entries()) {
|
|
4878
|
+
const key = keyOf2(secret);
|
|
4879
|
+
if (keyId(key) !== box.k) continue;
|
|
4880
|
+
const decipher = createDecipheriv2("aes-256-gcm", key, Buffer.from(String(box.iv), "base64"));
|
|
4881
|
+
decipher.setAuthTag(Buffer.from(String(box.t), "base64"));
|
|
4882
|
+
const text4 = Buffer.concat([
|
|
4883
|
+
decipher.update(Buffer.from(String(box.d), "base64")),
|
|
4884
|
+
decipher.final()
|
|
4885
|
+
]).toString("utf8");
|
|
4886
|
+
return { values: JSON.parse(text4), old: i > 0 };
|
|
4887
|
+
}
|
|
4888
|
+
throw new Error("A connection was encrypted with a secret that is no longer configured");
|
|
4889
|
+
}
|
|
4890
|
+
var keyIdOf = (sealed) => {
|
|
4891
|
+
try {
|
|
4892
|
+
return JSON.parse(Buffer.from(String(sealed), "base64url").toString("utf8")).k;
|
|
4893
|
+
} catch {
|
|
4894
|
+
return void 0;
|
|
4895
|
+
}
|
|
4896
|
+
};
|
|
4897
|
+
function signState(secret, payload) {
|
|
4898
|
+
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
4899
|
+
return `${body}.${signValue(secret, `integration:${body}`)}`;
|
|
4900
|
+
}
|
|
4901
|
+
function readState(secrets, state) {
|
|
4902
|
+
const [body, signature] = state.split(".");
|
|
4903
|
+
if (!body || !signature || !signedWith(secrets, `integration:${body}`, signature)) return null;
|
|
4904
|
+
const payload = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
|
|
4905
|
+
return typeof payload.exp === "number" && payload.exp > Date.now() ? payload : null;
|
|
4906
|
+
}
|
|
4907
|
+
var OFFICIAL_PLUGINS = [
|
|
4908
|
+
{
|
|
4909
|
+
name: "@easy-cms/plugin-social",
|
|
4910
|
+
category: "social",
|
|
4911
|
+
description: {
|
|
4912
|
+
en: "Post to Facebook, LINE and webhooks from the admin",
|
|
4913
|
+
th: "\u0E42\u0E1E\u0E2A\u0E15\u0E4C\u0E44\u0E1B Facebook, LINE \u0E41\u0E25\u0E30 webhook \u0E08\u0E32\u0E01 admin"
|
|
4914
|
+
},
|
|
4915
|
+
docs: "https://easycms.dev/docs/integrations"
|
|
4916
|
+
},
|
|
4917
|
+
{
|
|
4918
|
+
name: "@easy-cms/plugin-notify",
|
|
4919
|
+
category: "notifications",
|
|
4920
|
+
description: { en: "Telegram notifications for admins", th: "\u0E41\u0E08\u0E49\u0E07\u0E40\u0E15\u0E37\u0E2D\u0E19\u0E1C\u0E39\u0E49\u0E14\u0E39\u0E41\u0E25\u0E17\u0E32\u0E07 Telegram" },
|
|
4921
|
+
docs: "https://easycms.dev/docs/notifications"
|
|
4922
|
+
},
|
|
4923
|
+
{
|
|
4924
|
+
name: "@easy-cms/plugin-ai",
|
|
4925
|
+
category: "ai",
|
|
4926
|
+
description: {
|
|
4927
|
+
en: "Claude, GPT and Gemini for alt text, SEO and translation",
|
|
4928
|
+
th: "Claude, GPT \u0E41\u0E25\u0E30 Gemini \u0E2A\u0E33\u0E2B\u0E23\u0E31\u0E1A alt text, SEO \u0E41\u0E25\u0E30\u0E41\u0E1B\u0E25\u0E20\u0E32\u0E29\u0E32"
|
|
4929
|
+
},
|
|
4930
|
+
docs: "https://easycms.dev/docs/integrations"
|
|
4931
|
+
},
|
|
4932
|
+
{
|
|
4933
|
+
name: "@easy-cms/plugin-ecommerce",
|
|
4934
|
+
category: "payments",
|
|
4935
|
+
description: { en: "A shop with Stripe payments", th: "\u0E23\u0E49\u0E32\u0E19\u0E04\u0E49\u0E32\u0E1E\u0E23\u0E49\u0E2D\u0E21\u0E0A\u0E33\u0E23\u0E30\u0E40\u0E07\u0E34\u0E19\u0E1C\u0E48\u0E32\u0E19 Stripe" },
|
|
4936
|
+
docs: "https://easycms.dev/docs/ecommerce"
|
|
4937
|
+
}
|
|
4938
|
+
];
|
|
4939
|
+
var mask = (value) => value.length > 4 ? `\u2022\u2022\u2022\u2022${value.slice(-4)}` : "\u2022\u2022\u2022\u2022";
|
|
4940
|
+
var Integrations = class {
|
|
4941
|
+
constructor(cms) {
|
|
4942
|
+
this.cms = cms;
|
|
4943
|
+
}
|
|
4944
|
+
cms;
|
|
4945
|
+
get config() {
|
|
4946
|
+
return this.cms.config;
|
|
4947
|
+
}
|
|
4948
|
+
get secrets() {
|
|
4949
|
+
return secretsOf(this.config);
|
|
4950
|
+
}
|
|
4951
|
+
/** The integrations plugins declared. */
|
|
4952
|
+
get definitions() {
|
|
4953
|
+
return this.config.integrations;
|
|
4954
|
+
}
|
|
4955
|
+
definition(id) {
|
|
4956
|
+
const found = this.definitions.find((d) => d.id === id);
|
|
4957
|
+
if (!found) throw new NotFoundError("integrations", id);
|
|
4958
|
+
return found;
|
|
4959
|
+
}
|
|
4960
|
+
async rows(id, kind) {
|
|
4961
|
+
const found = await this.cms.db.find({
|
|
4962
|
+
collection: CONNECTIONS,
|
|
4963
|
+
where: { and: [{ integration: { equals: id } }, { kind: { equals: kind } }] },
|
|
4964
|
+
sort: ["id"],
|
|
4965
|
+
limit: 0,
|
|
4966
|
+
page: 1
|
|
4967
|
+
});
|
|
4968
|
+
return found.docs;
|
|
4969
|
+
}
|
|
4970
|
+
/** The values of the fields set in the environment. */
|
|
4971
|
+
envValues(def) {
|
|
4972
|
+
const out = {};
|
|
4973
|
+
for (const [field, name] of Object.entries(def.env ?? {})) {
|
|
4974
|
+
const value = process.env[name];
|
|
4975
|
+
if (value) out[field] = value;
|
|
4976
|
+
}
|
|
4977
|
+
return out;
|
|
4978
|
+
}
|
|
4979
|
+
/** Every field is in the environment: an "Environment" connection. */
|
|
4980
|
+
envComplete(def) {
|
|
4981
|
+
const env = this.envValues(def);
|
|
4982
|
+
const fields = def.fields ?? [];
|
|
4983
|
+
return fields.length > 0 && fields.every((f) => env[f.name] || !f.required) && Object.keys(env).length > 0;
|
|
4984
|
+
}
|
|
4985
|
+
async enabled(id) {
|
|
4986
|
+
const [row] = await this.rows(id, "integration");
|
|
4987
|
+
return row ? row.enabled !== false : true;
|
|
4988
|
+
}
|
|
4989
|
+
decrypt(row) {
|
|
4990
|
+
const plain = row.values ?? {};
|
|
4991
|
+
if (!row.secret) return { ...plain };
|
|
4992
|
+
const { values, old } = decryptValues(this.secrets, String(row.secret));
|
|
4993
|
+
if (old) void this.reseal(row, values);
|
|
4994
|
+
return { ...plain, ...values };
|
|
4995
|
+
}
|
|
4996
|
+
async reseal(row, secrets) {
|
|
4997
|
+
await this.cms.db.update({
|
|
4998
|
+
collection: CONNECTIONS,
|
|
4999
|
+
id: row.id,
|
|
5000
|
+
data: { ...row, secret: encryptValues(this.config.secret, secrets) }
|
|
5001
|
+
});
|
|
5002
|
+
}
|
|
5003
|
+
/** Splits values into the plain ones and the secret ones (encrypted). */
|
|
5004
|
+
seal(def, values) {
|
|
5005
|
+
const secretNames = new Set((def.fields ?? []).filter((f) => f.secret).map((f) => f.name));
|
|
5006
|
+
const plain = {};
|
|
5007
|
+
const secret = {};
|
|
5008
|
+
for (const [name, value] of Object.entries(values))
|
|
5009
|
+
(secretNames.has(name) || def.kind === "oauth" ? secret : plain)[name] = value;
|
|
5010
|
+
return {
|
|
5011
|
+
values: plain,
|
|
5012
|
+
secret: Object.keys(secret).length ? encryptValues(this.config.secret, secret) : null
|
|
5013
|
+
};
|
|
5014
|
+
}
|
|
5015
|
+
/**
|
|
5016
|
+
* A connection for a plugin: the default one, or the one named; `null` when there is none
|
|
5017
|
+
* or the integration is turned off. Values from the environment win.
|
|
5018
|
+
*/
|
|
5019
|
+
async get(id, options = {}) {
|
|
5020
|
+
const def = this.definition(id);
|
|
5021
|
+
if (!await this.enabled(id)) return null;
|
|
5022
|
+
const env = this.envValues(def);
|
|
5023
|
+
if (this.envComplete(def) && (!options.name || options.name === ENV_NAME))
|
|
5024
|
+
return { name: ENV_NAME, values: env, env: true };
|
|
5025
|
+
const rows2 = await this.rows(id, "connection");
|
|
5026
|
+
const row = options.name ? rows2.find((r) => r.name === options.name) : rows2.find((r) => r.isDefault === true) ?? rows2[0];
|
|
5027
|
+
if (!row) return null;
|
|
5028
|
+
return { name: String(row.name), values: { ...this.decrypt(row), ...env }, env: false };
|
|
5029
|
+
}
|
|
5030
|
+
/**
|
|
5031
|
+
* Every connection of an integration (the environment's first), e.g. to notify each chat;
|
|
5032
|
+
* none when it is turned off.
|
|
5033
|
+
*/
|
|
5034
|
+
async all(id) {
|
|
5035
|
+
const def = this.definition(id);
|
|
5036
|
+
if (!await this.enabled(id)) return [];
|
|
5037
|
+
const env = this.envValues(def);
|
|
5038
|
+
const out = this.envComplete(def) ? [{ name: ENV_NAME, values: env, env: true }] : [];
|
|
5039
|
+
for (const row of await this.rows(id, "connection"))
|
|
5040
|
+
out.push({ name: String(row.name), values: { ...this.decrypt(row), ...env }, env: false });
|
|
5041
|
+
return out;
|
|
5042
|
+
}
|
|
5043
|
+
/** The connection was used: its card says when, and an earlier error is gone. */
|
|
5044
|
+
async markUsed(id, name) {
|
|
5045
|
+
await this.mark(id, name, { lastUsedAt: (/* @__PURE__ */ new Date()).toISOString(), lastError: null });
|
|
5046
|
+
}
|
|
5047
|
+
/** It failed: the card and the dashboard's "needs attention" say why. */
|
|
5048
|
+
async markError(id, message2, name) {
|
|
5049
|
+
await this.mark(id, name, { lastError: message2.slice(0, 500) });
|
|
5050
|
+
this.cms.systemEvent("integration.failed", {
|
|
5051
|
+
integration: id,
|
|
5052
|
+
name: name ?? null,
|
|
5053
|
+
error: message2
|
|
5054
|
+
});
|
|
5055
|
+
}
|
|
5056
|
+
async mark(id, name, patch) {
|
|
5057
|
+
const rows2 = await this.rows(id, "connection");
|
|
5058
|
+
const row = name ? rows2.find((r) => r.name === name) : rows2.find((r) => r.isDefault === true) ?? rows2[0];
|
|
5059
|
+
if (!row) return;
|
|
5060
|
+
await this.cms.db.update({
|
|
5061
|
+
collection: CONNECTIONS,
|
|
5062
|
+
id: row.id,
|
|
5063
|
+
data: { ...row, ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
5064
|
+
});
|
|
5065
|
+
}
|
|
5066
|
+
// --- The admin's page ------------------------------------------------------------------------
|
|
5067
|
+
async overview() {
|
|
5068
|
+
const integrations = [];
|
|
5069
|
+
const current = keyId(keyOf2(this.config.secret));
|
|
5070
|
+
let stale = 0;
|
|
5071
|
+
for (const def of this.definitions) {
|
|
5072
|
+
const rows2 = await this.rows(def.id, "connection");
|
|
5073
|
+
const env = this.envValues(def);
|
|
5074
|
+
const connections = rows2.map((row) => {
|
|
5075
|
+
if (row.secret && keyIdOf(row.secret) !== current) stale++;
|
|
5076
|
+
const secretNames = (def.fields ?? []).filter((f) => f.secret).map((f) => f.name);
|
|
5077
|
+
let values = { ...row.values ?? {} };
|
|
5078
|
+
try {
|
|
5079
|
+
const all = this.decrypt(row);
|
|
5080
|
+
for (const name of secretNames) if (all[name]) values[name] = mask(all[name]);
|
|
5081
|
+
} catch {
|
|
5082
|
+
values = { ...values };
|
|
5083
|
+
}
|
|
5084
|
+
return {
|
|
5085
|
+
id: row.id,
|
|
5086
|
+
name: String(row.name),
|
|
5087
|
+
isDefault: row.isDefault === true,
|
|
5088
|
+
env: false,
|
|
5089
|
+
account: row.account ?? null,
|
|
5090
|
+
lastUsedAt: row.lastUsedAt ?? null,
|
|
5091
|
+
lastError: row.lastError ?? null,
|
|
5092
|
+
values
|
|
5093
|
+
};
|
|
5094
|
+
});
|
|
5095
|
+
if (this.envComplete(def)) {
|
|
5096
|
+
for (const c of connections) c.isDefault = false;
|
|
5097
|
+
connections.unshift({
|
|
5098
|
+
id: "env",
|
|
5099
|
+
name: ENV_NAME,
|
|
5100
|
+
isDefault: true,
|
|
5101
|
+
env: true,
|
|
5102
|
+
account: null,
|
|
5103
|
+
lastUsedAt: null,
|
|
5104
|
+
lastError: null,
|
|
5105
|
+
values: Object.fromEntries(
|
|
5106
|
+
Object.entries(env).map(
|
|
5107
|
+
([k, v]) => (def.fields ?? []).find((f) => f.name === k)?.secret ? [k, mask(v)] : [k, v]
|
|
5108
|
+
)
|
|
5109
|
+
)
|
|
5110
|
+
});
|
|
5111
|
+
}
|
|
5112
|
+
integrations.push({
|
|
5113
|
+
id: def.id,
|
|
5114
|
+
label: def.label,
|
|
5115
|
+
category: def.category,
|
|
5116
|
+
...def.description ? { description: def.description } : {},
|
|
5117
|
+
...def.icon ? { icon: def.icon } : {},
|
|
5118
|
+
...def.docs ? { docs: def.docs } : {},
|
|
5119
|
+
...def.plugin ? { plugin: def.plugin } : {},
|
|
5120
|
+
kind: def.kind,
|
|
5121
|
+
// Functions don't travel: `findable` says a field has a Find button.
|
|
5122
|
+
fields: (def.fields ?? []).map(({ lookup, ...field }) => ({
|
|
5123
|
+
...field,
|
|
5124
|
+
...lookup ? { findable: true } : {}
|
|
5125
|
+
})),
|
|
5126
|
+
enabled: await this.enabled(def.id),
|
|
5127
|
+
envFields: Object.keys(env),
|
|
5128
|
+
testable: typeof def.test === "function",
|
|
5129
|
+
connections
|
|
5130
|
+
});
|
|
5131
|
+
}
|
|
5132
|
+
const installed = new Set(this.config.installedPlugins.map((p) => p.name));
|
|
5133
|
+
return {
|
|
5134
|
+
integrations,
|
|
5135
|
+
configured: this.configured(),
|
|
5136
|
+
catalog: OFFICIAL_PLUGINS.filter((p) => !installed.has(p.name)),
|
|
5137
|
+
staleSecrets: stale
|
|
5138
|
+
};
|
|
5139
|
+
}
|
|
5140
|
+
/** The adapters the config sets: email, storage, search, sign-in, payments. */
|
|
5141
|
+
configured() {
|
|
5142
|
+
const out = [];
|
|
5143
|
+
const { config } = this;
|
|
5144
|
+
if (config.email) out.push({ id: "email", kind: "email", name: config.email.name ?? "email" });
|
|
5145
|
+
out.push({ id: "storage", kind: "storage", name: config.upload.storage?.name ?? "disk" });
|
|
5146
|
+
if (config.search)
|
|
5147
|
+
out.push({ id: "search", kind: "search", name: config.search.name ?? "search" });
|
|
5148
|
+
for (const provider of config.auth.providers ?? [])
|
|
5149
|
+
out.push({ id: `sign-in:${provider.id}`, kind: "sign-in", name: provider.id });
|
|
5150
|
+
if (config.installedPlugins.some((p) => p.name === "@easy-cms/plugin-ecommerce"))
|
|
5151
|
+
out.push({ id: "payments", kind: "payments", name: "Stripe" });
|
|
5152
|
+
return out;
|
|
5153
|
+
}
|
|
5154
|
+
async audit(user, action, id, detail = {}) {
|
|
5155
|
+
await this.cms.audit.record({ action: `integration.${action}`, target: id, user, detail });
|
|
5156
|
+
}
|
|
5157
|
+
checkFields(def, values, partial) {
|
|
5158
|
+
const errors = (def.fields ?? []).filter((f) => f.required && !partial && !values[f.name]).map((f) => ({ field: f.name, message: "is required" }));
|
|
5159
|
+
if (errors.length) throw new ValidationError(CONNECTIONS, errors);
|
|
5160
|
+
}
|
|
5161
|
+
cleanValues(def, raw) {
|
|
5162
|
+
const input = raw && typeof raw === "object" ? raw : {};
|
|
5163
|
+
const out = {};
|
|
5164
|
+
for (const field of def.fields ?? []) {
|
|
5165
|
+
const raw2 = input[field.name];
|
|
5166
|
+
const value = Array.isArray(raw2) ? raw2.filter((v) => typeof v === "string").join(",") : raw2;
|
|
5167
|
+
if (typeof value === "string" && value.trim() !== "") out[field.name] = value.trim();
|
|
5168
|
+
}
|
|
5169
|
+
return out;
|
|
5170
|
+
}
|
|
5171
|
+
async uniqueName(id, name, self) {
|
|
5172
|
+
const trimmed = name.trim();
|
|
5173
|
+
if (!trimmed || trimmed.length > 60)
|
|
5174
|
+
throw new ValidationError(CONNECTIONS, [
|
|
5175
|
+
{ field: "name", message: "must be 1 to 60 characters" }
|
|
5176
|
+
]);
|
|
5177
|
+
if (trimmed === ENV_NAME || (await this.rows(id, "connection")).some((r) => r.name === trimmed && r.id !== self))
|
|
5178
|
+
throw new ValidationError(CONNECTIONS, [
|
|
5179
|
+
{ field: "name", message: `there is already a connection named "${trimmed}"` }
|
|
5180
|
+
]);
|
|
5181
|
+
return trimmed;
|
|
5182
|
+
}
|
|
5183
|
+
async create(def, name, values, account) {
|
|
5184
|
+
const existing = await this.rows(def.id, "connection");
|
|
5185
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5186
|
+
return await this.cms.db.create({
|
|
5187
|
+
collection: CONNECTIONS,
|
|
5188
|
+
data: {
|
|
5189
|
+
kind: "connection",
|
|
5190
|
+
integration: def.id,
|
|
5191
|
+
name,
|
|
5192
|
+
isDefault: existing.length === 0,
|
|
5193
|
+
...this.seal(def, values),
|
|
5194
|
+
account,
|
|
5195
|
+
lastUsedAt: null,
|
|
5196
|
+
lastError: null,
|
|
5197
|
+
enabled: null,
|
|
5198
|
+
createdAt: now,
|
|
5199
|
+
updatedAt: now
|
|
5200
|
+
}
|
|
5201
|
+
});
|
|
5202
|
+
}
|
|
5203
|
+
/** Connects with pasted values (`kind: 'fields'`); tested first when the plugin can. */
|
|
5204
|
+
async connect(id, body, user) {
|
|
5205
|
+
const def = this.definition(id);
|
|
5206
|
+
if (def.kind !== "fields") throw new QueryError(`${id} connects with its Connect button`);
|
|
5207
|
+
const input = body ?? {};
|
|
5208
|
+
const name = await this.uniqueName(id, String(input.name ?? ""));
|
|
5209
|
+
const values = this.cleanValues(def, input.values);
|
|
5210
|
+
this.checkFields(def, values, false);
|
|
5211
|
+
const result = def.test ? await def.test(values, { cms: this.cms }) : null;
|
|
5212
|
+
if (result && !result.ok)
|
|
5213
|
+
throw new ValidationError(CONNECTIONS, [
|
|
5214
|
+
{ field: "values", message: result.message ?? "the provider refused these values" }
|
|
5215
|
+
]);
|
|
5216
|
+
const row = await this.create(def, name, values, result?.account ?? null);
|
|
5217
|
+
await this.audit(user, "connect", id, { name });
|
|
5218
|
+
return { id: row.id };
|
|
5219
|
+
}
|
|
5220
|
+
async row(id, cid) {
|
|
5221
|
+
const row = await this.cms.db.findById({ collection: CONNECTIONS, id: cid });
|
|
5222
|
+
if (!row || row.integration !== id || row.kind !== "connection")
|
|
5223
|
+
throw new NotFoundError(CONNECTIONS, cid);
|
|
5224
|
+
return row;
|
|
5225
|
+
}
|
|
5226
|
+
/** A new name or values (secrets left empty are kept), or the default. */
|
|
5227
|
+
async update(id, cid, body, user) {
|
|
5228
|
+
const def = this.definition(id);
|
|
5229
|
+
const row = await this.row(id, cid);
|
|
5230
|
+
const input = body ?? {};
|
|
5231
|
+
const next = { ...row, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5232
|
+
if (input.name !== void 0) next.name = await this.uniqueName(id, String(input.name), cid);
|
|
5233
|
+
if (input.values !== void 0) {
|
|
5234
|
+
const values = { ...this.decrypt(row), ...this.cleanValues(def, input.values) };
|
|
5235
|
+
this.checkFields(def, values, false);
|
|
5236
|
+
Object.assign(next, this.seal(def, values), { lastError: null });
|
|
5237
|
+
}
|
|
5238
|
+
if (input.isDefault === true) {
|
|
5239
|
+
for (const other of await this.rows(id, "connection"))
|
|
5240
|
+
if (other.id !== cid && other.isDefault === true)
|
|
5241
|
+
await this.cms.db.update({
|
|
5242
|
+
collection: CONNECTIONS,
|
|
5243
|
+
id: other.id,
|
|
5244
|
+
data: { ...other, isDefault: false }
|
|
5245
|
+
});
|
|
5246
|
+
next.isDefault = true;
|
|
5247
|
+
}
|
|
5248
|
+
await this.cms.db.update({ collection: CONNECTIONS, id: cid, data: next });
|
|
5249
|
+
await this.audit(user, input.isDefault === true ? "default" : "update", id, { name: next.name });
|
|
5250
|
+
return { ok: true };
|
|
5251
|
+
}
|
|
5252
|
+
/** Removes the connection and its secrets; the oldest left becomes the default. */
|
|
5253
|
+
async disconnect(id, cid, user) {
|
|
5254
|
+
const row = await this.row(id, cid);
|
|
5255
|
+
await this.cms.db.delete({ collection: CONNECTIONS, id: cid });
|
|
5256
|
+
if (row.isDefault === true) {
|
|
5257
|
+
const [oldest] = await this.rows(id, "connection");
|
|
5258
|
+
if (oldest)
|
|
5259
|
+
await this.cms.db.update({
|
|
5260
|
+
collection: CONNECTIONS,
|
|
5261
|
+
id: oldest.id,
|
|
5262
|
+
data: { ...oldest, isDefault: true }
|
|
5263
|
+
});
|
|
5264
|
+
}
|
|
5265
|
+
await this.audit(user, "disconnect", id, { name: row.name });
|
|
5266
|
+
return { deleted: 1 };
|
|
5267
|
+
}
|
|
5268
|
+
/** Turns the integration on or off; its connections are kept. */
|
|
5269
|
+
async setEnabled(id, enabled, user) {
|
|
5270
|
+
this.definition(id);
|
|
5271
|
+
const [row] = await this.rows(id, "integration");
|
|
5272
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5273
|
+
if (row)
|
|
5274
|
+
await this.cms.db.update({
|
|
5275
|
+
collection: CONNECTIONS,
|
|
5276
|
+
id: row.id,
|
|
5277
|
+
data: { ...row, enabled, updatedAt: now }
|
|
5278
|
+
});
|
|
5279
|
+
else
|
|
5280
|
+
await this.cms.db.create({
|
|
5281
|
+
collection: CONNECTIONS,
|
|
5282
|
+
data: {
|
|
5283
|
+
kind: "integration",
|
|
5284
|
+
integration: id,
|
|
5285
|
+
name: null,
|
|
5286
|
+
isDefault: false,
|
|
5287
|
+
values: null,
|
|
5288
|
+
secret: null,
|
|
5289
|
+
account: null,
|
|
5290
|
+
lastUsedAt: null,
|
|
5291
|
+
lastError: null,
|
|
5292
|
+
enabled,
|
|
5293
|
+
createdAt: now,
|
|
5294
|
+
updatedAt: now
|
|
5295
|
+
}
|
|
5296
|
+
});
|
|
5297
|
+
await this.audit(user, enabled ? "enable" : "disable", id);
|
|
5298
|
+
return { enabled };
|
|
5299
|
+
}
|
|
5300
|
+
/** Checks a connection's values with the provider; the card shows the result. */
|
|
5301
|
+
async test(id, cid) {
|
|
5302
|
+
const def = this.definition(id);
|
|
5303
|
+
if (!def.test) throw new QueryError(`${id} can't be tested`);
|
|
5304
|
+
const values = cid === "env" ? this.envValues(def) : { ...this.decrypt(await this.row(id, cid)), ...this.envValues(def) };
|
|
5305
|
+
let result;
|
|
5306
|
+
try {
|
|
5307
|
+
result = await def.test(values, { cms: this.cms });
|
|
5308
|
+
} catch (error) {
|
|
5309
|
+
result = { ok: false, message: error.message };
|
|
5310
|
+
}
|
|
5311
|
+
if (cid !== "env") {
|
|
5312
|
+
const row = await this.row(id, cid);
|
|
5313
|
+
await this.cms.db.update({
|
|
5314
|
+
collection: CONNECTIONS,
|
|
5315
|
+
id: cid,
|
|
5316
|
+
data: {
|
|
5317
|
+
...row,
|
|
5318
|
+
...result.ok ? { lastError: null, ...result.account ? { account: result.account } : {} } : { lastError: result.message ?? "Test failed" },
|
|
5319
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5320
|
+
}
|
|
5321
|
+
});
|
|
5322
|
+
if (!result.ok)
|
|
5323
|
+
this.cms.systemEvent("integration.failed", {
|
|
5324
|
+
integration: id,
|
|
5325
|
+
name: row.name ?? null,
|
|
5326
|
+
error: result.message ?? "Test failed"
|
|
5327
|
+
});
|
|
5328
|
+
}
|
|
5329
|
+
return result;
|
|
5330
|
+
}
|
|
5331
|
+
/**
|
|
5332
|
+
* A field's choices from the provider, with the values typed (and, editing a connection, its
|
|
5333
|
+
* stored secrets for the fields left empty).
|
|
5334
|
+
*/
|
|
5335
|
+
async lookup(id, fieldName, body) {
|
|
5336
|
+
const def = this.definition(id);
|
|
5337
|
+
const field = (def.fields ?? []).find((f) => f.name === fieldName);
|
|
5338
|
+
if (!field?.lookup) throw new QueryError(`${id}.${fieldName} has nothing to look up`);
|
|
5339
|
+
const input = body ?? {};
|
|
5340
|
+
const stored = input.connection !== void 0 && input.connection !== null ? this.decrypt(await this.row(id, input.connection)) : {};
|
|
5341
|
+
const values = { ...stored, ...this.envValues(def), ...this.cleanValues(def, input.values) };
|
|
5342
|
+
try {
|
|
5343
|
+
return { choices: [...await field.lookup(values, { cms: this.cms })].slice(0, 100) };
|
|
5344
|
+
} catch (error) {
|
|
5345
|
+
throw new ValidationError(CONNECTIONS, [
|
|
5346
|
+
{ field: fieldName, message: error.message }
|
|
5347
|
+
]);
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
// --- OAuth -----------------------------------------------------------------------------------
|
|
5351
|
+
/** The provider's sign-in page to send the admin to. */
|
|
5352
|
+
authorize(id, user, redirectUri, name) {
|
|
5353
|
+
const def = this.definition(id);
|
|
5354
|
+
if (def.kind !== "oauth" || !def.oauth)
|
|
5355
|
+
throw new QueryError(`${id} connects with pasted values`);
|
|
5356
|
+
const state = signState(this.config.secret, {
|
|
5357
|
+
i: id,
|
|
5358
|
+
u: String(user.id),
|
|
5359
|
+
n: name,
|
|
5360
|
+
exp: Date.now() + PENDING_MS
|
|
5361
|
+
});
|
|
5362
|
+
return { url: def.oauth.authorizeUrl({ redirectUri, state }) };
|
|
5363
|
+
}
|
|
5364
|
+
/**
|
|
5365
|
+
* Back from the provider: one account is connected at once; several wait (encrypted, 10
|
|
5366
|
+
* minutes) for the admin to choose. Returns where the admin goes next.
|
|
5367
|
+
*/
|
|
5368
|
+
async callback(id, code, stateText, redirectUri, user) {
|
|
5369
|
+
const def = this.definition(id);
|
|
5370
|
+
const state = readState(this.secrets, stateText);
|
|
5371
|
+
if (!state || state.i !== id || state.u !== String(user.id) || !def.oauth)
|
|
5372
|
+
throw new ForbiddenError(
|
|
5373
|
+
"This connection link has expired: start again from the Integrations page"
|
|
5374
|
+
);
|
|
5375
|
+
const result = await def.oauth.exchange({ code, redirectUri });
|
|
5376
|
+
const name = typeof state.n === "string" && state.n ? state.n : String(def.label);
|
|
5377
|
+
if (result.accounts && result.accounts.length > 0) {
|
|
5378
|
+
if (result.accounts.length === 1) {
|
|
5379
|
+
const [account] = result.accounts;
|
|
5380
|
+
await this.create(
|
|
5381
|
+
def,
|
|
5382
|
+
await this.uniqueName(id, name),
|
|
5383
|
+
{ ...account.values },
|
|
5384
|
+
account.name
|
|
5385
|
+
);
|
|
5386
|
+
await this.audit(user, "connect", id, { name });
|
|
5387
|
+
return { connected: true };
|
|
5388
|
+
}
|
|
5389
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5390
|
+
const pending = await this.cms.db.create({
|
|
5391
|
+
collection: CONNECTIONS,
|
|
5392
|
+
data: {
|
|
5393
|
+
kind: "pending",
|
|
5394
|
+
integration: id,
|
|
5395
|
+
name,
|
|
5396
|
+
isDefault: false,
|
|
5397
|
+
values: {
|
|
5398
|
+
user: String(user.id),
|
|
5399
|
+
expires: String(Date.now() + PENDING_MS),
|
|
5400
|
+
accounts: JSON.stringify(result.accounts.map((a) => ({ id: a.id, name: a.name })))
|
|
5401
|
+
},
|
|
5402
|
+
secret: encryptValues(
|
|
5403
|
+
this.config.secret,
|
|
5404
|
+
Object.fromEntries(result.accounts.map((a) => [a.id, JSON.stringify(a.values)]))
|
|
5405
|
+
),
|
|
5406
|
+
account: null,
|
|
5407
|
+
lastUsedAt: null,
|
|
5408
|
+
lastError: null,
|
|
5409
|
+
enabled: null,
|
|
5410
|
+
createdAt: now,
|
|
5411
|
+
updatedAt: now
|
|
5412
|
+
}
|
|
5413
|
+
});
|
|
5414
|
+
return { pending: pending.id };
|
|
5415
|
+
}
|
|
5416
|
+
await this.create(
|
|
5417
|
+
def,
|
|
5418
|
+
await this.uniqueName(id, name),
|
|
5419
|
+
{ ...result.values ?? {} },
|
|
5420
|
+
result.account ?? null
|
|
5421
|
+
);
|
|
5422
|
+
await this.audit(user, "connect", id, { name });
|
|
5423
|
+
return { connected: true };
|
|
5424
|
+
}
|
|
5425
|
+
/** The accounts waiting to be chosen from, by name only. */
|
|
5426
|
+
async pending(id, pid, user) {
|
|
5427
|
+
const row = await this.pendingRow(id, pid, user);
|
|
5428
|
+
const values = row.values;
|
|
5429
|
+
return {
|
|
5430
|
+
name: String(row.name),
|
|
5431
|
+
accounts: JSON.parse(values.accounts ?? "[]")
|
|
5432
|
+
};
|
|
5433
|
+
}
|
|
5434
|
+
async pendingRow(id, pid, user) {
|
|
5435
|
+
const row = await this.cms.db.findById({ collection: CONNECTIONS, id: pid });
|
|
5436
|
+
const values = row?.values ?? {};
|
|
5437
|
+
if (!row || row.kind !== "pending" || row.integration !== id || values.user !== String(user.id) || Number(values.expires) < Date.now())
|
|
5438
|
+
throw new NotFoundError(CONNECTIONS, pid);
|
|
5439
|
+
return row;
|
|
5440
|
+
}
|
|
5441
|
+
/** Connects the account chosen. */
|
|
5442
|
+
async choose(id, pid, accountId, user) {
|
|
5443
|
+
const def = this.definition(id);
|
|
5444
|
+
const row = await this.pendingRow(id, pid, user);
|
|
5445
|
+
const secrets = decryptValues(this.secrets, String(row.secret)).values;
|
|
5446
|
+
const chosen = secrets[accountId];
|
|
5447
|
+
const accounts = JSON.parse(
|
|
5448
|
+
String(row.values.accounts ?? "[]")
|
|
5449
|
+
);
|
|
5450
|
+
const account = accounts.find((a) => a.id === accountId);
|
|
5451
|
+
if (!chosen || !account) throw new NotFoundError("accounts", accountId);
|
|
5452
|
+
await this.cms.db.delete({ collection: CONNECTIONS, id: pid });
|
|
5453
|
+
const name = await this.uniqueName(id, String(row.name));
|
|
5454
|
+
const created = await this.create(
|
|
5455
|
+
def,
|
|
5456
|
+
name,
|
|
5457
|
+
JSON.parse(chosen),
|
|
5458
|
+
account.name
|
|
5459
|
+
);
|
|
5460
|
+
await this.audit(user, "connect", id, { name });
|
|
5461
|
+
return { id: created.id };
|
|
5462
|
+
}
|
|
5463
|
+
// --- Rotating the secret ---------------------------------------------------------------------
|
|
5464
|
+
/** Encrypts again, with the current secret, every connection still on an older one. */
|
|
5465
|
+
async rotate() {
|
|
5466
|
+
const current = keyId(keyOf2(this.config.secret));
|
|
5467
|
+
const found = await this.cms.db.find({
|
|
5468
|
+
collection: CONNECTIONS,
|
|
5469
|
+
limit: 0,
|
|
5470
|
+
page: 1,
|
|
5471
|
+
sort: ["id"]
|
|
5472
|
+
});
|
|
5473
|
+
let resealed = 0;
|
|
5474
|
+
for (const row of found.docs) {
|
|
5475
|
+
if (!row.secret || keyIdOf(row.secret) === current) continue;
|
|
5476
|
+
const { values } = decryptValues(this.secrets, String(row.secret));
|
|
5477
|
+
await this.reseal(row, values);
|
|
5478
|
+
resealed++;
|
|
5479
|
+
}
|
|
5480
|
+
return { resealed };
|
|
5481
|
+
}
|
|
5482
|
+
/** Connections whose test or last use failed, for the dashboard's "needs attention". */
|
|
5483
|
+
async broken() {
|
|
5484
|
+
const found = await this.cms.db.find({
|
|
5485
|
+
collection: CONNECTIONS,
|
|
5486
|
+
where: { and: [{ kind: { equals: "connection" } }, { lastError: { exists: true } }] },
|
|
5487
|
+
limit: 0,
|
|
5488
|
+
page: 1,
|
|
5489
|
+
sort: ["id"]
|
|
5490
|
+
});
|
|
5491
|
+
return found.docs.filter((r) => this.definitions.some((d) => d.id === r.integration)).map((r) => ({
|
|
5492
|
+
integration: String(r.integration),
|
|
5493
|
+
name: String(r.name),
|
|
5494
|
+
error: String(r.lastError)
|
|
5495
|
+
}));
|
|
5496
|
+
}
|
|
5497
|
+
};
|
|
5498
|
+
|
|
4806
5499
|
// src/localization.ts
|
|
4807
5500
|
var ALL_LOCALES = "all";
|
|
4808
5501
|
var isPlainObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -4847,12 +5540,12 @@ function toLocaleMaps(fields, input, current, locale) {
|
|
|
4847
5540
|
locale
|
|
4848
5541
|
);
|
|
4849
5542
|
} else if (hasRows(field) && Array.isArray(value)) {
|
|
4850
|
-
const
|
|
5543
|
+
const rows2 = new Map(
|
|
4851
5544
|
(Array.isArray(previous) ? previous : []).filter(isPlainObject2).map((row) => [String(row.id), row])
|
|
4852
5545
|
);
|
|
4853
5546
|
result[field.name] = value.map((row) => {
|
|
4854
5547
|
const fields2 = rowFields(field, row);
|
|
4855
|
-
return fields2 && isPlainObject2(row) ? toLocaleMaps(fields2, row,
|
|
5548
|
+
return fields2 && isPlainObject2(row) ? toLocaleMaps(fields2, row, rows2.get(String(row.id)) ?? {}, locale) : row;
|
|
4856
5549
|
});
|
|
4857
5550
|
}
|
|
4858
5551
|
}
|
|
@@ -5272,13 +5965,13 @@ function embeddedIPv4(address) {
|
|
|
5272
5965
|
return bytes ? bytes.slice(12).join(".") : void 0;
|
|
5273
5966
|
}
|
|
5274
5967
|
function ipv6Bytes(address) {
|
|
5275
|
-
let
|
|
5276
|
-
const tail = /(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(
|
|
5968
|
+
let text4 = address;
|
|
5969
|
+
const tail = /(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(text4);
|
|
5277
5970
|
if (tail) {
|
|
5278
5971
|
const [a, b, c, d] = tail.slice(1).map(Number);
|
|
5279
|
-
|
|
5972
|
+
text4 = `${text4.slice(0, tail.index)}${(a << 8 | b).toString(16)}:${(c << 8 | d).toString(16)}`;
|
|
5280
5973
|
}
|
|
5281
|
-
const [head = "", rest] =
|
|
5974
|
+
const [head = "", rest] = text4.split("::");
|
|
5282
5975
|
const left = head ? head.split(":") : [];
|
|
5283
5976
|
const right = rest === void 0 ? [] : rest ? rest.split(":") : [];
|
|
5284
5977
|
const groups = rest === void 0 ? left : [...left, ...Array(8 - left.length - right.length).fill("0"), ...right];
|
|
@@ -5457,7 +6150,7 @@ function read(response, maxBytes, deadline) {
|
|
|
5457
6150
|
|
|
5458
6151
|
// src/config-keys.ts
|
|
5459
6152
|
var keys = (list) => new Set(list.split(/\s+/).filter(Boolean));
|
|
5460
|
-
var CONFIG = keys(`secret previousSecrets db serverURL cors cronSecret timeZone webhooks events jobs localization routes admin
|
|
6153
|
+
var CONFIG = keys(`secret previousSecrets integrations listeners db serverURL cors cronSecret timeZone webhooks events jobs localization routes admin
|
|
5461
6154
|
upload auth collections globals endpoints cliCommands onRequest fieldTypes apiKeys email backups
|
|
5462
6155
|
audit logs countCacheSeconds maxPage httpCache outbox rateLimit search telemetry plugins`);
|
|
5463
6156
|
var ADMIN = keys("path locale brand siteURL nav commands modules pages dashboard switcher");
|
|
@@ -5476,7 +6169,7 @@ var GLOBAL = keys(
|
|
|
5476
6169
|
);
|
|
5477
6170
|
var CONTAINER_ADMIN = keys("icon order sidebar layout group");
|
|
5478
6171
|
var COLLECTION_ADMIN = keys(
|
|
5479
|
-
"icon order sidebar layout group editIn badge count empty list calendar ownerField confirmDelete"
|
|
6172
|
+
"icon order sidebar layout group editIn badge count empty list calendar ownerField confirmDelete views export"
|
|
5480
6173
|
);
|
|
5481
6174
|
var FIELD_ADMIN = keys(
|
|
5482
6175
|
"description width condition component after cell column allowCreate initialValue position filter"
|
|
@@ -5640,7 +6333,8 @@ var BUILTIN_NAV = [
|
|
|
5640
6333
|
children: [
|
|
5641
6334
|
{ id: "site", label: { en: "Site", th: "\u0E40\u0E27\u0E47\u0E1A\u0E44\u0E0B\u0E15\u0E4C" }, icon: "globe" },
|
|
5642
6335
|
{ id: "people", label: { en: "Users & access", th: "\u0E1C\u0E39\u0E49\u0E43\u0E0A\u0E49\u0E41\u0E25\u0E30\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C" }, icon: "users" },
|
|
5643
|
-
{ id: "system", label: { en: "System", th: "\u0E23\u0E30\u0E1A\u0E1A" }, icon: "sliders-horizontal" }
|
|
6336
|
+
{ id: "system", label: { en: "System", th: "\u0E23\u0E30\u0E1A\u0E1A" }, icon: "sliders-horizontal" },
|
|
6337
|
+
{ id: "integrations", label: { en: "Integrations", th: "\u0E01\u0E32\u0E23\u0E40\u0E0A\u0E37\u0E48\u0E2D\u0E21\u0E15\u0E48\u0E2D" }, icon: "link" }
|
|
5644
6338
|
]
|
|
5645
6339
|
}
|
|
5646
6340
|
];
|
|
@@ -5747,26 +6441,26 @@ function fieldsText(fields, data) {
|
|
|
5747
6441
|
}
|
|
5748
6442
|
function richText(node) {
|
|
5749
6443
|
if (!node || typeof node !== "object") return "";
|
|
5750
|
-
const { text:
|
|
5751
|
-
if (typeof
|
|
6444
|
+
const { text: text4, content } = node;
|
|
6445
|
+
if (typeof text4 === "string") return text4;
|
|
5752
6446
|
if (!Array.isArray(content)) return "";
|
|
5753
6447
|
const inline = content.some((child) => typeof child?.text === "string");
|
|
5754
6448
|
return content.map(richText).filter(Boolean).join(inline ? "" : "\n");
|
|
5755
6449
|
}
|
|
5756
6450
|
function searchText(fields, doc, paths) {
|
|
5757
|
-
const
|
|
6451
|
+
const text4 = {};
|
|
5758
6452
|
for (const path of paths) {
|
|
5759
6453
|
const field = fieldAt(fields, path);
|
|
5760
6454
|
let value = doc;
|
|
5761
6455
|
for (const name of path.split("."))
|
|
5762
6456
|
value = value && typeof value === "object" ? value[name] : void 0;
|
|
5763
|
-
|
|
6457
|
+
text4[path] = field ? textOf(field, value) : "";
|
|
5764
6458
|
}
|
|
5765
|
-
return
|
|
6459
|
+
return text4;
|
|
5766
6460
|
}
|
|
5767
6461
|
var segmenter = new Intl.Segmenter(void 0, { granularity: "word" });
|
|
5768
|
-
function words(
|
|
5769
|
-
return [...segmenter.segment(
|
|
6462
|
+
function words(text4) {
|
|
6463
|
+
return [...segmenter.segment(text4.normalize("NFC").toLowerCase())].filter((s) => s.isWordLike).map((s) => s.segment);
|
|
5770
6464
|
}
|
|
5771
6465
|
function memorySearch() {
|
|
5772
6466
|
const entries = /* @__PURE__ */ new Map();
|
|
@@ -5993,6 +6687,12 @@ function verifyWebhook(body, headers, secret, options = {}) {
|
|
|
5993
6687
|
}
|
|
5994
6688
|
|
|
5995
6689
|
// src/webhooks.ts
|
|
6690
|
+
var SYSTEM_EVENTS = [
|
|
6691
|
+
"scheduled.failed",
|
|
6692
|
+
"delivery.failed",
|
|
6693
|
+
"backup.failed",
|
|
6694
|
+
"integration.failed"
|
|
6695
|
+
];
|
|
5996
6696
|
var WEBHOOK_EVENTS = [
|
|
5997
6697
|
"create",
|
|
5998
6698
|
"update",
|
|
@@ -6184,10 +6884,13 @@ var Webhooks = class {
|
|
|
6184
6884
|
return { ok: false, error: error.message };
|
|
6185
6885
|
}
|
|
6186
6886
|
}
|
|
6887
|
+
/** Told when a delivery is given up on (`delivery.failed`). */
|
|
6888
|
+
onGiveUp;
|
|
6187
6889
|
giveUp(hook, event, error, attempts) {
|
|
6188
6890
|
this.logger.error(
|
|
6189
6891
|
`Webhook ${hook.url} failed for ${event} after ${attempts} attempts: ${error}`
|
|
6190
6892
|
);
|
|
6893
|
+
this.onGiveUp?.({ url: hook.url, event, error, attempts });
|
|
6191
6894
|
}
|
|
6192
6895
|
};
|
|
6193
6896
|
var later4 = (from, ms) => new Date(from.getTime() + ms).toISOString();
|
|
@@ -6276,6 +6979,8 @@ function validateConfig(config) {
|
|
|
6276
6979
|
validateEndpoints(config, collectionSlugs, add);
|
|
6277
6980
|
validateCommands(config, add);
|
|
6278
6981
|
validateJobs(config, add);
|
|
6982
|
+
validateIntegrations(config, add);
|
|
6983
|
+
validateListeners(config, add);
|
|
6279
6984
|
validateTimeZone(config, add);
|
|
6280
6985
|
validateEvents(config, add);
|
|
6281
6986
|
const apiKeys = config.apiKeys;
|
|
@@ -6803,7 +7508,7 @@ function validateWebhooks(config, add) {
|
|
|
6803
7508
|
...(config.collections ?? []).map((c) => c?.slug)
|
|
6804
7509
|
]);
|
|
6805
7510
|
const globals = new Set((config.globals ?? []).map((g) => g?.slug));
|
|
6806
|
-
const custom = new Set(config.events ?? []);
|
|
7511
|
+
const custom = /* @__PURE__ */ new Set([...config.events ?? [], ...SYSTEM_EVENTS]);
|
|
6807
7512
|
for (const [i, hook] of hooks.entries()) {
|
|
6808
7513
|
const path = `webhooks[${i}]`;
|
|
6809
7514
|
if (typeof hook?.url !== "string" || !/^https?:\/\/\S+$/.test(hook.url)) {
|
|
@@ -7207,7 +7912,26 @@ function validateLayout(layout, container, path, add) {
|
|
|
7207
7912
|
nodes(layout, path, true);
|
|
7208
7913
|
}
|
|
7209
7914
|
function validateCollectionAdmin(admin, path, add) {
|
|
7210
|
-
const { badge, count, empty } = admin;
|
|
7915
|
+
const { badge, count, empty, views } = admin;
|
|
7916
|
+
if (views !== void 0) {
|
|
7917
|
+
if (!Array.isArray(views)) add(`${path}.views`, "must be an array");
|
|
7918
|
+
const ids = /* @__PURE__ */ new Set();
|
|
7919
|
+
for (const [i, raw] of (Array.isArray(views) ? views : []).entries()) {
|
|
7920
|
+
const view = raw ?? {};
|
|
7921
|
+
const at = `${path}.views[${i}]`;
|
|
7922
|
+
if (typeof view.id !== "string" || !/^[a-z0-9][a-z0-9_-]*$/.test(view.id))
|
|
7923
|
+
add(`${at}.id`, 'must be lowercase letters, digits, "-" or "_"', "e.g. id: 'to-review'");
|
|
7924
|
+
else if (ids.has(view.id)) add(`${at}.id`, `duplicate view "${view.id}"`);
|
|
7925
|
+
else ids.add(view.id);
|
|
7926
|
+
if (!isLabel(view.label)) add(`${at}.label`, "must be a string or { en, th }");
|
|
7927
|
+
if (view.where !== void 0 && (typeof view.where !== "object" || view.where === null || Array.isArray(view.where)))
|
|
7928
|
+
add(`${at}.where`, "must be a where query", "e.g. { status: { equals: 'draft' } }");
|
|
7929
|
+
if (view.pageSize !== void 0 && ![10, 25, 50, 100].includes(view.pageSize))
|
|
7930
|
+
add(`${at}.pageSize`, "must be 10, 25, 50 or 100");
|
|
7931
|
+
if (view.density !== void 0 && !["comfortable", "compact"].includes(view.density))
|
|
7932
|
+
add(`${at}.density`, "must be 'comfortable' or 'compact'");
|
|
7933
|
+
}
|
|
7934
|
+
}
|
|
7211
7935
|
if (badge !== void 0) {
|
|
7212
7936
|
const b = badge ?? {};
|
|
7213
7937
|
if (typeof b.where !== "object" || b.where === null || Array.isArray(b.where))
|
|
@@ -7217,6 +7941,8 @@ function validateCollectionAdmin(admin, path, add) {
|
|
|
7217
7941
|
}
|
|
7218
7942
|
if (count !== void 0 && typeof count !== "boolean")
|
|
7219
7943
|
add(`${path}.count`, "must be true or false");
|
|
7944
|
+
if (admin.export !== void 0 && typeof admin.export !== "boolean")
|
|
7945
|
+
add(`${path}.export`, "must be true or false");
|
|
7220
7946
|
if (empty !== void 0) {
|
|
7221
7947
|
const e = empty ?? {};
|
|
7222
7948
|
if (e.description !== void 0 && !isLabel(e.description))
|
|
@@ -7872,6 +8598,56 @@ function validateAdapterVersions(config, add) {
|
|
|
7872
8598
|
);
|
|
7873
8599
|
}
|
|
7874
8600
|
}
|
|
8601
|
+
function validateIntegrations(config, add) {
|
|
8602
|
+
const list = config.integrations;
|
|
8603
|
+
if (list === void 0) return;
|
|
8604
|
+
if (!Array.isArray(list)) {
|
|
8605
|
+
add("integrations", "must be an array");
|
|
8606
|
+
return;
|
|
8607
|
+
}
|
|
8608
|
+
const ids = /* @__PURE__ */ new Set();
|
|
8609
|
+
for (const [i, raw] of list.entries()) {
|
|
8610
|
+
const at = `integrations[${i}]`;
|
|
8611
|
+
const item = raw ?? {};
|
|
8612
|
+
if (typeof item.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(item.id))
|
|
8613
|
+
add(`${at}.id`, 'must be lowercase letters, digits and "-"', "e.g. id: 'telegram'");
|
|
8614
|
+
else if (ids.has(item.id)) add(`${at}.id`, `two integrations are named "${item.id}"`);
|
|
8615
|
+
else ids.add(item.id);
|
|
8616
|
+
if (!isLabel(item.label)) add(`${at}.label`, "must be a string or { en, th }");
|
|
8617
|
+
if (!["social", "ai", "notifications", "automation"].includes(item.category))
|
|
8618
|
+
add(`${at}.category`, "must be 'social', 'ai', 'notifications' or 'automation'");
|
|
8619
|
+
if (item.kind === "fields") {
|
|
8620
|
+
if (!Array.isArray(item.fields) || item.fields.length === 0)
|
|
8621
|
+
add(`${at}.fields`, "needs at least one field to fill");
|
|
8622
|
+
} else if (item.kind === "oauth") {
|
|
8623
|
+
const oauth = item.oauth ?? {};
|
|
8624
|
+
if (typeof oauth.authorizeUrl !== "function" || typeof oauth.exchange !== "function")
|
|
8625
|
+
add(`${at}.oauth`, "needs authorizeUrl and exchange functions");
|
|
8626
|
+
} else add(`${at}.kind`, "must be 'fields' or 'oauth'");
|
|
8627
|
+
if (item.test !== void 0 && typeof item.test !== "function")
|
|
8628
|
+
add(`${at}.test`, "must be a function");
|
|
8629
|
+
for (const [f, field] of (Array.isArray(item.fields) ? item.fields : []).entries()) {
|
|
8630
|
+
const fieldItem = field ?? {};
|
|
8631
|
+
if (fieldItem.lookup !== void 0 && typeof fieldItem.lookup !== "function")
|
|
8632
|
+
add(`${at}.fields[${f}].lookup`, "must be a function");
|
|
8633
|
+
}
|
|
8634
|
+
}
|
|
8635
|
+
}
|
|
8636
|
+
function validateListeners(config, add) {
|
|
8637
|
+
const list = config.listeners;
|
|
8638
|
+
if (list === void 0) return;
|
|
8639
|
+
if (!Array.isArray(list)) {
|
|
8640
|
+
add("listeners", "must be an array");
|
|
8641
|
+
return;
|
|
8642
|
+
}
|
|
8643
|
+
for (const [i, raw] of list.entries()) {
|
|
8644
|
+
const item = raw ?? {};
|
|
8645
|
+
const events = item.events;
|
|
8646
|
+
if (events !== "*" && !(Array.isArray(events) && events.every((e) => typeof e === "string" && e.length > 0)))
|
|
8647
|
+
add(`listeners[${i}].events`, "must be event names or '*'", "e.g. events: ['order.created']");
|
|
8648
|
+
if (typeof item.handler !== "function") add(`listeners[${i}].handler`, "must be a function");
|
|
8649
|
+
}
|
|
8650
|
+
}
|
|
7875
8651
|
|
|
7876
8652
|
// src/resolve-config.ts
|
|
7877
8653
|
var DEFAULT_ADMIN_PATH = "/admin";
|
|
@@ -7981,14 +8757,18 @@ async function resolveConfig(input) {
|
|
|
7981
8757
|
...config.rateLimit ? [rateLimitsCollection] : [],
|
|
7982
8758
|
databaseBackupsCollection,
|
|
7983
8759
|
bulkOperationsCollection,
|
|
8760
|
+
savedViewsCollection,
|
|
8761
|
+
...resolvedIntegrations(config).length > 0 ? [connectionsCollection] : [],
|
|
7984
8762
|
...config.auth?.rbac ? [rolesCollection] : [],
|
|
7985
8763
|
...(config.auth?.providers?.length ?? 0) > 0 ? [userIdentitiesCollection] : [],
|
|
7986
8764
|
...config.audit ? [auditLogsCollection] : []
|
|
7987
8765
|
],
|
|
7988
8766
|
globals: config.globals ?? [],
|
|
7989
8767
|
endpoints: config.endpoints ?? [],
|
|
8768
|
+
integrations: resolvedIntegrations(config),
|
|
7990
8769
|
cliCommands: config.cliCommands ?? [],
|
|
7991
|
-
|
|
8770
|
+
// Easy CMS's own events (#356), for webhooks and `cms.on`.
|
|
8771
|
+
events: [.../* @__PURE__ */ new Set([...config.events ?? [], ...SYSTEM_EVENTS])],
|
|
7992
8772
|
jobs: config.jobs ?? [],
|
|
7993
8773
|
warnings,
|
|
7994
8774
|
installedPlugins: (plugins ?? []).map(
|
|
@@ -8064,9 +8844,12 @@ function resolveAudit(audit) {
|
|
|
8064
8844
|
scope: audit.scope ?? false
|
|
8065
8845
|
};
|
|
8066
8846
|
}
|
|
8847
|
+
function resolvedIntegrations(config) {
|
|
8848
|
+
return config.integrations ?? [];
|
|
8849
|
+
}
|
|
8067
8850
|
|
|
8068
8851
|
// src/rest/http-cache.ts
|
|
8069
|
-
import { createHash as
|
|
8852
|
+
import { createHash as createHash5 } from "crypto";
|
|
8070
8853
|
var cacheTags = {
|
|
8071
8854
|
collection: (slug2) => ["ecms", `ecms:${slug2}`],
|
|
8072
8855
|
document: (slug2, id) => ["ecms", `ecms:${slug2}`, `ecms:${slug2}:${id}`],
|
|
@@ -8093,7 +8876,7 @@ function cacheableResponse(request, body, status, headers, times, tags) {
|
|
|
8093
8876
|
);
|
|
8094
8877
|
headers.set("cache-tag", tags.join(","));
|
|
8095
8878
|
headers.set("surrogate-key", tags.join(" "));
|
|
8096
|
-
const etag = `W/"${
|
|
8879
|
+
const etag = `W/"${createHash5("sha1").update(body).digest("base64url")}"`;
|
|
8097
8880
|
headers.set("etag", etag);
|
|
8098
8881
|
if (matches2(request.headers.get("if-none-match"), etag))
|
|
8099
8882
|
return new Response(null, { status: 304, headers });
|
|
@@ -8101,7 +8884,7 @@ function cacheableResponse(request, body, status, headers, times, tags) {
|
|
|
8101
8884
|
}
|
|
8102
8885
|
|
|
8103
8886
|
// src/version.ts
|
|
8104
|
-
var VERSION = true ? "1.
|
|
8887
|
+
var VERSION = true ? "1.32.0" : "0.0.0-dev";
|
|
8105
8888
|
|
|
8106
8889
|
// src/telemetry.ts
|
|
8107
8890
|
var DB_CALLS = /* @__PURE__ */ new Set([
|
|
@@ -8378,6 +9161,8 @@ var EasyCMS = class {
|
|
|
8378
9161
|
audit;
|
|
8379
9162
|
/** Folders of the media library and their permissions (`upload.folders`). */
|
|
8380
9163
|
folders;
|
|
9164
|
+
/** Accounts connected to plugins' integrations (`integrations`, Settings → Integrations). */
|
|
9165
|
+
integrations;
|
|
8381
9166
|
versions;
|
|
8382
9167
|
webhooks;
|
|
8383
9168
|
mailer;
|
|
@@ -8405,6 +9190,12 @@ var EasyCMS = class {
|
|
|
8405
9190
|
(entry) => this.audit.record(entry)
|
|
8406
9191
|
);
|
|
8407
9192
|
this.audit = new AuditLog(this);
|
|
9193
|
+
this.integrations = new Integrations(this);
|
|
9194
|
+
for (const listener of config.listeners ?? []) {
|
|
9195
|
+
const run = (message2) => listener.handler(message2, { cms: this });
|
|
9196
|
+
if (listener.events === "*") this.anyListeners.add(run);
|
|
9197
|
+
else for (const event of listener.events) this.on(event, run);
|
|
9198
|
+
}
|
|
8408
9199
|
this.folders = new MediaFolders(config, db, this.roles);
|
|
8409
9200
|
this.webhooks = new Webhooks(
|
|
8410
9201
|
config.webhooks ?? [],
|
|
@@ -8416,6 +9207,8 @@ var EasyCMS = class {
|
|
|
8416
9207
|
logger,
|
|
8417
9208
|
config.collections.some((c) => c.slug === EMAIL_DELIVERIES) ? this.emailQueue() : void 0
|
|
8418
9209
|
);
|
|
9210
|
+
this.webhooks.onGiveUp = ({ url, event, error, attempts }) => this.systemEvent("delivery.failed", { kind: "webhook", url, event, error, attempts });
|
|
9211
|
+
this.mailer.onGiveUp = ({ to, error, attempts }) => this.systemEvent("delivery.failed", { kind: "email", to, error, attempts });
|
|
8419
9212
|
this.outbox = config.collections.some((c) => c.slug === OUTBOX) ? new Outbox(
|
|
8420
9213
|
db,
|
|
8421
9214
|
logger,
|
|
@@ -8608,7 +9401,7 @@ var EasyCMS = class {
|
|
|
8608
9401
|
}
|
|
8609
9402
|
]);
|
|
8610
9403
|
}
|
|
8611
|
-
const random2 =
|
|
9404
|
+
const random2 = randomBytes7(4).toString("hex");
|
|
8612
9405
|
const isPrivate = await this.folders.isPrivate(folderOf(MEDIA, data));
|
|
8613
9406
|
const storage = this.storageFor(isPrivate);
|
|
8614
9407
|
const filename = withPrivacy(storageKey(file.name, mimeType, random2), isPrivate);
|
|
@@ -8676,7 +9469,7 @@ var EasyCMS = class {
|
|
|
8676
9469
|
]);
|
|
8677
9470
|
const isPrivate = await this.folders.isPrivate(folder);
|
|
8678
9471
|
const storage = this.storageFor(isPrivate);
|
|
8679
|
-
const key = withPrivacy(storageKey(name, type,
|
|
9472
|
+
const key = withPrivacy(storageKey(name, type, randomBytes7(4).toString("hex")), isPrivate);
|
|
8680
9473
|
const upload = storage.uploadURL ? await storage.uploadURL(key, { contentType: type, size, expiresIn: UPLOAD_URL_SECONDS }) : null;
|
|
8681
9474
|
const ticket = this.sign("upload", {
|
|
8682
9475
|
key,
|
|
@@ -9801,12 +10594,12 @@ var EasyCMS = class {
|
|
|
9801
10594
|
if (asked !== void 0 && localization && !localization.locales.includes(asked))
|
|
9802
10595
|
throw new QueryError(`Unknown locale "${asked}"`);
|
|
9803
10596
|
const locale = localization ? asked ?? localization.defaultLocale ?? null : null;
|
|
9804
|
-
const
|
|
10597
|
+
const text4 = query.trim().slice(0, 200);
|
|
9805
10598
|
const empty = { hits: [], total: 0, page, limit, hasNextPage: false };
|
|
9806
|
-
if (!
|
|
10599
|
+
if (!text4 || collections.length === 0) return empty;
|
|
9807
10600
|
const { collections: _, limit: __, page: ___, ...read2 } = options;
|
|
9808
10601
|
const { hits, total } = await index.search(
|
|
9809
|
-
{ query:
|
|
10602
|
+
{ query: text4, collections, locale, limit, offset: (page - 1) * limit },
|
|
9810
10603
|
async (collection2, ids) => (await this.find(collection2, {
|
|
9811
10604
|
...read2,
|
|
9812
10605
|
...locale ? { locale } : {},
|
|
@@ -9960,13 +10753,13 @@ var EasyCMS = class {
|
|
|
9960
10753
|
const { jobs } = this.config;
|
|
9961
10754
|
if (jobs.length === 0) return { ran: 0, failed: 0 };
|
|
9962
10755
|
const { docs } = await this.db.find({ collection: JOB_RUNS, sort: ["name"], limit: 0, page: 1 });
|
|
9963
|
-
const
|
|
10756
|
+
const rows2 = new Map(docs.map((row) => [String(row.name), row]));
|
|
9964
10757
|
let legacy;
|
|
9965
10758
|
const stamp = now.toISOString();
|
|
9966
10759
|
let ran = 0;
|
|
9967
10760
|
let failed = 0;
|
|
9968
10761
|
for (const job of jobs) {
|
|
9969
|
-
const fresh =
|
|
10762
|
+
const fresh = rows2.has(job.name) ? await this.db.findById({ collection: JOB_RUNS, id: rows2.get(job.name)?.id }) : null;
|
|
9970
10763
|
const row = fresh ?? void 0;
|
|
9971
10764
|
if (row?.pausedAt) continue;
|
|
9972
10765
|
const schedule = jobSchedule(job, row);
|
|
@@ -10080,6 +10873,37 @@ var EasyCMS = class {
|
|
|
10080
10873
|
if (!this.config.events.includes(event))
|
|
10081
10874
|
throw new QueryError(`Unknown event "${event}": add it to \`events\` in the config`);
|
|
10082
10875
|
this.webhooks.emit(event, about ?? {}, data);
|
|
10876
|
+
this.dispatch(event, data, about);
|
|
10877
|
+
}
|
|
10878
|
+
listeners = /* @__PURE__ */ new Map();
|
|
10879
|
+
/** Listeners of every event (`events: '*'` in the config's `listeners`). */
|
|
10880
|
+
anyListeners = /* @__PURE__ */ new Set();
|
|
10881
|
+
/**
|
|
10882
|
+
* Runs `handler` after each `event` on this server (#356): the config's and plugins' events, the
|
|
10883
|
+
* content changes webhooks get (`create`, `publish`…), and Easy CMS's own (`scheduled.failed`,
|
|
10884
|
+
* `delivery.failed`, `backup.failed`, `integration.failed`). It runs after the change, without
|
|
10885
|
+
* being waited for; its errors are logged. Returns a function that stops it.
|
|
10886
|
+
*/
|
|
10887
|
+
on(event, handler) {
|
|
10888
|
+
const set = this.listeners.get(event) ?? /* @__PURE__ */ new Set();
|
|
10889
|
+
set.add(handler);
|
|
10890
|
+
this.listeners.set(event, set);
|
|
10891
|
+
return () => set.delete(handler);
|
|
10892
|
+
}
|
|
10893
|
+
dispatch(event, data, about) {
|
|
10894
|
+
const set = [...this.listeners.get(event) ?? [], ...this.anyListeners];
|
|
10895
|
+
if (set.length === 0) return;
|
|
10896
|
+
for (const handler of set)
|
|
10897
|
+
queueMicrotask(() => {
|
|
10898
|
+
Promise.resolve().then(() => handler({ event, data, ...about ? { about } : {} })).catch(
|
|
10899
|
+
(error) => this.logger.error(`A listener of ${event} failed: ${errorText(error)}`, { error })
|
|
10900
|
+
);
|
|
10901
|
+
});
|
|
10902
|
+
}
|
|
10903
|
+
/** @internal Easy CMS's own events (`SYSTEM_EVENTS`): to webhooks that list them and `cms.on`. */
|
|
10904
|
+
systemEvent(event, data) {
|
|
10905
|
+
this.webhooks.emit(event, {}, data);
|
|
10906
|
+
this.dispatch(event, data);
|
|
10083
10907
|
}
|
|
10084
10908
|
/**
|
|
10085
10909
|
* Tries webhook deliveries that are due for another attempt. Deliveries are kept in the
|
|
@@ -10238,6 +11062,13 @@ var EasyCMS = class {
|
|
|
10238
11062
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10239
11063
|
}
|
|
10240
11064
|
});
|
|
11065
|
+
this.systemEvent("scheduled.failed", {
|
|
11066
|
+
job: row.id,
|
|
11067
|
+
action,
|
|
11068
|
+
...row.collection ? { collection: row.collection } : { global: row.global },
|
|
11069
|
+
doc: row.doc ?? null,
|
|
11070
|
+
error: message2
|
|
11071
|
+
});
|
|
10241
11072
|
}
|
|
10242
11073
|
}
|
|
10243
11074
|
return { ran, failed };
|
|
@@ -10350,7 +11181,7 @@ var EasyCMS = class {
|
|
|
10350
11181
|
...options.from !== void 0 ? { gte: options.from } : {},
|
|
10351
11182
|
...options.to !== void 0 ? { lt: options.to } : {}
|
|
10352
11183
|
};
|
|
10353
|
-
const
|
|
11184
|
+
const rows2 = await this.db.find({
|
|
10354
11185
|
collection: SCHEDULED_JOBS,
|
|
10355
11186
|
where: {
|
|
10356
11187
|
and: [
|
|
@@ -10385,7 +11216,7 @@ var EasyCMS = class {
|
|
|
10385
11216
|
return result;
|
|
10386
11217
|
};
|
|
10387
11218
|
const jobs = [];
|
|
10388
|
-
for (const row of
|
|
11219
|
+
for (const row of rows2.docs) {
|
|
10389
11220
|
const parent = String(row.parent);
|
|
10390
11221
|
if (!await may2(parent)) continue;
|
|
10391
11222
|
const target = parent.startsWith("global:") ? { global: splitGlobalKey(parent.slice("global:".length)).slug } : { collection: parent };
|
|
@@ -10395,7 +11226,7 @@ var EasyCMS = class {
|
|
|
10395
11226
|
return jobs;
|
|
10396
11227
|
}
|
|
10397
11228
|
async pendingJobs(parent, doc) {
|
|
10398
|
-
const
|
|
11229
|
+
const rows2 = await this.db.find({
|
|
10399
11230
|
collection: SCHEDULED_JOBS,
|
|
10400
11231
|
where: {
|
|
10401
11232
|
and: [
|
|
@@ -10408,7 +11239,7 @@ var EasyCMS = class {
|
|
|
10408
11239
|
limit: 0,
|
|
10409
11240
|
page: 1
|
|
10410
11241
|
});
|
|
10411
|
-
return
|
|
11242
|
+
return rows2.docs.map(toJob);
|
|
10412
11243
|
}
|
|
10413
11244
|
async removeJob(parent, doc, jobId) {
|
|
10414
11245
|
const job = (await this.pendingJobs(parent, doc)).find((j) => String(j.id) === String(jobId));
|
|
@@ -10866,8 +11697,10 @@ var EasyCMS = class {
|
|
|
10866
11697
|
* are logged.
|
|
10867
11698
|
*/
|
|
10868
11699
|
emitChange(config, event, doc, before) {
|
|
10869
|
-
for (const change of this.changeEvents(config, event, doc, before))
|
|
11700
|
+
for (const change of this.changeEvents(config, event, doc, before)) {
|
|
10870
11701
|
this.webhooks.emit(change.event, change.target, change.body);
|
|
11702
|
+
this.dispatch(change.event, change.body, change.target);
|
|
11703
|
+
}
|
|
10871
11704
|
if (this.searches(config, event)) this.searchIndex?.syncLater(config.slug, doc.id);
|
|
10872
11705
|
const purge = this.config.httpCache?.purge;
|
|
10873
11706
|
const tags = this.purgeTags(config, event, doc, before);
|
|
@@ -10917,8 +11750,10 @@ var EasyCMS = class {
|
|
|
10917
11750
|
}
|
|
10918
11751
|
if (!done.has("webhooks")) {
|
|
10919
11752
|
const changes = this.changeEvents(config, event, doc, entry.before);
|
|
10920
|
-
for (const change of changes)
|
|
11753
|
+
for (const change of changes) {
|
|
10921
11754
|
await this.webhooks.emitSaved(change.event, change.target, change.body);
|
|
11755
|
+
this.dispatch(change.event, change.body, change.target);
|
|
11756
|
+
}
|
|
10922
11757
|
if (changes.length > 0 && (this.config.webhooks?.length ?? 0) > 0) await mark("webhooks");
|
|
10923
11758
|
}
|
|
10924
11759
|
if (!done.has("search") && this.searchIndex && this.searches(config, event)) {
|
|
@@ -11409,7 +12244,7 @@ function durationSeconds(value) {
|
|
|
11409
12244
|
}
|
|
11410
12245
|
function cursorQuery(collection2, where, sort, locale, draft) {
|
|
11411
12246
|
const key = JSON.stringify([collection2, where ?? null, sort, locale ?? null, draft]);
|
|
11412
|
-
return
|
|
12247
|
+
return createHash6("sha256").update(key).digest("base64url").slice(0, 16);
|
|
11413
12248
|
}
|
|
11414
12249
|
function writeCursor(keys2, query) {
|
|
11415
12250
|
return Buffer.from(JSON.stringify({ k: keys2, q: query })).toString("base64url");
|
|
@@ -11441,8 +12276,8 @@ function hasCalendarPage(config) {
|
|
|
11441
12276
|
var text2 = (value) => typeof value === "string" && value !== "" ? value : null;
|
|
11442
12277
|
async function adminJobs(cms, user) {
|
|
11443
12278
|
const { config } = cms;
|
|
11444
|
-
const
|
|
11445
|
-
const byName = new Map(
|
|
12279
|
+
const rows2 = has(config, JOB_RUNS) ? (await cms.db.find({ collection: JOB_RUNS, sort: ["name"], limit: 0, page: 1 })).docs : [];
|
|
12280
|
+
const byName = new Map(rows2.map((row) => [String(row.name), row]));
|
|
11446
12281
|
const jobs = config.jobs.map((job) => {
|
|
11447
12282
|
const row = byName.get(job.name);
|
|
11448
12283
|
const lastRunAt = text2(row?.lastRunAt);
|
|
@@ -11719,7 +12554,8 @@ var Roles = class {
|
|
|
11719
12554
|
async canView(user, id) {
|
|
11720
12555
|
if (isSystemAdmin(user)) return true;
|
|
11721
12556
|
if (user.apiKey) return false;
|
|
11722
|
-
if ((id === "deliveries" || id === "jobs") && user.scoped === true)
|
|
12557
|
+
if ((id === "deliveries" || id === "jobs" || id === "integrations") && user.scoped === true)
|
|
12558
|
+
return false;
|
|
11723
12559
|
if (id === "audit" && user.role === "admin" && user.scoped === true && this.config.audit !== false && typeof this.config.audit.scope === "function")
|
|
11724
12560
|
return true;
|
|
11725
12561
|
if (!this.enabled)
|
|
@@ -11816,9 +12652,9 @@ var Roles = class {
|
|
|
11816
12652
|
}
|
|
11817
12653
|
async addMissing() {
|
|
11818
12654
|
if (!this.enabled) return;
|
|
11819
|
-
const
|
|
11820
|
-
const first =
|
|
11821
|
-
const known = new Set(
|
|
12655
|
+
const rows2 = await this.rows();
|
|
12656
|
+
const first = rows2.length === 0;
|
|
12657
|
+
const known = new Set(rows2.map((r) => String(r.key)));
|
|
11822
12658
|
for (const key of this.config.auth.roles) {
|
|
11823
12659
|
if (known.has(key)) continue;
|
|
11824
12660
|
try {
|
|
@@ -11876,15 +12712,15 @@ var Roles = class {
|
|
|
11876
12712
|
// --- Settings → Roles ----------------------------------------------------
|
|
11877
12713
|
async list() {
|
|
11878
12714
|
await this.sync();
|
|
11879
|
-
const
|
|
12715
|
+
const rows2 = await this.rows();
|
|
11880
12716
|
const counts = /* @__PURE__ */ new Map();
|
|
11881
|
-
for (const row of
|
|
12717
|
+
for (const row of rows2) {
|
|
11882
12718
|
counts.set(
|
|
11883
12719
|
String(row.key),
|
|
11884
12720
|
await this.db.count({ collection: USERS, where: { role: { equals: row.key } } })
|
|
11885
12721
|
);
|
|
11886
12722
|
}
|
|
11887
|
-
const roles =
|
|
12723
|
+
const roles = rows2.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));
|
|
11888
12724
|
const decided = roles.filter((r) => r.key !== "admin").map((r) => r.permissions);
|
|
11889
12725
|
const seen = (group, slug2) => decided.some((p) => p[group]?.[slug2] !== void 0);
|
|
11890
12726
|
return {
|
|
@@ -12007,6 +12843,7 @@ var Roles = class {
|
|
|
12007
12843
|
(c) => c.slug === WEBHOOK_DELIVERIES || c.slug === EMAIL_DELIVERIES
|
|
12008
12844
|
) ? [{ id: "deliveries" }] : [],
|
|
12009
12845
|
...hasJobsPage(this.config) ? [{ id: "jobs" }] : [],
|
|
12846
|
+
...this.config.integrations.length > 0 ? [{ id: "integrations" }] : [],
|
|
12010
12847
|
...hasCalendarPage(this.config) ? [{ id: "calendar" }] : [],
|
|
12011
12848
|
...this.config.admin.pages.map((page) => ({ id: viewId.page(page.path), label: page.label })),
|
|
12012
12849
|
...this.config.admin.dashboard.map((widget) => ({
|
|
@@ -12209,7 +13046,7 @@ function rank(role, config) {
|
|
|
12209
13046
|
}
|
|
12210
13047
|
|
|
12211
13048
|
// src/rest/admin-modules.ts
|
|
12212
|
-
import { createHash as
|
|
13049
|
+
import { createHash as createHash7 } from "crypto";
|
|
12213
13050
|
import { readFile as readFile3 } from "fs/promises";
|
|
12214
13051
|
import { createRequire } from "module";
|
|
12215
13052
|
import { join as join3 } from "path";
|
|
@@ -12250,7 +13087,7 @@ async function readAdminModule(cms, index) {
|
|
|
12250
13087
|
if (!file) return void 0;
|
|
12251
13088
|
const body = await readFile3(file, "utf8").catch(() => void 0);
|
|
12252
13089
|
if (body === void 0) return void 0;
|
|
12253
|
-
return { body, etag: `"${
|
|
13090
|
+
return { body, etag: `"${createHash7("sha1").update(body).digest("base64url")}"` };
|
|
12254
13091
|
}
|
|
12255
13092
|
|
|
12256
13093
|
// src/rest/handler.ts
|
|
@@ -12261,9 +13098,9 @@ var PER_PAGE = 50;
|
|
|
12261
13098
|
var collectionOf = (kind) => kind === "webhook" ? WEBHOOK_DELIVERIES : EMAIL_DELIVERIES;
|
|
12262
13099
|
var exists = (cms, kind) => cms.config.collections.some((c) => c.slug === collectionOf(kind));
|
|
12263
13100
|
var isDeliveryKind = (value) => value === "webhook" || value === "email";
|
|
12264
|
-
function parse3(
|
|
13101
|
+
function parse3(text4) {
|
|
12265
13102
|
try {
|
|
12266
|
-
const value = JSON.parse(String(
|
|
13103
|
+
const value = JSON.parse(String(text4 ?? ""));
|
|
12267
13104
|
return typeof value === "object" && value !== null ? value : {};
|
|
12268
13105
|
} catch {
|
|
12269
13106
|
return {};
|
|
@@ -12445,7 +13282,7 @@ async function sendTestEmail(cms, user, to, locale) {
|
|
|
12445
13282
|
const th = locale === "th" || locale !== "en" && cms.config.admin.locale === "th";
|
|
12446
13283
|
const time = (/* @__PURE__ */ new Date()).toISOString();
|
|
12447
13284
|
const subject = th ? `\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E17\u0E14\u0E2A\u0E2D\u0E1A\u0E08\u0E32\u0E01 ${site}` : `Test email from ${site}`;
|
|
12448
|
-
const
|
|
13285
|
+
const text4 = th ? `\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E19\u0E35\u0E49\u0E2A\u0E48\u0E07\u0E08\u0E32\u0E01\u0E2B\u0E19\u0E49\u0E32 \u0E15\u0E31\u0E49\u0E07\u0E04\u0E48\u0E32 \u2192 \u0E2D\u0E35\u0E40\u0E21\u0E25 \u0E02\u0E2D\u0E07 ${site} \u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E17\u0E14\u0E2A\u0E2D\u0E1A\u0E01\u0E32\u0E23\u0E15\u0E31\u0E49\u0E07\u0E04\u0E48\u0E32
|
|
12449
13286
|
|
|
12450
13287
|
\u0E2A\u0E48\u0E07\u0E42\u0E14\u0E22: ${user.email}
|
|
12451
13288
|
Adapter: ${adapter.name ?? "custom"}
|
|
@@ -12457,7 +13294,7 @@ Adapter: ${adapter.name ?? "custom"}
|
|
|
12457
13294
|
From: ${from}
|
|
12458
13295
|
Time: ${time}`;
|
|
12459
13296
|
try {
|
|
12460
|
-
await adapter.send({ to: address, subject, text:
|
|
13297
|
+
await adapter.send({ to: address, subject, text: text4, from });
|
|
12461
13298
|
return logsOnly(cms) ? { ok: true, logged: true } : { ok: true };
|
|
12462
13299
|
} catch (error) {
|
|
12463
13300
|
return { ok: false, error: message(error) };
|
|
@@ -12647,6 +13484,8 @@ async function collection(cms, config, user, localized, roles, context) {
|
|
|
12647
13484
|
if (config.admin?.empty) result.empty = JSON.parse(JSON.stringify(config.admin.empty));
|
|
12648
13485
|
if (config.admin?.list?.tree) result.tree = config.admin.list.tree;
|
|
12649
13486
|
if (config.admin?.list?.sort) result.defaultSort = config.admin.list.sort;
|
|
13487
|
+
if (config.admin?.views?.length) result.views = JSON.parse(JSON.stringify(config.admin.views));
|
|
13488
|
+
if (config.admin?.export === false) result.noExport = true;
|
|
12650
13489
|
if (config.admin?.calendar) result.calendar = { ...config.admin.calendar };
|
|
12651
13490
|
if (config.admin?.confirmDelete) result.confirmDelete = { ...config.admin.confirmDelete };
|
|
12652
13491
|
const owner = cms.roles.ownerOf(config.slug);
|
|
@@ -12703,7 +13542,8 @@ async function adminSchema(cms, user, origin, context = {}) {
|
|
|
12703
13542
|
roles: admin && cms.roles.enabled,
|
|
12704
13543
|
sso: admin && cms.auth.sso.enabled,
|
|
12705
13544
|
audit: cms.audit.enabled && await cms.roles.canView(user, "audit"),
|
|
12706
|
-
calendar: hasCalendarPage(cms.config) && await cms.roles.canView(user, "calendar")
|
|
13545
|
+
calendar: hasCalendarPage(cms.config) && await cms.roles.canView(user, "calendar"),
|
|
13546
|
+
integrations: cms.config.integrations.length > 0 && await cms.roles.canView(user, "integrations")
|
|
12707
13547
|
};
|
|
12708
13548
|
const roles = await cms.roles.options();
|
|
12709
13549
|
const adminCollections = await Promise.all(
|
|
@@ -12843,7 +13683,8 @@ var VIEW_GROUPS = [
|
|
|
12843
13683
|
["email", "settings.system"],
|
|
12844
13684
|
["deliveries", "settings.system"],
|
|
12845
13685
|
["jobs", "settings.system"],
|
|
12846
|
-
["audit", "settings.system"]
|
|
13686
|
+
["audit", "settings.system"],
|
|
13687
|
+
["integrations", "settings.integrations"]
|
|
12847
13688
|
];
|
|
12848
13689
|
function buildNav(cms, collections, globals, pageRefs, views) {
|
|
12849
13690
|
const groups = navGroups(cms.config.admin.nav);
|
|
@@ -13076,6 +13917,15 @@ async function adminStatus(cms) {
|
|
|
13076
13917
|
if (failed.length > 0) attention.push({ id: "jobs", count: failed.length, names: failed });
|
|
13077
13918
|
const paused = await pausedJobs(cms);
|
|
13078
13919
|
if (paused.length > 0) attention.push({ id: "jobs-paused", count: paused.length, names: paused });
|
|
13920
|
+
if (config.integrations.length > 0) {
|
|
13921
|
+
const broken = await cms.integrations.broken();
|
|
13922
|
+
if (broken.length > 0)
|
|
13923
|
+
attention.push({
|
|
13924
|
+
id: "integrations",
|
|
13925
|
+
count: broken.length,
|
|
13926
|
+
names: broken.map((b) => `${b.integration}: ${b.name}`)
|
|
13927
|
+
});
|
|
13928
|
+
}
|
|
13079
13929
|
const backups = await backupAttention(cms);
|
|
13080
13930
|
if (backups) attention.push(backups);
|
|
13081
13931
|
attention.push(...await cms.audit.attention());
|
|
@@ -13097,6 +13947,182 @@ async function adminStatus(cms) {
|
|
|
13097
13947
|
};
|
|
13098
13948
|
}
|
|
13099
13949
|
|
|
13950
|
+
// src/rest/export-csv.ts
|
|
13951
|
+
var EXPORT_MAX = 1e5;
|
|
13952
|
+
var STEP = 500;
|
|
13953
|
+
var SYSTEM = {
|
|
13954
|
+
id: "ID",
|
|
13955
|
+
status: { en: "Status", th: "\u0E2A\u0E16\u0E32\u0E19\u0E30" },
|
|
13956
|
+
createdAt: { en: "Created", th: "\u0E2A\u0E23\u0E49\u0E32\u0E07\u0E40\u0E21\u0E37\u0E48\u0E2D" },
|
|
13957
|
+
updatedAt: { en: "Updated", th: "\u0E41\u0E01\u0E49\u0E44\u0E02\u0E40\u0E21\u0E37\u0E48\u0E2D" }
|
|
13958
|
+
};
|
|
13959
|
+
var text3 = (label, fallback, lang) => label === void 0 ? fallback : typeof label === "string" ? label : label[lang] ?? label.en ?? Object.values(label)[0] ?? fallback;
|
|
13960
|
+
function isoInZone(value, timeZone) {
|
|
13961
|
+
const date = new Date(value);
|
|
13962
|
+
if (!timeZone || Number.isNaN(date.getTime())) return value;
|
|
13963
|
+
const parts = Object.fromEntries(
|
|
13964
|
+
new Intl.DateTimeFormat("en-CA", {
|
|
13965
|
+
timeZone,
|
|
13966
|
+
year: "numeric",
|
|
13967
|
+
month: "2-digit",
|
|
13968
|
+
day: "2-digit",
|
|
13969
|
+
hour: "2-digit",
|
|
13970
|
+
minute: "2-digit",
|
|
13971
|
+
second: "2-digit",
|
|
13972
|
+
hourCycle: "h23"
|
|
13973
|
+
}).formatToParts(date).map((p) => [p.type, p.value])
|
|
13974
|
+
);
|
|
13975
|
+
const local = `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}`;
|
|
13976
|
+
const offset = Math.round((Date.parse(`${local}Z`) - date.getTime()) / 6e4);
|
|
13977
|
+
const sign3 = offset < 0 ? "-" : "+";
|
|
13978
|
+
const abs = Math.abs(offset);
|
|
13979
|
+
return `${local}${sign3}${String(Math.floor(abs / 60)).padStart(2, "0")}:${String(abs % 60).padStart(2, "0")}`;
|
|
13980
|
+
}
|
|
13981
|
+
function cellOf(cms, field, value, key, lang) {
|
|
13982
|
+
if (value === null || value === void 0) return "";
|
|
13983
|
+
const timeZone = cms.config.timeZone;
|
|
13984
|
+
if (!field)
|
|
13985
|
+
return key === "createdAt" || key === "updatedAt" ? isoInZone(String(value), timeZone) : value;
|
|
13986
|
+
const titleOf = (slug2, doc) => {
|
|
13987
|
+
if (!doc || typeof doc !== "object") return String(doc);
|
|
13988
|
+
const d = doc;
|
|
13989
|
+
if (slug2 === "media" && typeof d.url === "string") return d.url;
|
|
13990
|
+
const collection2 = cms.config.collections.find((c) => c.slug === slug2);
|
|
13991
|
+
const title = collection2?.useAsTitle ? d[collection2.useAsTitle] : void 0;
|
|
13992
|
+
return typeof title === "string" && title !== "" ? title : String(d.id ?? "");
|
|
13993
|
+
};
|
|
13994
|
+
switch (field.type) {
|
|
13995
|
+
case "date":
|
|
13996
|
+
return isoInZone(String(value), timeZone);
|
|
13997
|
+
case "boolean":
|
|
13998
|
+
return value ? "true" : "false";
|
|
13999
|
+
case "select": {
|
|
14000
|
+
const label = (v) => {
|
|
14001
|
+
const option = field.options.find((o) => (typeof o === "string" ? o : o.value) === v);
|
|
14002
|
+
return option === void 0 || typeof option === "string" ? String(v) : text3(option.label, String(v), lang);
|
|
14003
|
+
};
|
|
14004
|
+
return Array.isArray(value) ? value.map(label).join("; ") : label(value);
|
|
14005
|
+
}
|
|
14006
|
+
case "richText":
|
|
14007
|
+
return richText(value);
|
|
14008
|
+
case "relationship":
|
|
14009
|
+
case "upload": {
|
|
14010
|
+
const one = (item) => {
|
|
14011
|
+
if (isPolymorphic(field)) {
|
|
14012
|
+
const pair = polymorphicValue(item);
|
|
14013
|
+
if (!pair) return "";
|
|
14014
|
+
const collection2 = cms.config.collections.find((c) => c.slug === pair.relationTo);
|
|
14015
|
+
const name = text3(collection2?.labels?.singular, pair.relationTo, lang);
|
|
14016
|
+
return `${name} \xB7 ${titleOf(pair.relationTo, pair.value)}`;
|
|
14017
|
+
}
|
|
14018
|
+
return titleOf(relationTargets(field)[0], item);
|
|
14019
|
+
};
|
|
14020
|
+
return Array.isArray(value) ? value.map(one).join("; ") : one(value);
|
|
14021
|
+
}
|
|
14022
|
+
default:
|
|
14023
|
+
return value;
|
|
14024
|
+
}
|
|
14025
|
+
}
|
|
14026
|
+
async function columnsOf(cms, config, asked, options) {
|
|
14027
|
+
const rules = await cms.roles.fieldRules(options.user);
|
|
14028
|
+
const read2 = new FieldAccessChecker(
|
|
14029
|
+
"read",
|
|
14030
|
+
{ user: options.user, context: options.context },
|
|
14031
|
+
rules
|
|
14032
|
+
);
|
|
14033
|
+
const readable3 = [];
|
|
14034
|
+
for (const field of config.fields)
|
|
14035
|
+
if (!field.hidden && field.name !== "password" && await read2.allows(field))
|
|
14036
|
+
readable3.push(field);
|
|
14037
|
+
const system2 = [...config.drafts ? ["status"] : [], "createdAt", "updatedAt"];
|
|
14038
|
+
const of = (key) => {
|
|
14039
|
+
const field = readable3.find((f) => f.name === key);
|
|
14040
|
+
if (field) return { key, label: field.label ?? key, field };
|
|
14041
|
+
return key in SYSTEM && (key !== "status" || config.drafts) ? { key, label: SYSTEM[key] } : void 0;
|
|
14042
|
+
};
|
|
14043
|
+
const keys2 = asked ?? [...readable3.map((f) => f.name), ...system2];
|
|
14044
|
+
const columns = keys2.filter((k) => k !== "id").map(of).filter((c) => c !== void 0);
|
|
14045
|
+
return [{ key: "id", label: SYSTEM.id }, ...columns];
|
|
14046
|
+
}
|
|
14047
|
+
async function exportCsv(cms, slug2, url, options) {
|
|
14048
|
+
const config = cms.collection(slug2);
|
|
14049
|
+
if (config.admin?.export === false)
|
|
14050
|
+
throw new ForbiddenError("Export is turned off for this collection");
|
|
14051
|
+
const q = url.searchParams;
|
|
14052
|
+
const lang = q.get("lang") ?? "en";
|
|
14053
|
+
const names = q.get("headers") === "names";
|
|
14054
|
+
const columnsParam = q.get("columns");
|
|
14055
|
+
const asked = columnsParam && columnsParam !== "all" ? columnsParam.split(",").filter(Boolean) : null;
|
|
14056
|
+
const ids = q.get("ids")?.split(",").filter(Boolean);
|
|
14057
|
+
const where = ids?.length ? { and: [...options.where ? [options.where] : [], { id: { in: ids } }] } : options.where;
|
|
14058
|
+
const read2 = {
|
|
14059
|
+
user: options.user,
|
|
14060
|
+
context: options.context,
|
|
14061
|
+
overrideAccess: false,
|
|
14062
|
+
draft: options.draft,
|
|
14063
|
+
...options.locale ? { locale: options.locale } : {},
|
|
14064
|
+
...options.fallbackLocale === false ? { fallbackLocale: false } : {}
|
|
14065
|
+
};
|
|
14066
|
+
const total = await cms.count(slug2, { ...read2, ...where ? { where } : {}, max: EXPORT_MAX });
|
|
14067
|
+
if (total > EXPORT_MAX)
|
|
14068
|
+
throw new QueryError(
|
|
14069
|
+
`More than ${EXPORT_MAX.toLocaleString("en")} documents match: filter the list further`
|
|
14070
|
+
);
|
|
14071
|
+
const columns = await columnsOf(cms, config, asked, options);
|
|
14072
|
+
const header2 = columns.map((c) => names ? c.key : text3(c.label, c.key, lang));
|
|
14073
|
+
await cms.audit.record({
|
|
14074
|
+
action: "export",
|
|
14075
|
+
target: slug2,
|
|
14076
|
+
user: options.user,
|
|
14077
|
+
detail: { rows: total, columns: columns.map((c) => c.key) }
|
|
14078
|
+
});
|
|
14079
|
+
const encoder = new TextEncoder();
|
|
14080
|
+
let cursor = "";
|
|
14081
|
+
const stream = new ReadableStream({
|
|
14082
|
+
start(controller) {
|
|
14083
|
+
controller.enqueue(encoder.encode(`\uFEFF${header2.map(csvCell).join(",")}\r
|
|
14084
|
+
`));
|
|
14085
|
+
},
|
|
14086
|
+
async pull(controller) {
|
|
14087
|
+
if (cursor === null) {
|
|
14088
|
+
controller.close();
|
|
14089
|
+
return;
|
|
14090
|
+
}
|
|
14091
|
+
try {
|
|
14092
|
+
const page = await cms.find(
|
|
14093
|
+
slug2,
|
|
14094
|
+
{
|
|
14095
|
+
...read2,
|
|
14096
|
+
...where ? { where } : {},
|
|
14097
|
+
...options.sort?.length ? { sort: options.sort } : {},
|
|
14098
|
+
limit: STEP,
|
|
14099
|
+
depth: 1,
|
|
14100
|
+
cursor
|
|
14101
|
+
}
|
|
14102
|
+
);
|
|
14103
|
+
const lines = page.docs.map(
|
|
14104
|
+
(doc) => columns.map((c) => csvCell(cellOf(cms, c.field, doc[c.key], c.key, lang))).join(",")
|
|
14105
|
+
);
|
|
14106
|
+
if (lines.length) controller.enqueue(encoder.encode(`${lines.join("\r\n")}\r
|
|
14107
|
+
`));
|
|
14108
|
+
cursor = page.nextCursor ?? null;
|
|
14109
|
+
if (cursor === null) controller.close();
|
|
14110
|
+
} catch (error) {
|
|
14111
|
+
controller.error(error);
|
|
14112
|
+
}
|
|
14113
|
+
}
|
|
14114
|
+
});
|
|
14115
|
+
const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
14116
|
+
return new Response(stream, {
|
|
14117
|
+
headers: {
|
|
14118
|
+
"content-type": "text/csv; charset=utf-8",
|
|
14119
|
+
"content-disposition": `attachment; filename="${slug2}-${day}.csv"`,
|
|
14120
|
+
"cache-control": "no-store",
|
|
14121
|
+
"x-content-type-options": "nosniff"
|
|
14122
|
+
}
|
|
14123
|
+
});
|
|
14124
|
+
}
|
|
14125
|
+
|
|
13100
14126
|
// src/rest/filter-options.ts
|
|
13101
14127
|
function fieldAt2(fields, path) {
|
|
13102
14128
|
let current = fields;
|
|
@@ -13138,12 +14164,12 @@ async function blockSummaries(cms, user, body) {
|
|
|
13138
14164
|
const summary = field.blocks.find((b) => b.slug === blockType)?.admin?.summary;
|
|
13139
14165
|
if (typeof id !== "string" || typeof summary !== "function") continue;
|
|
13140
14166
|
try {
|
|
13141
|
-
const
|
|
14167
|
+
const text4 = await summary({
|
|
13142
14168
|
fields: fields && typeof fields === "object" ? fields : {},
|
|
13143
14169
|
user,
|
|
13144
14170
|
cms
|
|
13145
14171
|
});
|
|
13146
|
-
if (typeof
|
|
14172
|
+
if (typeof text4 === "string") summaries[id] = text4;
|
|
13147
14173
|
} catch (error) {
|
|
13148
14174
|
cms.logger.warn(`admin.summary of block "${String(blockType)}" failed: ${String(error)}`);
|
|
13149
14175
|
}
|
|
@@ -13252,6 +14278,191 @@ function parseDepth(url) {
|
|
|
13252
14278
|
return depth === void 0 ? {} : { depth };
|
|
13253
14279
|
}
|
|
13254
14280
|
|
|
14281
|
+
// src/rest/saved-views.ts
|
|
14282
|
+
var MAX_OWN_VIEWS = 50;
|
|
14283
|
+
var MAX_STATE = 1e4;
|
|
14284
|
+
var QUERY_KEY = /^(where\[and\]\[\d+\]\[[^\]]+\]\[[a-z_]+\]|q|sort|scheduled|type|calendar)$/;
|
|
14285
|
+
var userKey = (user) => String(user.id);
|
|
14286
|
+
var stamps = () => {
|
|
14287
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
14288
|
+
return { createdAt: now, updatedAt: now };
|
|
14289
|
+
};
|
|
14290
|
+
async function readable2(cms, slug2, who) {
|
|
14291
|
+
if (INTERNAL_COLLECTIONS.has(slug2) || !cms.config.collections.some((c) => c.slug === slug2))
|
|
14292
|
+
throw new NotFoundError("collections", slug2);
|
|
14293
|
+
try {
|
|
14294
|
+
await cms.count(slug2, { ...who, overrideAccess: false, where: { id: { exists: false } } });
|
|
14295
|
+
} catch {
|
|
14296
|
+
throw new ForbiddenError();
|
|
14297
|
+
}
|
|
14298
|
+
}
|
|
14299
|
+
function cleanState(raw) {
|
|
14300
|
+
const input = raw && typeof raw === "object" ? raw : {};
|
|
14301
|
+
const query = {};
|
|
14302
|
+
const rawQuery = input.query && typeof input.query === "object" ? input.query : {};
|
|
14303
|
+
for (const [key, value] of Object.entries(rawQuery))
|
|
14304
|
+
if (QUERY_KEY.test(key) && typeof value === "string") query[key] = value;
|
|
14305
|
+
const columns = Array.isArray(input.columns) ? input.columns.filter((c) => typeof c === "string").slice(0, 50) : void 0;
|
|
14306
|
+
const pageSize = [10, 25, 50, 100].includes(input.pageSize) ? input.pageSize : void 0;
|
|
14307
|
+
const density = input.density === "compact" || input.density === "comfortable" ? input.density : void 0;
|
|
14308
|
+
const state = {
|
|
14309
|
+
query,
|
|
14310
|
+
...columns ? { columns } : {},
|
|
14311
|
+
...pageSize ? { pageSize } : {},
|
|
14312
|
+
...density ? { density } : {}
|
|
14313
|
+
};
|
|
14314
|
+
if (JSON.stringify(state).length > MAX_STATE)
|
|
14315
|
+
throw new ValidationError(SAVED_VIEWS, [{ field: "state", message: "is too large" }]);
|
|
14316
|
+
return state;
|
|
14317
|
+
}
|
|
14318
|
+
function toView(row, user) {
|
|
14319
|
+
const mine2 = row.user === userKey(user);
|
|
14320
|
+
return {
|
|
14321
|
+
id: row.id,
|
|
14322
|
+
name: String(row.name ?? ""),
|
|
14323
|
+
shared: row.shared === true,
|
|
14324
|
+
mine: mine2,
|
|
14325
|
+
editable: mine2 || row.shared === true && isSystemAdmin(user),
|
|
14326
|
+
state: cleanState(row.state)
|
|
14327
|
+
};
|
|
14328
|
+
}
|
|
14329
|
+
async function rows(cms, slug2, where) {
|
|
14330
|
+
const found = await cms.db.find({
|
|
14331
|
+
collection: SAVED_VIEWS,
|
|
14332
|
+
where: { and: [{ collection: { equals: slug2 } }, where] },
|
|
14333
|
+
sort: ["id"],
|
|
14334
|
+
limit: 0,
|
|
14335
|
+
page: 1
|
|
14336
|
+
});
|
|
14337
|
+
return found.docs;
|
|
14338
|
+
}
|
|
14339
|
+
async function listViews(cms, slug2, who) {
|
|
14340
|
+
await readable2(cms, slug2, who);
|
|
14341
|
+
const key = userKey(who.user);
|
|
14342
|
+
const found = await rows(cms, slug2, {
|
|
14343
|
+
or: [
|
|
14344
|
+
{ and: [{ kind: { equals: "view" } }, { shared: { equals: true } }] },
|
|
14345
|
+
{ user: { equals: key } }
|
|
14346
|
+
]
|
|
14347
|
+
});
|
|
14348
|
+
const views = found.filter((r) => r.kind === "view").map((r) => toView(r, who.user));
|
|
14349
|
+
const preference = found.find((r) => r.kind === "default" && r.user === key);
|
|
14350
|
+
return {
|
|
14351
|
+
views: [...views.filter((v) => v.shared), ...views.filter((v) => !v.shared)],
|
|
14352
|
+
default: typeof preference?.target === "string" ? preference.target : null
|
|
14353
|
+
};
|
|
14354
|
+
}
|
|
14355
|
+
function checkName(name) {
|
|
14356
|
+
const trimmed = typeof name === "string" ? name.trim() : "";
|
|
14357
|
+
if (trimmed === "" || trimmed.length > 60)
|
|
14358
|
+
throw new ValidationError(SAVED_VIEWS, [
|
|
14359
|
+
{ field: "name", message: "must be 1 to 60 characters" }
|
|
14360
|
+
]);
|
|
14361
|
+
return trimmed;
|
|
14362
|
+
}
|
|
14363
|
+
async function checkUnique(cms, slug2, who, name, shared, self) {
|
|
14364
|
+
const same = await rows(cms, slug2, {
|
|
14365
|
+
and: [
|
|
14366
|
+
{ kind: { equals: "view" } },
|
|
14367
|
+
{ name: { equals: name } },
|
|
14368
|
+
shared ? { shared: { equals: true } } : { user: { equals: userKey(who.user) } }
|
|
14369
|
+
]
|
|
14370
|
+
});
|
|
14371
|
+
if (same.some((r) => r.id !== self && (shared || r.shared !== true)))
|
|
14372
|
+
throw new ValidationError(SAVED_VIEWS, [
|
|
14373
|
+
{ field: "name", message: `there is already a view named "${name}"` }
|
|
14374
|
+
]);
|
|
14375
|
+
}
|
|
14376
|
+
async function createView(cms, slug2, who, body) {
|
|
14377
|
+
await readable2(cms, slug2, who);
|
|
14378
|
+
const input = body ?? {};
|
|
14379
|
+
const name = checkName(input.name);
|
|
14380
|
+
const shared = input.shared === true;
|
|
14381
|
+
await checkUnique(cms, slug2, who, name, shared);
|
|
14382
|
+
const own = await rows(cms, slug2, {
|
|
14383
|
+
and: [{ kind: { equals: "view" } }, { user: { equals: userKey(who.user) } }]
|
|
14384
|
+
});
|
|
14385
|
+
if (own.length >= MAX_OWN_VIEWS)
|
|
14386
|
+
throw new ValidationError(SAVED_VIEWS, [
|
|
14387
|
+
{ field: "name", message: `at most ${MAX_OWN_VIEWS} views per list` }
|
|
14388
|
+
]);
|
|
14389
|
+
const row = await cms.db.create({
|
|
14390
|
+
collection: SAVED_VIEWS,
|
|
14391
|
+
data: {
|
|
14392
|
+
collection: slug2,
|
|
14393
|
+
user: userKey(who.user),
|
|
14394
|
+
kind: "view",
|
|
14395
|
+
name,
|
|
14396
|
+
shared,
|
|
14397
|
+
state: cleanState(input.state),
|
|
14398
|
+
target: null,
|
|
14399
|
+
...stamps()
|
|
14400
|
+
}
|
|
14401
|
+
});
|
|
14402
|
+
return toView(row, who.user);
|
|
14403
|
+
}
|
|
14404
|
+
async function editable(cms, slug2, who, id) {
|
|
14405
|
+
await readable2(cms, slug2, who);
|
|
14406
|
+
const row = await cms.db.findById({ collection: SAVED_VIEWS, id });
|
|
14407
|
+
if (!row || row.collection !== slug2 || row.kind !== "view")
|
|
14408
|
+
throw new NotFoundError(SAVED_VIEWS, id);
|
|
14409
|
+
const view = toView(row, who.user);
|
|
14410
|
+
if (!view.mine && !view.shared) throw new NotFoundError(SAVED_VIEWS, id);
|
|
14411
|
+
if (!view.editable) throw new ForbiddenError("Only its owner or an admin can change this view");
|
|
14412
|
+
return row;
|
|
14413
|
+
}
|
|
14414
|
+
async function updateView(cms, slug2, who, id, body) {
|
|
14415
|
+
const row = await editable(cms, slug2, who, id);
|
|
14416
|
+
const input = body ?? {};
|
|
14417
|
+
const name = input.name === void 0 ? String(row.name) : checkName(input.name);
|
|
14418
|
+
const shared = input.shared === void 0 ? row.shared === true : input.shared === true;
|
|
14419
|
+
if (name !== row.name || shared !== (row.shared === true))
|
|
14420
|
+
await checkUnique(cms, slug2, who, name, shared, id);
|
|
14421
|
+
const updated = await cms.db.update({
|
|
14422
|
+
collection: SAVED_VIEWS,
|
|
14423
|
+
id,
|
|
14424
|
+
// The whole row, as the adapter writes it.
|
|
14425
|
+
data: {
|
|
14426
|
+
...row,
|
|
14427
|
+
name,
|
|
14428
|
+
shared,
|
|
14429
|
+
...input.state !== void 0 ? { state: cleanState(input.state) } : {},
|
|
14430
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14431
|
+
}
|
|
14432
|
+
});
|
|
14433
|
+
return toView(updated, who.user);
|
|
14434
|
+
}
|
|
14435
|
+
async function deleteView(cms, slug2, who, id) {
|
|
14436
|
+
await editable(cms, slug2, who, id);
|
|
14437
|
+
await cms.db.delete({ collection: SAVED_VIEWS, id });
|
|
14438
|
+
return { deleted: 1 };
|
|
14439
|
+
}
|
|
14440
|
+
async function setDefaultView(cms, slug2, who, body) {
|
|
14441
|
+
await readable2(cms, slug2, who);
|
|
14442
|
+
const raw = body ?? {};
|
|
14443
|
+
const target = raw.view === null || raw.view === void 0 ? null : String(raw.view);
|
|
14444
|
+
const key = userKey(who.user);
|
|
14445
|
+
const current = await rows(cms, slug2, {
|
|
14446
|
+
and: [{ kind: { equals: "default" } }, { user: { equals: key } }]
|
|
14447
|
+
});
|
|
14448
|
+
for (const row of current) await cms.db.delete({ collection: SAVED_VIEWS, id: row.id });
|
|
14449
|
+
if (target !== null)
|
|
14450
|
+
await cms.db.create({
|
|
14451
|
+
collection: SAVED_VIEWS,
|
|
14452
|
+
data: {
|
|
14453
|
+
collection: slug2,
|
|
14454
|
+
user: key,
|
|
14455
|
+
kind: "default",
|
|
14456
|
+
name: null,
|
|
14457
|
+
shared: false,
|
|
14458
|
+
state: null,
|
|
14459
|
+
target,
|
|
14460
|
+
...stamps()
|
|
14461
|
+
}
|
|
14462
|
+
});
|
|
14463
|
+
return { default: target };
|
|
14464
|
+
}
|
|
14465
|
+
|
|
13255
14466
|
// src/rest/handler.ts
|
|
13256
14467
|
var SCHEDULED_RANGE_LIMIT = 500;
|
|
13257
14468
|
var CSRF_COOKIE = "ecms-csrf";
|
|
@@ -13443,7 +14654,8 @@ var ADMIN_UI_ROUTES = /* @__PURE__ */ new Set([
|
|
|
13443
14654
|
"media-usage",
|
|
13444
14655
|
"owned",
|
|
13445
14656
|
"sso",
|
|
13446
|
-
"block-summaries"
|
|
14657
|
+
"block-summaries",
|
|
14658
|
+
"views"
|
|
13447
14659
|
]);
|
|
13448
14660
|
var CORS_METHODS = "GET, HEAD, POST, PATCH, PUT, DELETE";
|
|
13449
14661
|
var CORS_HEADERS = `authorization, content-type, ${CSRF_HEADER}`;
|
|
@@ -13820,6 +15032,27 @@ async function route(cms, ctx, method, path, options) {
|
|
|
13820
15032
|
if (parsed === void 0) throw new HttpError("Not found", 404);
|
|
13821
15033
|
return { body: await cms.roles.owned(parsed) };
|
|
13822
15034
|
}
|
|
15035
|
+
if (first === "admin" && second === "views" && third !== void 0 && segments.length <= 4) {
|
|
15036
|
+
if (!ctx.user || ctx.user.apiKey) throw new UnauthorizedError();
|
|
15037
|
+
const who = { user: ctx.user, context: ctx.context };
|
|
15038
|
+
const fourth = segments[3];
|
|
15039
|
+
if (fourth === void 0) {
|
|
15040
|
+
if (method === "GET") return { body: await listViews(cms, third, who) };
|
|
15041
|
+
if (method === "POST")
|
|
15042
|
+
return { status: 201, body: await createView(cms, third, who, await readJson(ctx.request)) };
|
|
15043
|
+
throw methodNotAllowed(ctx, "GET, POST");
|
|
15044
|
+
}
|
|
15045
|
+
if (fourth === "default") {
|
|
15046
|
+
if (method !== "PUT") throw methodNotAllowed(ctx, "PUT");
|
|
15047
|
+
return { body: await setDefaultView(cms, third, who, await readJson(ctx.request)) };
|
|
15048
|
+
}
|
|
15049
|
+
const id2 = parseId(fourth);
|
|
15050
|
+
if (id2 === void 0) throw new HttpError("Not found", 404);
|
|
15051
|
+
if (method === "PATCH")
|
|
15052
|
+
return { body: await updateView(cms, third, who, id2, await readJson(ctx.request)) };
|
|
15053
|
+
if (method === "DELETE") return { body: await deleteView(cms, third, who, id2) };
|
|
15054
|
+
throw methodNotAllowed(ctx, "PATCH, DELETE");
|
|
15055
|
+
}
|
|
13823
15056
|
if (first === "admin" && second === "block-summaries" && third === void 0) {
|
|
13824
15057
|
if (!ctx.user) throw new UnauthorizedError();
|
|
13825
15058
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
@@ -13864,6 +15097,91 @@ async function route(cms, ctx, method, path, options) {
|
|
|
13864
15097
|
}
|
|
13865
15098
|
throw methodNotAllowed(ctx, "PATCH, DELETE");
|
|
13866
15099
|
}
|
|
15100
|
+
if (first === "admin" && second === "integrations") {
|
|
15101
|
+
if (!ctx.user) throw new UnauthorizedError();
|
|
15102
|
+
if (cms.config.integrations.length === 0) throw new HttpError("Not found", 404);
|
|
15103
|
+
if (!await cms.roles.canView(ctx.user, "integrations")) throw new ForbiddenError();
|
|
15104
|
+
const user = ctx.user;
|
|
15105
|
+
const [, , id2, part, cid, action, extra] = segments;
|
|
15106
|
+
const integrations = cms.integrations;
|
|
15107
|
+
if (id2 === void 0) {
|
|
15108
|
+
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
15109
|
+
return { body: await integrations.overview() };
|
|
15110
|
+
}
|
|
15111
|
+
const callbackUrl = `${cms.config.serverURL ?? ctx.url.origin}${cms.config.routes.api}/admin/integrations/${id2}/callback`;
|
|
15112
|
+
if (part === void 0) {
|
|
15113
|
+
if (method !== "PUT") throw methodNotAllowed(ctx, "PUT");
|
|
15114
|
+
const body = await readJson(ctx.request);
|
|
15115
|
+
return { body: await integrations.setEnabled(id2, body.enabled !== false, user) };
|
|
15116
|
+
}
|
|
15117
|
+
if (part === "authorize" && cid === void 0) {
|
|
15118
|
+
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
15119
|
+
const body = await readJson(ctx.request);
|
|
15120
|
+
return { body: integrations.authorize(id2, user, callbackUrl, String(body.name ?? "")) };
|
|
15121
|
+
}
|
|
15122
|
+
if (part === "callback" && cid === void 0) {
|
|
15123
|
+
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
15124
|
+
const page = new URL(`${cms.config.admin.path}/integrations`, ctx.url.origin);
|
|
15125
|
+
page.searchParams.set("integration", id2);
|
|
15126
|
+
try {
|
|
15127
|
+
const error = ctx.url.searchParams.get("error_description") ?? ctx.url.searchParams.get("error");
|
|
15128
|
+
if (error) throw new Error(error);
|
|
15129
|
+
const result = await integrations.callback(
|
|
15130
|
+
id2,
|
|
15131
|
+
ctx.url.searchParams.get("code") ?? "",
|
|
15132
|
+
ctx.url.searchParams.get("state") ?? "",
|
|
15133
|
+
callbackUrl,
|
|
15134
|
+
user
|
|
15135
|
+
);
|
|
15136
|
+
if ("pending" in result) page.searchParams.set("pending", String(result.pending));
|
|
15137
|
+
else page.searchParams.set("connected", "1");
|
|
15138
|
+
} catch (error) {
|
|
15139
|
+
page.searchParams.set("error", error.message.slice(0, 300));
|
|
15140
|
+
}
|
|
15141
|
+
return { body: new Response(null, { status: 302, headers: { location: page.toString() } }) };
|
|
15142
|
+
}
|
|
15143
|
+
if (part === "lookup" && cid !== void 0 && action === void 0) {
|
|
15144
|
+
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
15145
|
+
return { body: await integrations.lookup(id2, cid, await readJson(ctx.request)) };
|
|
15146
|
+
}
|
|
15147
|
+
if (part === "pending" && cid !== void 0 && action === void 0) {
|
|
15148
|
+
const pid = parseId(cid);
|
|
15149
|
+
if (pid === void 0) throw new HttpError("Not found", 404);
|
|
15150
|
+
if (method === "GET") return { body: await integrations.pending(id2, pid, user) };
|
|
15151
|
+
if (method === "POST") {
|
|
15152
|
+
const body = await readJson(ctx.request);
|
|
15153
|
+
return {
|
|
15154
|
+
status: 201,
|
|
15155
|
+
body: await integrations.choose(id2, pid, String(body.account ?? ""), user)
|
|
15156
|
+
};
|
|
15157
|
+
}
|
|
15158
|
+
throw methodNotAllowed(ctx, "GET, POST");
|
|
15159
|
+
}
|
|
15160
|
+
if (part === "connections" && extra === void 0) {
|
|
15161
|
+
if (cid === void 0) {
|
|
15162
|
+
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
15163
|
+
return {
|
|
15164
|
+
status: 201,
|
|
15165
|
+
body: await integrations.connect(id2, await readJson(ctx.request), user)
|
|
15166
|
+
};
|
|
15167
|
+
}
|
|
15168
|
+
if (action === "test") {
|
|
15169
|
+
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
15170
|
+
const target = cid === "env" ? "env" : parseId(cid);
|
|
15171
|
+
if (target === void 0) throw new HttpError("Not found", 404);
|
|
15172
|
+
return { body: await integrations.test(id2, target) };
|
|
15173
|
+
}
|
|
15174
|
+
const connection = parseId(cid);
|
|
15175
|
+
if (connection === void 0 || action !== void 0) throw new HttpError("Not found", 404);
|
|
15176
|
+
if (method === "PATCH")
|
|
15177
|
+
return {
|
|
15178
|
+
body: await integrations.update(id2, connection, await readJson(ctx.request), user)
|
|
15179
|
+
};
|
|
15180
|
+
if (method === "DELETE") return { body: await integrations.disconnect(id2, connection, user) };
|
|
15181
|
+
throw methodNotAllowed(ctx, "PATCH, DELETE");
|
|
15182
|
+
}
|
|
15183
|
+
throw new HttpError("Not found", 404);
|
|
15184
|
+
}
|
|
13867
15185
|
if (first === "admin" && second === "deliveries") {
|
|
13868
15186
|
if (!ctx.user) throw new UnauthorizedError();
|
|
13869
15187
|
if (!await cms.roles.canView(ctx.user, "deliveries")) throw new ForbiddenError();
|
|
@@ -14166,6 +15484,21 @@ async function route(cms, ctx, method, path, options) {
|
|
|
14166
15484
|
if (INTERNAL_COLLECTIONS.has(collection2) || !cms.config.collections.some((c) => c.slug === collection2)) {
|
|
14167
15485
|
throw new HttpError(`Unknown collection "${collection2}"`, 404);
|
|
14168
15486
|
}
|
|
15487
|
+
if (second === "export.csv" && third === void 0) {
|
|
15488
|
+
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
15489
|
+
if (!ctx.user) throw new UnauthorizedError();
|
|
15490
|
+
const { where, sort } = parseListQuery(ctx.url);
|
|
15491
|
+
return {
|
|
15492
|
+
body: await exportCsv(cms, collection2, ctx.url, {
|
|
15493
|
+
user: ctx.user,
|
|
15494
|
+
context: ctx.context,
|
|
15495
|
+
...parseLocale(ctx.url),
|
|
15496
|
+
draft,
|
|
15497
|
+
...where ? { where } : {},
|
|
15498
|
+
...sort ? { sort } : {}
|
|
15499
|
+
})
|
|
15500
|
+
};
|
|
15501
|
+
}
|
|
14169
15502
|
if (second === "bulk") return bulkRoute(cms, ctx, method, collection2, segments.slice(2), access);
|
|
14170
15503
|
if (second !== void 0 && third !== void 0) {
|
|
14171
15504
|
return documentAction(cms, ctx, method, collection2, second, segments.slice(2));
|
|
@@ -14623,10 +15956,10 @@ async function readJson(request) {
|
|
|
14623
15956
|
if (!type.toLowerCase().startsWith("application/json")) {
|
|
14624
15957
|
throw new HttpError("Content-Type must be application/json", 415);
|
|
14625
15958
|
}
|
|
14626
|
-
const
|
|
15959
|
+
const text4 = new TextDecoder().decode(await readBody(request, MAX_BODY_BYTES));
|
|
14627
15960
|
let body;
|
|
14628
15961
|
try {
|
|
14629
|
-
body = JSON.parse(
|
|
15962
|
+
body = JSON.parse(text4);
|
|
14630
15963
|
} catch {
|
|
14631
15964
|
throw new HttpError("Request body is not valid JSON", 400);
|
|
14632
15965
|
}
|