@easy-cms/core 1.31.0 → 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-ZA5KIRYX.js → chunk-J4NASOMO.js} +908 -20
- package/dist/{config-BUySbadL.d.ts → config-Dr_NZGm6.d.ts} +300 -3
- package/dist/{handler-rhJ8-GMQ.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-DmTHvlFA.d.ts → plugin-DBHFjM-r.d.ts} +4 -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
|
}
|
|
@@ -162,6 +163,7 @@ var OUTBOX = "outbox";
|
|
|
162
163
|
var RATE_LIMITS = "rate-limits";
|
|
163
164
|
var BULK_OPERATIONS = "bulk-operations";
|
|
164
165
|
var SAVED_VIEWS = "saved-views";
|
|
166
|
+
var CONNECTIONS = "connections";
|
|
165
167
|
var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
166
168
|
SESSIONS,
|
|
167
169
|
LOGIN_ATTEMPTS,
|
|
@@ -177,7 +179,8 @@ var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
|
177
179
|
OUTBOX,
|
|
178
180
|
RATE_LIMITS,
|
|
179
181
|
BULK_OPERATIONS,
|
|
180
|
-
SAVED_VIEWS
|
|
182
|
+
SAVED_VIEWS,
|
|
183
|
+
CONNECTIONS
|
|
181
184
|
]);
|
|
182
185
|
var DEFAULT_ROLES = ["admin", "editor"];
|
|
183
186
|
var CREATED_BY_FIELD = {
|
|
@@ -581,6 +584,24 @@ var savedViewsCollection = {
|
|
|
581
584
|
{ name: "target", type: "text" }
|
|
582
585
|
]
|
|
583
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
|
+
};
|
|
584
605
|
var webhookDeliveriesCollection = {
|
|
585
606
|
slug: WEBHOOK_DELIVERIES,
|
|
586
607
|
access: { read: nobody, create: nobody, update: nobody, delete: nobody },
|
|
@@ -3023,6 +3044,8 @@ var Mailer = class {
|
|
|
3023
3044
|
logger;
|
|
3024
3045
|
queue;
|
|
3025
3046
|
pending = /* @__PURE__ */ new Set();
|
|
3047
|
+
/** Told when an email is given up on (`delivery.failed`). */
|
|
3048
|
+
onGiveUp;
|
|
3026
3049
|
get configured() {
|
|
3027
3050
|
return this.adapter !== void 0;
|
|
3028
3051
|
}
|
|
@@ -3080,6 +3103,8 @@ var Mailer = class {
|
|
|
3080
3103
|
if (delay === void 0) {
|
|
3081
3104
|
failed++;
|
|
3082
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 });
|
|
3083
3108
|
}
|
|
3084
3109
|
await this.queue.update(id, {
|
|
3085
3110
|
...queued,
|
|
@@ -3559,11 +3584,13 @@ async function runBackup(cms, record) {
|
|
|
3559
3584
|
});
|
|
3560
3585
|
cms.logger.info(`Backup ${key} written (${data.byteLength} bytes)`);
|
|
3561
3586
|
} catch (error) {
|
|
3587
|
+
const message2 = (error.message || String(error)).slice(0, 500);
|
|
3562
3588
|
await update(cms, row, {
|
|
3563
3589
|
state: "failed",
|
|
3564
|
-
error:
|
|
3590
|
+
error: message2,
|
|
3565
3591
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3566
3592
|
});
|
|
3593
|
+
cms.systemEvent("backup.failed", { backup: row.id, error: message2 });
|
|
3567
3594
|
throw error;
|
|
3568
3595
|
} finally {
|
|
3569
3596
|
await rm2(dir, { recursive: true, force: true });
|
|
@@ -4309,7 +4336,7 @@ var VersionStore = class {
|
|
|
4309
4336
|
};
|
|
4310
4337
|
|
|
4311
4338
|
// src/local-api.ts
|
|
4312
|
-
import { createHash as
|
|
4339
|
+
import { createHash as createHash6, createHmac as createHmac8, randomBytes as randomBytes7, randomUUID as randomUUID3 } from "crypto";
|
|
4313
4340
|
|
|
4314
4341
|
// src/auth/common-passwords.ts
|
|
4315
4342
|
var LIST = `
|
|
@@ -4826,6 +4853,649 @@ function toOperation(row) {
|
|
|
4826
4853
|
};
|
|
4827
4854
|
}
|
|
4828
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
|
+
|
|
4829
5499
|
// src/localization.ts
|
|
4830
5500
|
var ALL_LOCALES = "all";
|
|
4831
5501
|
var isPlainObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -5480,7 +6150,7 @@ function read(response, maxBytes, deadline) {
|
|
|
5480
6150
|
|
|
5481
6151
|
// src/config-keys.ts
|
|
5482
6152
|
var keys = (list) => new Set(list.split(/\s+/).filter(Boolean));
|
|
5483
|
-
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
|
|
5484
6154
|
upload auth collections globals endpoints cliCommands onRequest fieldTypes apiKeys email backups
|
|
5485
6155
|
audit logs countCacheSeconds maxPage httpCache outbox rateLimit search telemetry plugins`);
|
|
5486
6156
|
var ADMIN = keys("path locale brand siteURL nav commands modules pages dashboard switcher");
|
|
@@ -5663,7 +6333,8 @@ var BUILTIN_NAV = [
|
|
|
5663
6333
|
children: [
|
|
5664
6334
|
{ id: "site", label: { en: "Site", th: "\u0E40\u0E27\u0E47\u0E1A\u0E44\u0E0B\u0E15\u0E4C" }, icon: "globe" },
|
|
5665
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" },
|
|
5666
|
-
{ 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" }
|
|
5667
6338
|
]
|
|
5668
6339
|
}
|
|
5669
6340
|
];
|
|
@@ -6016,6 +6687,12 @@ function verifyWebhook(body, headers, secret, options = {}) {
|
|
|
6016
6687
|
}
|
|
6017
6688
|
|
|
6018
6689
|
// src/webhooks.ts
|
|
6690
|
+
var SYSTEM_EVENTS = [
|
|
6691
|
+
"scheduled.failed",
|
|
6692
|
+
"delivery.failed",
|
|
6693
|
+
"backup.failed",
|
|
6694
|
+
"integration.failed"
|
|
6695
|
+
];
|
|
6019
6696
|
var WEBHOOK_EVENTS = [
|
|
6020
6697
|
"create",
|
|
6021
6698
|
"update",
|
|
@@ -6207,10 +6884,13 @@ var Webhooks = class {
|
|
|
6207
6884
|
return { ok: false, error: error.message };
|
|
6208
6885
|
}
|
|
6209
6886
|
}
|
|
6887
|
+
/** Told when a delivery is given up on (`delivery.failed`). */
|
|
6888
|
+
onGiveUp;
|
|
6210
6889
|
giveUp(hook, event, error, attempts) {
|
|
6211
6890
|
this.logger.error(
|
|
6212
6891
|
`Webhook ${hook.url} failed for ${event} after ${attempts} attempts: ${error}`
|
|
6213
6892
|
);
|
|
6893
|
+
this.onGiveUp?.({ url: hook.url, event, error, attempts });
|
|
6214
6894
|
}
|
|
6215
6895
|
};
|
|
6216
6896
|
var later4 = (from, ms) => new Date(from.getTime() + ms).toISOString();
|
|
@@ -6299,6 +6979,8 @@ function validateConfig(config) {
|
|
|
6299
6979
|
validateEndpoints(config, collectionSlugs, add);
|
|
6300
6980
|
validateCommands(config, add);
|
|
6301
6981
|
validateJobs(config, add);
|
|
6982
|
+
validateIntegrations(config, add);
|
|
6983
|
+
validateListeners(config, add);
|
|
6302
6984
|
validateTimeZone(config, add);
|
|
6303
6985
|
validateEvents(config, add);
|
|
6304
6986
|
const apiKeys = config.apiKeys;
|
|
@@ -6826,7 +7508,7 @@ function validateWebhooks(config, add) {
|
|
|
6826
7508
|
...(config.collections ?? []).map((c) => c?.slug)
|
|
6827
7509
|
]);
|
|
6828
7510
|
const globals = new Set((config.globals ?? []).map((g) => g?.slug));
|
|
6829
|
-
const custom = new Set(config.events ?? []);
|
|
7511
|
+
const custom = /* @__PURE__ */ new Set([...config.events ?? [], ...SYSTEM_EVENTS]);
|
|
6830
7512
|
for (const [i, hook] of hooks.entries()) {
|
|
6831
7513
|
const path = `webhooks[${i}]`;
|
|
6832
7514
|
if (typeof hook?.url !== "string" || !/^https?:\/\/\S+$/.test(hook.url)) {
|
|
@@ -7916,6 +8598,56 @@ function validateAdapterVersions(config, add) {
|
|
|
7916
8598
|
);
|
|
7917
8599
|
}
|
|
7918
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
|
+
}
|
|
7919
8651
|
|
|
7920
8652
|
// src/resolve-config.ts
|
|
7921
8653
|
var DEFAULT_ADMIN_PATH = "/admin";
|
|
@@ -8026,14 +8758,17 @@ async function resolveConfig(input) {
|
|
|
8026
8758
|
databaseBackupsCollection,
|
|
8027
8759
|
bulkOperationsCollection,
|
|
8028
8760
|
savedViewsCollection,
|
|
8761
|
+
...resolvedIntegrations(config).length > 0 ? [connectionsCollection] : [],
|
|
8029
8762
|
...config.auth?.rbac ? [rolesCollection] : [],
|
|
8030
8763
|
...(config.auth?.providers?.length ?? 0) > 0 ? [userIdentitiesCollection] : [],
|
|
8031
8764
|
...config.audit ? [auditLogsCollection] : []
|
|
8032
8765
|
],
|
|
8033
8766
|
globals: config.globals ?? [],
|
|
8034
8767
|
endpoints: config.endpoints ?? [],
|
|
8768
|
+
integrations: resolvedIntegrations(config),
|
|
8035
8769
|
cliCommands: config.cliCommands ?? [],
|
|
8036
|
-
|
|
8770
|
+
// Easy CMS's own events (#356), for webhooks and `cms.on`.
|
|
8771
|
+
events: [.../* @__PURE__ */ new Set([...config.events ?? [], ...SYSTEM_EVENTS])],
|
|
8037
8772
|
jobs: config.jobs ?? [],
|
|
8038
8773
|
warnings,
|
|
8039
8774
|
installedPlugins: (plugins ?? []).map(
|
|
@@ -8109,9 +8844,12 @@ function resolveAudit(audit) {
|
|
|
8109
8844
|
scope: audit.scope ?? false
|
|
8110
8845
|
};
|
|
8111
8846
|
}
|
|
8847
|
+
function resolvedIntegrations(config) {
|
|
8848
|
+
return config.integrations ?? [];
|
|
8849
|
+
}
|
|
8112
8850
|
|
|
8113
8851
|
// src/rest/http-cache.ts
|
|
8114
|
-
import { createHash as
|
|
8852
|
+
import { createHash as createHash5 } from "crypto";
|
|
8115
8853
|
var cacheTags = {
|
|
8116
8854
|
collection: (slug2) => ["ecms", `ecms:${slug2}`],
|
|
8117
8855
|
document: (slug2, id) => ["ecms", `ecms:${slug2}`, `ecms:${slug2}:${id}`],
|
|
@@ -8138,7 +8876,7 @@ function cacheableResponse(request, body, status, headers, times, tags) {
|
|
|
8138
8876
|
);
|
|
8139
8877
|
headers.set("cache-tag", tags.join(","));
|
|
8140
8878
|
headers.set("surrogate-key", tags.join(" "));
|
|
8141
|
-
const etag = `W/"${
|
|
8879
|
+
const etag = `W/"${createHash5("sha1").update(body).digest("base64url")}"`;
|
|
8142
8880
|
headers.set("etag", etag);
|
|
8143
8881
|
if (matches2(request.headers.get("if-none-match"), etag))
|
|
8144
8882
|
return new Response(null, { status: 304, headers });
|
|
@@ -8146,7 +8884,7 @@ function cacheableResponse(request, body, status, headers, times, tags) {
|
|
|
8146
8884
|
}
|
|
8147
8885
|
|
|
8148
8886
|
// src/version.ts
|
|
8149
|
-
var VERSION = true ? "1.
|
|
8887
|
+
var VERSION = true ? "1.32.0" : "0.0.0-dev";
|
|
8150
8888
|
|
|
8151
8889
|
// src/telemetry.ts
|
|
8152
8890
|
var DB_CALLS = /* @__PURE__ */ new Set([
|
|
@@ -8423,6 +9161,8 @@ var EasyCMS = class {
|
|
|
8423
9161
|
audit;
|
|
8424
9162
|
/** Folders of the media library and their permissions (`upload.folders`). */
|
|
8425
9163
|
folders;
|
|
9164
|
+
/** Accounts connected to plugins' integrations (`integrations`, Settings → Integrations). */
|
|
9165
|
+
integrations;
|
|
8426
9166
|
versions;
|
|
8427
9167
|
webhooks;
|
|
8428
9168
|
mailer;
|
|
@@ -8450,6 +9190,12 @@ var EasyCMS = class {
|
|
|
8450
9190
|
(entry) => this.audit.record(entry)
|
|
8451
9191
|
);
|
|
8452
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
|
+
}
|
|
8453
9199
|
this.folders = new MediaFolders(config, db, this.roles);
|
|
8454
9200
|
this.webhooks = new Webhooks(
|
|
8455
9201
|
config.webhooks ?? [],
|
|
@@ -8461,6 +9207,8 @@ var EasyCMS = class {
|
|
|
8461
9207
|
logger,
|
|
8462
9208
|
config.collections.some((c) => c.slug === EMAIL_DELIVERIES) ? this.emailQueue() : void 0
|
|
8463
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 });
|
|
8464
9212
|
this.outbox = config.collections.some((c) => c.slug === OUTBOX) ? new Outbox(
|
|
8465
9213
|
db,
|
|
8466
9214
|
logger,
|
|
@@ -8653,7 +9401,7 @@ var EasyCMS = class {
|
|
|
8653
9401
|
}
|
|
8654
9402
|
]);
|
|
8655
9403
|
}
|
|
8656
|
-
const random2 =
|
|
9404
|
+
const random2 = randomBytes7(4).toString("hex");
|
|
8657
9405
|
const isPrivate = await this.folders.isPrivate(folderOf(MEDIA, data));
|
|
8658
9406
|
const storage = this.storageFor(isPrivate);
|
|
8659
9407
|
const filename = withPrivacy(storageKey(file.name, mimeType, random2), isPrivate);
|
|
@@ -8721,7 +9469,7 @@ var EasyCMS = class {
|
|
|
8721
9469
|
]);
|
|
8722
9470
|
const isPrivate = await this.folders.isPrivate(folder);
|
|
8723
9471
|
const storage = this.storageFor(isPrivate);
|
|
8724
|
-
const key = withPrivacy(storageKey(name, type,
|
|
9472
|
+
const key = withPrivacy(storageKey(name, type, randomBytes7(4).toString("hex")), isPrivate);
|
|
8725
9473
|
const upload = storage.uploadURL ? await storage.uploadURL(key, { contentType: type, size, expiresIn: UPLOAD_URL_SECONDS }) : null;
|
|
8726
9474
|
const ticket = this.sign("upload", {
|
|
8727
9475
|
key,
|
|
@@ -10125,6 +10873,37 @@ var EasyCMS = class {
|
|
|
10125
10873
|
if (!this.config.events.includes(event))
|
|
10126
10874
|
throw new QueryError(`Unknown event "${event}": add it to \`events\` in the config`);
|
|
10127
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);
|
|
10128
10907
|
}
|
|
10129
10908
|
/**
|
|
10130
10909
|
* Tries webhook deliveries that are due for another attempt. Deliveries are kept in the
|
|
@@ -10283,6 +11062,13 @@ var EasyCMS = class {
|
|
|
10283
11062
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10284
11063
|
}
|
|
10285
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
|
+
});
|
|
10286
11072
|
}
|
|
10287
11073
|
}
|
|
10288
11074
|
return { ran, failed };
|
|
@@ -10911,8 +11697,10 @@ var EasyCMS = class {
|
|
|
10911
11697
|
* are logged.
|
|
10912
11698
|
*/
|
|
10913
11699
|
emitChange(config, event, doc, before) {
|
|
10914
|
-
for (const change of this.changeEvents(config, event, doc, before))
|
|
11700
|
+
for (const change of this.changeEvents(config, event, doc, before)) {
|
|
10915
11701
|
this.webhooks.emit(change.event, change.target, change.body);
|
|
11702
|
+
this.dispatch(change.event, change.body, change.target);
|
|
11703
|
+
}
|
|
10916
11704
|
if (this.searches(config, event)) this.searchIndex?.syncLater(config.slug, doc.id);
|
|
10917
11705
|
const purge = this.config.httpCache?.purge;
|
|
10918
11706
|
const tags = this.purgeTags(config, event, doc, before);
|
|
@@ -10962,8 +11750,10 @@ var EasyCMS = class {
|
|
|
10962
11750
|
}
|
|
10963
11751
|
if (!done.has("webhooks")) {
|
|
10964
11752
|
const changes = this.changeEvents(config, event, doc, entry.before);
|
|
10965
|
-
for (const change of changes)
|
|
11753
|
+
for (const change of changes) {
|
|
10966
11754
|
await this.webhooks.emitSaved(change.event, change.target, change.body);
|
|
11755
|
+
this.dispatch(change.event, change.body, change.target);
|
|
11756
|
+
}
|
|
10967
11757
|
if (changes.length > 0 && (this.config.webhooks?.length ?? 0) > 0) await mark("webhooks");
|
|
10968
11758
|
}
|
|
10969
11759
|
if (!done.has("search") && this.searchIndex && this.searches(config, event)) {
|
|
@@ -11454,7 +12244,7 @@ function durationSeconds(value) {
|
|
|
11454
12244
|
}
|
|
11455
12245
|
function cursorQuery(collection2, where, sort, locale, draft) {
|
|
11456
12246
|
const key = JSON.stringify([collection2, where ?? null, sort, locale ?? null, draft]);
|
|
11457
|
-
return
|
|
12247
|
+
return createHash6("sha256").update(key).digest("base64url").slice(0, 16);
|
|
11458
12248
|
}
|
|
11459
12249
|
function writeCursor(keys2, query) {
|
|
11460
12250
|
return Buffer.from(JSON.stringify({ k: keys2, q: query })).toString("base64url");
|
|
@@ -11764,7 +12554,8 @@ var Roles = class {
|
|
|
11764
12554
|
async canView(user, id) {
|
|
11765
12555
|
if (isSystemAdmin(user)) return true;
|
|
11766
12556
|
if (user.apiKey) return false;
|
|
11767
|
-
if ((id === "deliveries" || id === "jobs") && user.scoped === true)
|
|
12557
|
+
if ((id === "deliveries" || id === "jobs" || id === "integrations") && user.scoped === true)
|
|
12558
|
+
return false;
|
|
11768
12559
|
if (id === "audit" && user.role === "admin" && user.scoped === true && this.config.audit !== false && typeof this.config.audit.scope === "function")
|
|
11769
12560
|
return true;
|
|
11770
12561
|
if (!this.enabled)
|
|
@@ -12052,6 +12843,7 @@ var Roles = class {
|
|
|
12052
12843
|
(c) => c.slug === WEBHOOK_DELIVERIES || c.slug === EMAIL_DELIVERIES
|
|
12053
12844
|
) ? [{ id: "deliveries" }] : [],
|
|
12054
12845
|
...hasJobsPage(this.config) ? [{ id: "jobs" }] : [],
|
|
12846
|
+
...this.config.integrations.length > 0 ? [{ id: "integrations" }] : [],
|
|
12055
12847
|
...hasCalendarPage(this.config) ? [{ id: "calendar" }] : [],
|
|
12056
12848
|
...this.config.admin.pages.map((page) => ({ id: viewId.page(page.path), label: page.label })),
|
|
12057
12849
|
...this.config.admin.dashboard.map((widget) => ({
|
|
@@ -12254,7 +13046,7 @@ function rank(role, config) {
|
|
|
12254
13046
|
}
|
|
12255
13047
|
|
|
12256
13048
|
// src/rest/admin-modules.ts
|
|
12257
|
-
import { createHash as
|
|
13049
|
+
import { createHash as createHash7 } from "crypto";
|
|
12258
13050
|
import { readFile as readFile3 } from "fs/promises";
|
|
12259
13051
|
import { createRequire } from "module";
|
|
12260
13052
|
import { join as join3 } from "path";
|
|
@@ -12295,7 +13087,7 @@ async function readAdminModule(cms, index) {
|
|
|
12295
13087
|
if (!file) return void 0;
|
|
12296
13088
|
const body = await readFile3(file, "utf8").catch(() => void 0);
|
|
12297
13089
|
if (body === void 0) return void 0;
|
|
12298
|
-
return { body, etag: `"${
|
|
13090
|
+
return { body, etag: `"${createHash7("sha1").update(body).digest("base64url")}"` };
|
|
12299
13091
|
}
|
|
12300
13092
|
|
|
12301
13093
|
// src/rest/handler.ts
|
|
@@ -12750,7 +13542,8 @@ async function adminSchema(cms, user, origin, context = {}) {
|
|
|
12750
13542
|
roles: admin && cms.roles.enabled,
|
|
12751
13543
|
sso: admin && cms.auth.sso.enabled,
|
|
12752
13544
|
audit: cms.audit.enabled && await cms.roles.canView(user, "audit"),
|
|
12753
|
-
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")
|
|
12754
13547
|
};
|
|
12755
13548
|
const roles = await cms.roles.options();
|
|
12756
13549
|
const adminCollections = await Promise.all(
|
|
@@ -12890,7 +13683,8 @@ var VIEW_GROUPS = [
|
|
|
12890
13683
|
["email", "settings.system"],
|
|
12891
13684
|
["deliveries", "settings.system"],
|
|
12892
13685
|
["jobs", "settings.system"],
|
|
12893
|
-
["audit", "settings.system"]
|
|
13686
|
+
["audit", "settings.system"],
|
|
13687
|
+
["integrations", "settings.integrations"]
|
|
12894
13688
|
];
|
|
12895
13689
|
function buildNav(cms, collections, globals, pageRefs, views) {
|
|
12896
13690
|
const groups = navGroups(cms.config.admin.nav);
|
|
@@ -13123,6 +13917,15 @@ async function adminStatus(cms) {
|
|
|
13123
13917
|
if (failed.length > 0) attention.push({ id: "jobs", count: failed.length, names: failed });
|
|
13124
13918
|
const paused = await pausedJobs(cms);
|
|
13125
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
|
+
}
|
|
13126
13929
|
const backups = await backupAttention(cms);
|
|
13127
13930
|
if (backups) attention.push(backups);
|
|
13128
13931
|
attention.push(...await cms.audit.attention());
|
|
@@ -14294,6 +15097,91 @@ async function route(cms, ctx, method, path, options) {
|
|
|
14294
15097
|
}
|
|
14295
15098
|
throw methodNotAllowed(ctx, "PATCH, DELETE");
|
|
14296
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
|
+
}
|
|
14297
15185
|
if (first === "admin" && second === "deliveries") {
|
|
14298
15186
|
if (!ctx.user) throw new UnauthorizedError();
|
|
14299
15187
|
if (!await cms.roles.canView(ctx.user, "deliveries")) throw new ForbiddenError();
|