@easy-cms/core 0.45.0 → 0.47.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-C4KJ5TFK.js +48 -0
- package/dist/conditions.d.ts +31 -0
- package/dist/conditions.js +8 -0
- package/dist/index.d.ts +441 -28
- package/dist/index.js +905 -74
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
conditionIssues,
|
|
3
|
+
matchesCondition
|
|
4
|
+
} from "./chunk-C4KJ5TFK.js";
|
|
5
|
+
|
|
1
6
|
// src/access.ts
|
|
2
7
|
var isSystemAdmin = (user) => user?.role === "admin" && !user.apiKey && user.scoped !== true;
|
|
3
8
|
var anyone = () => true;
|
|
4
|
-
var isLoggedIn = ({ user }) => user !== null;
|
|
9
|
+
var isLoggedIn = ({ user }) => user !== null && user.member !== true;
|
|
10
|
+
var isSignedIn = ({ user }) => user !== null;
|
|
5
11
|
var isAdmin = ({ user }) => user?.role === "admin";
|
|
6
12
|
|
|
7
13
|
// src/api-keys.ts
|
|
@@ -192,7 +198,12 @@ var adminOrSelf = ({ user }) => {
|
|
|
192
198
|
var adminOnly = {
|
|
193
199
|
update: ({ user }) => isSystemAdmin(user)
|
|
194
200
|
};
|
|
195
|
-
|
|
201
|
+
var staffOrSelf = ({ user }) => {
|
|
202
|
+
if (!user) return false;
|
|
203
|
+
if (user.member !== true) return true;
|
|
204
|
+
return { id: { equals: user.id } };
|
|
205
|
+
};
|
|
206
|
+
function userFields(roles, rbac, members) {
|
|
196
207
|
const defaultRole = roles.includes("editor") ? "editor" : roles[roles.length - 1];
|
|
197
208
|
return [
|
|
198
209
|
{
|
|
@@ -227,6 +238,16 @@ function userFields(roles, rbac) {
|
|
|
227
238
|
access: adminOnly,
|
|
228
239
|
label: { en: "Active", th: "\u0E43\u0E0A\u0E49\u0E07\u0E32\u0E19" }
|
|
229
240
|
},
|
|
241
|
+
// Accounts made by signing up (`auth.members.signup`) are `false` until the email is confirmed.
|
|
242
|
+
...members ? [
|
|
243
|
+
{
|
|
244
|
+
name: "emailVerified",
|
|
245
|
+
type: "boolean",
|
|
246
|
+
access: adminOnly,
|
|
247
|
+
position: "sidebar",
|
|
248
|
+
label: { en: "Email confirmed", th: "\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E41\u0E25\u0E49\u0E27" }
|
|
249
|
+
}
|
|
250
|
+
] : [],
|
|
230
251
|
{ name: "passwordHash", type: "text", hidden: true }
|
|
231
252
|
];
|
|
232
253
|
}
|
|
@@ -239,9 +260,12 @@ function withUsers(config) {
|
|
|
239
260
|
useAsTitle: "email",
|
|
240
261
|
icon: "users",
|
|
241
262
|
...custom,
|
|
242
|
-
fields: [
|
|
263
|
+
fields: [
|
|
264
|
+
...userFields(roles, config.auth?.rbac === true, config.auth?.members?.signup !== void 0),
|
|
265
|
+
...custom?.fields ?? []
|
|
266
|
+
],
|
|
243
267
|
access: {
|
|
244
|
-
read:
|
|
268
|
+
read: staffOrSelf,
|
|
245
269
|
create: ({ user }) => isSystemAdmin(user),
|
|
246
270
|
update: adminOrSelf,
|
|
247
271
|
delete: ({ user }) => isSystemAdmin(user),
|
|
@@ -1273,6 +1297,48 @@ async function filterInput(fields, data, update2) {
|
|
|
1273
1297
|
return result;
|
|
1274
1298
|
}
|
|
1275
1299
|
|
|
1300
|
+
// src/spam.ts
|
|
1301
|
+
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
1302
|
+
var MAX_AGE = 24 * 36e5;
|
|
1303
|
+
var sign = (secret, value) => createHmac2("sha256", secret).update(value).digest("base64url");
|
|
1304
|
+
function formToken(secret, purpose, now = Date.now()) {
|
|
1305
|
+
return `${now}.${sign(secret, `${purpose}:${now}`)}`;
|
|
1306
|
+
}
|
|
1307
|
+
function checkFormToken(secret, purpose, token, minTime, now = Date.now()) {
|
|
1308
|
+
if (typeof token !== "string") return "invalid";
|
|
1309
|
+
const [time, signature] = token.split(".");
|
|
1310
|
+
const issued = Number(time);
|
|
1311
|
+
if (!Number.isFinite(issued) || !signature) return "invalid";
|
|
1312
|
+
const expected = Buffer.from(sign(secret, `${purpose}:${issued}`));
|
|
1313
|
+
const given = Buffer.from(signature);
|
|
1314
|
+
if (expected.length !== given.length || !timingSafeEqual2(expected, given)) return "invalid";
|
|
1315
|
+
if (now - issued < minTime) return "too-fast";
|
|
1316
|
+
if (now - issued > MAX_AGE) return "expired";
|
|
1317
|
+
return "ok";
|
|
1318
|
+
}
|
|
1319
|
+
function rateKeys(secret, subject, window, now = Date.now()) {
|
|
1320
|
+
const bucket = Math.floor(now / (window * 1e3));
|
|
1321
|
+
const key2 = (b) => sign(secret, `${subject}:${b}`).slice(0, 32);
|
|
1322
|
+
return { current: key2(bucket), previous: key2(bucket - 1) };
|
|
1323
|
+
}
|
|
1324
|
+
function honeypotName(names2) {
|
|
1325
|
+
return ["website", "homepage", "url_address"].find((n) => !names2.includes(n)) ?? "hp_field";
|
|
1326
|
+
}
|
|
1327
|
+
async function verifyTurnstile(secretKey, response, ip, fetcher = fetch) {
|
|
1328
|
+
if (typeof response !== "string" || !response) return false;
|
|
1329
|
+
const body = new URLSearchParams({ secret: secretKey, response, ...ip ? { remoteip: ip } : {} });
|
|
1330
|
+
try {
|
|
1331
|
+
const result = await fetcher("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
|
|
1332
|
+
method: "POST",
|
|
1333
|
+
body,
|
|
1334
|
+
signal: AbortSignal.timeout(1e4)
|
|
1335
|
+
});
|
|
1336
|
+
return (await result.json()).success === true;
|
|
1337
|
+
} catch {
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1276
1342
|
// src/auth/cookie.ts
|
|
1277
1343
|
var SESSION_COOKIE = "ecms-session";
|
|
1278
1344
|
|
|
@@ -1289,6 +1355,21 @@ function email(subject, lines, button) {
|
|
|
1289
1355
|
var hours = (expiresAt) => Math.max(1, Math.round(((expiresAt?.getTime() ?? Date.now()) - Date.now()) / 36e5));
|
|
1290
1356
|
var days = (expiresAt) => Math.max(1, Math.round(hours(expiresAt) / 24));
|
|
1291
1357
|
var DEFAULT_PASSWORD_EMAILS = {
|
|
1358
|
+
verifyEmail: ({ url = "", locale, expiresAt }) => locale === "th" ? email(
|
|
1359
|
+
"\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13",
|
|
1360
|
+
[
|
|
1361
|
+
"\u0E02\u0E2D\u0E1A\u0E04\u0E38\u0E13\u0E17\u0E35\u0E48\u0E2A\u0E21\u0E31\u0E04\u0E23\u0E2A\u0E21\u0E32\u0E0A\u0E34\u0E01 \u0E01\u0E14\u0E1B\u0E38\u0E48\u0E21\u0E14\u0E49\u0E32\u0E19\u0E25\u0E48\u0E32\u0E07\u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19\u0E2D\u0E35\u0E40\u0E21\u0E25\u0E41\u0E25\u0E30\u0E40\u0E02\u0E49\u0E32\u0E2A\u0E39\u0E48\u0E23\u0E30\u0E1A\u0E1A",
|
|
1362
|
+
`\u0E25\u0E34\u0E07\u0E01\u0E4C\u0E19\u0E35\u0E49\u0E43\u0E0A\u0E49\u0E44\u0E14\u0E49\u0E20\u0E32\u0E22\u0E43\u0E19 ${days(expiresAt)} \u0E27\u0E31\u0E19 \u0E16\u0E49\u0E32\u0E04\u0E38\u0E13\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49\u0E2A\u0E21\u0E31\u0E04\u0E23 \u0E44\u0E21\u0E48\u0E15\u0E49\u0E2D\u0E07\u0E17\u0E33\u0E2D\u0E30\u0E44\u0E23`
|
|
1363
|
+
],
|
|
1364
|
+
{ label: "\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19\u0E2D\u0E35\u0E40\u0E21\u0E25", url }
|
|
1365
|
+
) : email(
|
|
1366
|
+
"Confirm your email",
|
|
1367
|
+
[
|
|
1368
|
+
"Thanks for signing up. Confirm your email to sign in.",
|
|
1369
|
+
`The link works within ${days(expiresAt)} day(s). If you didn't sign up, ignore this email.`
|
|
1370
|
+
],
|
|
1371
|
+
{ label: "Confirm email", url }
|
|
1372
|
+
),
|
|
1292
1373
|
resetPassword: ({ url = "", locale, expiresAt }) => locale === "th" ? email(
|
|
1293
1374
|
"\u0E15\u0E31\u0E49\u0E07\u0E23\u0E2B\u0E31\u0E2A\u0E1C\u0E48\u0E32\u0E19\u0E43\u0E2B\u0E21\u0E48",
|
|
1294
1375
|
[
|
|
@@ -1339,7 +1420,7 @@ var DEFAULT_PASSWORD_EMAILS = {
|
|
|
1339
1420
|
};
|
|
1340
1421
|
|
|
1341
1422
|
// src/auth/password.ts
|
|
1342
|
-
import { randomBytes as randomBytes2, scrypt, timingSafeEqual as
|
|
1423
|
+
import { randomBytes as randomBytes2, scrypt, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
1343
1424
|
var MIN_PASSWORD_LENGTH = 8;
|
|
1344
1425
|
var N = 2 ** 17;
|
|
1345
1426
|
var R = 8;
|
|
@@ -1374,7 +1455,7 @@ async function verifyPassword(password, stored) {
|
|
|
1374
1455
|
Number(p),
|
|
1375
1456
|
expected.length
|
|
1376
1457
|
);
|
|
1377
|
-
return key2.length === expected.length &&
|
|
1458
|
+
return key2.length === expected.length && timingSafeEqual3(key2, expected);
|
|
1378
1459
|
}
|
|
1379
1460
|
var dummyHash;
|
|
1380
1461
|
async function fakeVerify(password) {
|
|
@@ -1384,10 +1465,10 @@ async function fakeVerify(password) {
|
|
|
1384
1465
|
}
|
|
1385
1466
|
|
|
1386
1467
|
// src/auth/sso.ts
|
|
1387
|
-
import { createHmac as
|
|
1468
|
+
import { createHmac as createHmac4, randomBytes as randomBytes4 } from "crypto";
|
|
1388
1469
|
|
|
1389
1470
|
// src/auth/tokens.ts
|
|
1390
|
-
import { createHash as createHash2, createHmac as
|
|
1471
|
+
import { createHash as createHash2, createHmac as createHmac3, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual4 } from "crypto";
|
|
1391
1472
|
function newToken() {
|
|
1392
1473
|
return randomBytes3(32).toString("base64url");
|
|
1393
1474
|
}
|
|
@@ -1395,7 +1476,7 @@ function hashToken(token) {
|
|
|
1395
1476
|
return createHash2("sha256").update(token).digest("hex");
|
|
1396
1477
|
}
|
|
1397
1478
|
function hmac(secret, value) {
|
|
1398
|
-
return
|
|
1479
|
+
return createHmac3("sha256", secret).update(value).digest("base64url");
|
|
1399
1480
|
}
|
|
1400
1481
|
function signToken(secret, token) {
|
|
1401
1482
|
return `${token}.${hmac(secret, `session:${token}`)}`;
|
|
@@ -1412,7 +1493,7 @@ function csrfToken(secret, token) {
|
|
|
1412
1493
|
function safeEqual(a, b) {
|
|
1413
1494
|
const left = Buffer.from(a);
|
|
1414
1495
|
const right = Buffer.from(b);
|
|
1415
|
-
return left.length === right.length &&
|
|
1496
|
+
return left.length === right.length && timingSafeEqual4(left, right);
|
|
1416
1497
|
}
|
|
1417
1498
|
function signPreviewToken(secret, target, expiresAt) {
|
|
1418
1499
|
const payload = Buffer.from(
|
|
@@ -1454,7 +1535,7 @@ function readPasswordToken(token) {
|
|
|
1454
1535
|
try {
|
|
1455
1536
|
const data = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
1456
1537
|
if (typeof data.u !== "string" || typeof data.e !== "number") return null;
|
|
1457
|
-
if (data.p !== "reset" && data.p !== "invite") return null;
|
|
1538
|
+
if (data.p !== "reset" && data.p !== "invite" && data.p !== "verify") return null;
|
|
1458
1539
|
return { userId: data.u, purpose: data.p, expiresAt: data.e, payload };
|
|
1459
1540
|
} catch {
|
|
1460
1541
|
return null;
|
|
@@ -1471,19 +1552,19 @@ function passwordTokenMatches(secret, token, fingerprint) {
|
|
|
1471
1552
|
var SSO_COOKIE = "ecms-sso";
|
|
1472
1553
|
var PENDING_TTL = 10 * 6e4;
|
|
1473
1554
|
var random = () => randomBytes4(32).toString("base64url");
|
|
1474
|
-
function
|
|
1475
|
-
return
|
|
1555
|
+
function sign2(secret, payload) {
|
|
1556
|
+
return createHmac4("sha256", secret).update(`sso:${payload}`).digest("base64url");
|
|
1476
1557
|
}
|
|
1477
1558
|
function seal(secret, pending) {
|
|
1478
1559
|
const payload = Buffer.from(JSON.stringify(pending)).toString("base64url");
|
|
1479
|
-
return `${payload}.${
|
|
1560
|
+
return `${payload}.${sign2(secret, payload)}`;
|
|
1480
1561
|
}
|
|
1481
1562
|
function unseal(secret, value) {
|
|
1482
1563
|
if (!value) return null;
|
|
1483
1564
|
const dot = value.lastIndexOf(".");
|
|
1484
1565
|
if (dot <= 0) return null;
|
|
1485
1566
|
const payload = value.slice(0, dot);
|
|
1486
|
-
if (!safeEqual(value.slice(dot + 1),
|
|
1567
|
+
if (!safeEqual(value.slice(dot + 1), sign2(secret, payload))) return null;
|
|
1487
1568
|
try {
|
|
1488
1569
|
const pending = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
1489
1570
|
return typeof pending.e === "number" && pending.e > Date.now() ? pending : null;
|
|
@@ -1761,6 +1842,16 @@ var SingleSignOn = class {
|
|
|
1761
1842
|
|
|
1762
1843
|
// src/auth/auth.ts
|
|
1763
1844
|
var INVALID = "Invalid email or password";
|
|
1845
|
+
var SIGNUP_FORM = "easy-cms-signup";
|
|
1846
|
+
var SIGNUP_MIN_TIME = 2e3;
|
|
1847
|
+
var SIGNUP_HONEYPOT = "website";
|
|
1848
|
+
function asMember(user, memberRoles) {
|
|
1849
|
+
const member = memberRoles.includes(String(user.role));
|
|
1850
|
+
if (member === (user.member === true)) return user;
|
|
1851
|
+
if (member) return { ...user, member: true };
|
|
1852
|
+
const { member: _member, ...rest } = user;
|
|
1853
|
+
return rest;
|
|
1854
|
+
}
|
|
1764
1855
|
var RESET_REQUESTS = 3;
|
|
1765
1856
|
var Auth = class {
|
|
1766
1857
|
constructor(cms) {
|
|
@@ -1810,6 +1901,16 @@ var Auth = class {
|
|
|
1810
1901
|
throw new UnauthorizedError(INVALID);
|
|
1811
1902
|
}
|
|
1812
1903
|
await this.clearFailures(key2);
|
|
1904
|
+
if (user.emailVerified === false) {
|
|
1905
|
+
await this.cms.audit.record({
|
|
1906
|
+
action: "login.failed",
|
|
1907
|
+
target: "auth",
|
|
1908
|
+
user: null,
|
|
1909
|
+
email: email2,
|
|
1910
|
+
detail: { reason: "email not confirmed" }
|
|
1911
|
+
});
|
|
1912
|
+
throw new UnauthorizedError("Confirm your email first: follow the link we sent you");
|
|
1913
|
+
}
|
|
1813
1914
|
if (!this.sso.passwordAllowed(user)) {
|
|
1814
1915
|
await this.cms.audit.record({
|
|
1815
1916
|
action: "login.failed",
|
|
@@ -1852,6 +1953,136 @@ var Auth = class {
|
|
|
1852
1953
|
const raw = await this.db.findById({ collection: USERS, id: user.id });
|
|
1853
1954
|
return this.startSession(raw);
|
|
1854
1955
|
}
|
|
1956
|
+
// --- Members signing up (`auth.members.signup`) ---------------------------------------------
|
|
1957
|
+
/** What a sign-up form needs: a token proving when it was loaded, and Turnstile's site key. */
|
|
1958
|
+
signupForm() {
|
|
1959
|
+
const signup = this.config.auth.members.signup;
|
|
1960
|
+
if (!signup) throw new NotFoundError(USERS, "signup");
|
|
1961
|
+
return {
|
|
1962
|
+
token: formToken(this.config.secret, SIGNUP_FORM),
|
|
1963
|
+
turnstile: signup.turnstile?.siteKey ?? null,
|
|
1964
|
+
verifyEmail: signup.verifyEmail !== false
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Creates a member's account (`auth.members.signup`). With `verifyEmail` (the default), emails
|
|
1969
|
+
* a link to confirm the address and answers the same whether or not the email has an account;
|
|
1970
|
+
* otherwise starts a session. Throws `ValidationError` for spam checks and bad input.
|
|
1971
|
+
*/
|
|
1972
|
+
async signup(args) {
|
|
1973
|
+
const signup = this.config.auth.members.signup;
|
|
1974
|
+
if (!signup) throw new NotFoundError(USERS, "signup");
|
|
1975
|
+
const key2 = `signup|${args.ip ?? ""}`;
|
|
1976
|
+
await this.checkRateLimit(key2);
|
|
1977
|
+
const refuse = (field, message2) => new ValidationError(USERS, [{ field, message: message2 }]);
|
|
1978
|
+
if (typeof args.honeypot === "string" && args.honeypot !== "") {
|
|
1979
|
+
await this.recordFailure(key2);
|
|
1980
|
+
throw refuse("form", "could not be sent");
|
|
1981
|
+
}
|
|
1982
|
+
const checked = checkFormToken(this.config.secret, SIGNUP_FORM, args.token, SIGNUP_MIN_TIME);
|
|
1983
|
+
if (checked !== "ok")
|
|
1984
|
+
throw refuse(
|
|
1985
|
+
"form",
|
|
1986
|
+
checked === "expired" ? "was open too long: reload the page" : "could not be sent"
|
|
1987
|
+
);
|
|
1988
|
+
if (signup.turnstile && !await verifyTurnstile(signup.turnstile.secretKey, args.turnstile, args.ip))
|
|
1989
|
+
throw refuse("turnstile", "is not complete");
|
|
1990
|
+
await this.recordFailure(key2);
|
|
1991
|
+
const email2 = typeof args.email === "string" ? args.email.trim().toLowerCase() : "";
|
|
1992
|
+
const verify = signup.verifyEmail !== false;
|
|
1993
|
+
if (verify && !this.canSendPasswordLinks(args.origin))
|
|
1994
|
+
throw new QueryError("Signing up needs `email` in the config, and a site URL in production");
|
|
1995
|
+
const existing = email2 ? await this.findByEmail(email2) : void 0;
|
|
1996
|
+
if (existing && verify) {
|
|
1997
|
+
if (existing.emailVerified === false && existing.active !== false)
|
|
1998
|
+
await this.mailVerifyLink(existing, args);
|
|
1999
|
+
return { verify: true };
|
|
2000
|
+
}
|
|
2001
|
+
const created = await this.cms.create(
|
|
2002
|
+
USERS,
|
|
2003
|
+
{
|
|
2004
|
+
email: email2,
|
|
2005
|
+
password: args.password,
|
|
2006
|
+
role: signup.role,
|
|
2007
|
+
active: true,
|
|
2008
|
+
emailVerified: !verify,
|
|
2009
|
+
...typeof args.name === "string" && args.name.trim() ? { name: args.name.trim() } : {}
|
|
2010
|
+
},
|
|
2011
|
+
{ context: args.context ?? {} }
|
|
2012
|
+
);
|
|
2013
|
+
const user = await this.db.findById({ collection: USERS, id: created.id });
|
|
2014
|
+
await this.cms.audit.record({
|
|
2015
|
+
action: "signup",
|
|
2016
|
+
target: "auth",
|
|
2017
|
+
user: await this.toAuthUser(user)
|
|
2018
|
+
});
|
|
2019
|
+
if (verify) {
|
|
2020
|
+
await this.mailVerifyLink(user, args);
|
|
2021
|
+
return { verify: true };
|
|
2022
|
+
}
|
|
2023
|
+
return { ...await this.startSession(user), verify: false };
|
|
2024
|
+
}
|
|
2025
|
+
/** Confirms a member's email from the link, and signs them in. */
|
|
2026
|
+
async verifyEmail(args) {
|
|
2027
|
+
const key2 = `verify-token|${args.ip ?? ""}`;
|
|
2028
|
+
await this.checkRateLimit(key2);
|
|
2029
|
+
const token = String(args.token ?? "");
|
|
2030
|
+
const claims = readPasswordToken(token);
|
|
2031
|
+
const id = claims && (/^\d+$/.test(claims.userId) ? Number(claims.userId) : claims.userId);
|
|
2032
|
+
const user = claims && claims.purpose === "verify" && claims.expiresAt > Date.now() && id !== null ? await this.db.findById({ collection: USERS, id }) : null;
|
|
2033
|
+
if (!user || user.active === false || !passwordTokenMatches(this.config.secret, token, passwordFingerprint(user.passwordHash))) {
|
|
2034
|
+
await this.recordFailure(key2);
|
|
2035
|
+
throw new ValidationError(USERS, [
|
|
2036
|
+
{ field: "token", message: "This link has expired or is not valid" }
|
|
2037
|
+
]);
|
|
2038
|
+
}
|
|
2039
|
+
await this.clearFailures(key2);
|
|
2040
|
+
if (user.emailVerified === false) {
|
|
2041
|
+
const { id: _id, ...rest } = user;
|
|
2042
|
+
await this.db.update({
|
|
2043
|
+
collection: USERS,
|
|
2044
|
+
id: user.id,
|
|
2045
|
+
data: { ...rest, emailVerified: true }
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
const session = await this.startSession(
|
|
2049
|
+
await this.db.findById({ collection: USERS, id: user.id })
|
|
2050
|
+
);
|
|
2051
|
+
await this.cms.audit.record({ action: "email.verified", target: "auth", user: session.user });
|
|
2052
|
+
return session;
|
|
2053
|
+
}
|
|
2054
|
+
async mailVerifyLink(user, options) {
|
|
2055
|
+
const expiresAt = Date.now() + this.config.auth.inviteExpiration * 1e3;
|
|
2056
|
+
const token = signPasswordToken(this.config.secret, {
|
|
2057
|
+
userId: String(user.id),
|
|
2058
|
+
purpose: "verify",
|
|
2059
|
+
expiresAt,
|
|
2060
|
+
fingerprint: passwordFingerprint(user.passwordHash)
|
|
2061
|
+
});
|
|
2062
|
+
const url = this.memberLink("verifyEmail", token, options.origin);
|
|
2063
|
+
const write = this.config.auth.members.emails?.verifyEmail ?? DEFAULT_PASSWORD_EMAILS.verifyEmail;
|
|
2064
|
+
const content = await write({
|
|
2065
|
+
user: await this.toAuthUser(user),
|
|
2066
|
+
url,
|
|
2067
|
+
locale: this.localeOf(options.locale),
|
|
2068
|
+
expiresAt: new Date(expiresAt)
|
|
2069
|
+
});
|
|
2070
|
+
await this.cms.sendEmail({ to: String(user.email), ...content });
|
|
2071
|
+
}
|
|
2072
|
+
/**
|
|
2073
|
+
* A link to one of the site's pages for members (`auth.members.pages`), else to the admin's
|
|
2074
|
+
* page of the same purpose.
|
|
2075
|
+
*/
|
|
2076
|
+
memberLink(page, token, origin) {
|
|
2077
|
+
const target = this.config.auth.members.pages?.[page];
|
|
2078
|
+
const query = `token=${encodeURIComponent(token)}`;
|
|
2079
|
+
if (target && /^https?:\/\//.test(target))
|
|
2080
|
+
return `${target}${target.includes("?") ? "&" : "?"}${query}`;
|
|
2081
|
+
const site = (this.config.admin.siteUrl || "").replace(/\/+$/, "");
|
|
2082
|
+
const base = site || this.passwordLinkBase(origin);
|
|
2083
|
+
const path = target ?? `${this.config.admin.path}/${page === "verifyEmail" ? "verify-email" : "reset-password"}`;
|
|
2084
|
+
return `${base}${path.startsWith("/") ? "" : "/"}${path}${path.includes("?") ? "&" : "?"}${query}`;
|
|
2085
|
+
}
|
|
1855
2086
|
async hasUsers() {
|
|
1856
2087
|
return await this.db.count({ collection: USERS }) > 0;
|
|
1857
2088
|
}
|
|
@@ -2024,7 +2255,7 @@ var Auth = class {
|
|
|
2024
2255
|
}
|
|
2025
2256
|
async passwordTokenUser(token) {
|
|
2026
2257
|
const claims = readPasswordToken(token);
|
|
2027
|
-
if (!claims || claims.expiresAt <= Date.now()) return null;
|
|
2258
|
+
if (!claims || claims.purpose === "verify" || claims.expiresAt <= Date.now()) return null;
|
|
2028
2259
|
const id = /^\d+$/.test(claims.userId) ? Number(claims.userId) : claims.userId;
|
|
2029
2260
|
const user = await this.db.findById({ collection: USERS, id });
|
|
2030
2261
|
if (!user || user.active === false || !this.sso.passwordAllowed(user)) return null;
|
|
@@ -2043,7 +2274,8 @@ var Auth = class {
|
|
|
2043
2274
|
fingerprint: passwordFingerprint(user.passwordHash)
|
|
2044
2275
|
});
|
|
2045
2276
|
const sso = this.sso.passwordAllowed(user) ? void 0 : this.config.auth.providers.map((p) => p.name);
|
|
2046
|
-
const
|
|
2277
|
+
const member = this.config.auth.members.roles.includes(String(user.role));
|
|
2278
|
+
const url = sso ? `${base}${this.config.admin.path}/login` : member ? this.memberLink("resetPassword", token, options.origin) : `${base}${this.config.admin.path}/reset-password?token=${encodeURIComponent(token)}`;
|
|
2047
2279
|
const write = purpose === "invite" ? this.config.auth.emails.invite ?? DEFAULT_PASSWORD_EMAILS.invite : this.config.auth.emails.resetPassword ?? DEFAULT_PASSWORD_EMAILS.resetPassword;
|
|
2048
2280
|
const content = await write({
|
|
2049
2281
|
user: await this.toAuthUser(user),
|
|
@@ -2136,7 +2368,8 @@ var Auth = class {
|
|
|
2136
2368
|
}
|
|
2137
2369
|
async toAuthUser(user) {
|
|
2138
2370
|
const fields = this.cms.collection(USERS).fields;
|
|
2139
|
-
|
|
2371
|
+
const stripped = await stripFields(fields, user);
|
|
2372
|
+
return asMember(stripped, this.config.auth.members.roles);
|
|
2140
2373
|
}
|
|
2141
2374
|
async deleteExpiredSessions(userId) {
|
|
2142
2375
|
const expired = await this.db.find({
|
|
@@ -2775,6 +3008,9 @@ async function validateFields(fields, input, options, prefix = "") {
|
|
|
2775
3008
|
const path = prefix + field.name;
|
|
2776
3009
|
const raw = input[field.name];
|
|
2777
3010
|
const fail = (message2) => errors.push({ field: path, message: message2 });
|
|
3011
|
+
const condition = field.admin?.condition;
|
|
3012
|
+
const skipRequired = options.skipRequired === true || condition !== void 0 && !matchesCondition(condition, input);
|
|
3013
|
+
const fieldOptions = skipRequired === (options.skipRequired === true) ? options : { ...options, skipRequired };
|
|
2778
3014
|
if (field.type === "group") {
|
|
2779
3015
|
if (!isEmpty(raw) && !isPlainObject(raw)) {
|
|
2780
3016
|
fail("must be an object");
|
|
@@ -2783,7 +3019,7 @@ async function validateFields(fields, input, options, prefix = "") {
|
|
|
2783
3019
|
const sub = await validateFields(
|
|
2784
3020
|
field.fields,
|
|
2785
3021
|
isPlainObject(raw) ? raw : {},
|
|
2786
|
-
|
|
3022
|
+
fieldOptions,
|
|
2787
3023
|
`${path}.`
|
|
2788
3024
|
);
|
|
2789
3025
|
data[field.name] = sub.data;
|
|
@@ -2806,7 +3042,7 @@ async function validateFields(fields, input, options, prefix = "") {
|
|
|
2806
3042
|
const failLocale = (message2) => errors.push({ field: localePath, message: message2 });
|
|
2807
3043
|
const value2 = map[locale];
|
|
2808
3044
|
if (isEmpty(value2) || Array.isArray(value2) && value2.length === 0) {
|
|
2809
|
-
if (own && field.required && !
|
|
3045
|
+
if (own && field.required && !skipRequired) failLocale("is required");
|
|
2810
3046
|
out[locale] = hasRows(field) || isHasMany(field) ? [] : null;
|
|
2811
3047
|
if (own && !field.required) await runCustom(field, null, options, failLocale);
|
|
2812
3048
|
continue;
|
|
@@ -2815,7 +3051,7 @@ async function validateFields(fields, input, options, prefix = "") {
|
|
|
2815
3051
|
field,
|
|
2816
3052
|
value2,
|
|
2817
3053
|
localePath,
|
|
2818
|
-
|
|
3054
|
+
fieldOptions,
|
|
2819
3055
|
failLocale,
|
|
2820
3056
|
errors,
|
|
2821
3057
|
references2
|
|
@@ -2828,14 +3064,14 @@ async function validateFields(fields, input, options, prefix = "") {
|
|
|
2828
3064
|
continue;
|
|
2829
3065
|
}
|
|
2830
3066
|
if (isEmpty(raw) || Array.isArray(raw) && raw.length === 0) {
|
|
2831
|
-
if (field.required && !
|
|
2832
|
-
else if (!
|
|
3067
|
+
if (field.required && !skipRequired) fail("is required");
|
|
3068
|
+
else if (!skipRequired && "minRows" in field && field.minRows)
|
|
2833
3069
|
fail(`must have at least ${itemsOf(field, field.minRows)}`);
|
|
2834
3070
|
data[field.name] = hasRows(field) || isHasMany(field) ? [] : null;
|
|
2835
3071
|
if (!field.required) await runCustom(field, null, options, fail);
|
|
2836
3072
|
continue;
|
|
2837
3073
|
}
|
|
2838
|
-
const value = await normalizeValue(field, raw, path,
|
|
3074
|
+
const value = await normalizeValue(field, raw, path, fieldOptions, fail, errors, references2);
|
|
2839
3075
|
if (value === void 0) continue;
|
|
2840
3076
|
data[field.name] = value;
|
|
2841
3077
|
await runCustom(field, value, options, fail);
|
|
@@ -3245,7 +3481,7 @@ function applyFieldTypes(config) {
|
|
|
3245
3481
|
}
|
|
3246
3482
|
|
|
3247
3483
|
// src/local-api.ts
|
|
3248
|
-
import { createHmac as
|
|
3484
|
+
import { createHmac as createHmac6, randomBytes as randomBytes5, timingSafeEqual as timingSafeEqual5 } from "crypto";
|
|
3249
3485
|
|
|
3250
3486
|
// src/localization.ts
|
|
3251
3487
|
var ALL_LOCALES = "all";
|
|
@@ -4609,8 +4845,72 @@ function rank(role, config) {
|
|
|
4609
4845
|
return index === -1 ? config.auth.roles.length : index;
|
|
4610
4846
|
}
|
|
4611
4847
|
|
|
4848
|
+
// src/nav.ts
|
|
4849
|
+
var BUILTIN_NAV = [
|
|
4850
|
+
{ id: "content", label: { en: "Content", th: "\u0E40\u0E19\u0E37\u0E49\u0E2D\u0E2B\u0E32" }, icon: "file-text", order: 0 },
|
|
4851
|
+
{
|
|
4852
|
+
id: "settings",
|
|
4853
|
+
label: { en: "Settings", th: "\u0E15\u0E31\u0E49\u0E07\u0E04\u0E48\u0E32" },
|
|
4854
|
+
icon: "settings",
|
|
4855
|
+
order: 1e3,
|
|
4856
|
+
children: [
|
|
4857
|
+
{ id: "site", label: { en: "Site", th: "\u0E40\u0E27\u0E47\u0E1A\u0E44\u0E0B\u0E15\u0E4C" }, icon: "globe" },
|
|
4858
|
+
{ id: "people", label: { en: "Users & access", th: "\u0E1C\u0E39\u0E49\u0E43\u0E0A\u0E49\u0E41\u0E25\u0E30\u0E2A\u0E34\u0E17\u0E18\u0E34\u0E4C" }, icon: "users" },
|
|
4859
|
+
{ id: "system", label: { en: "System", th: "\u0E23\u0E30\u0E1A\u0E1A" }, icon: "sliders-horizontal" }
|
|
4860
|
+
]
|
|
4861
|
+
}
|
|
4862
|
+
];
|
|
4863
|
+
var MEDIA_ORDER = 10;
|
|
4864
|
+
var LABEL_ORDER = 500;
|
|
4865
|
+
var NAV_ID = /^[a-z][a-z0-9-]*$/;
|
|
4866
|
+
function mergeGroups(lists, parent = "") {
|
|
4867
|
+
const byId = /* @__PURE__ */ new Map();
|
|
4868
|
+
let index = 0;
|
|
4869
|
+
for (const list of lists)
|
|
4870
|
+
for (const group of list) {
|
|
4871
|
+
const found = byId.get(group.id);
|
|
4872
|
+
if (found) {
|
|
4873
|
+
found.group = { ...group, ...stripUndefined(found.group) };
|
|
4874
|
+
if (group.children) found.children.push([...group.children]);
|
|
4875
|
+
} else {
|
|
4876
|
+
byId.set(group.id, {
|
|
4877
|
+
group,
|
|
4878
|
+
children: group.children ? [[...group.children]] : [],
|
|
4879
|
+
index: index++
|
|
4880
|
+
});
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
return [...byId.values()].map(({ group, children, index: index2 }) => ({
|
|
4884
|
+
id: parent ? `${parent}.${group.id}` : group.id,
|
|
4885
|
+
label: group.label,
|
|
4886
|
+
...group.icon ? { icon: group.icon } : {},
|
|
4887
|
+
order: group.order ?? 100 + index2,
|
|
4888
|
+
children: mergeGroups(children, parent ? `${parent}.${group.id}` : group.id)
|
|
4889
|
+
})).sort((a, b) => a.order - b.order);
|
|
4890
|
+
}
|
|
4891
|
+
function stripUndefined(value) {
|
|
4892
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
4893
|
+
}
|
|
4894
|
+
function navGroups(declared) {
|
|
4895
|
+
return mergeGroups([declared, BUILTIN_NAV]);
|
|
4896
|
+
}
|
|
4897
|
+
function navIds(groups) {
|
|
4898
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4899
|
+
for (const g of groups) {
|
|
4900
|
+
ids.add(g.id);
|
|
4901
|
+
for (const c of g.children) ids.add(c.id);
|
|
4902
|
+
}
|
|
4903
|
+
return ids;
|
|
4904
|
+
}
|
|
4905
|
+
function groupOf(group, ids, fallback) {
|
|
4906
|
+
if (group === void 0 || group === null) return { id: fallback };
|
|
4907
|
+
if (group === "settings") return { id: "settings.site" };
|
|
4908
|
+
if (typeof group === "string" && ids.has(group)) return { id: group };
|
|
4909
|
+
return { id: `label:${JSON.stringify(group)}`, label: group };
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4612
4912
|
// src/webhooks.ts
|
|
4613
|
-
import { createHmac as
|
|
4913
|
+
import { createHmac as createHmac5, randomUUID as randomUUID2 } from "crypto";
|
|
4614
4914
|
var WEBHOOK_EVENTS = [
|
|
4615
4915
|
"create",
|
|
4616
4916
|
"update",
|
|
@@ -4754,7 +5054,7 @@ var Webhooks = class {
|
|
|
4754
5054
|
...hook.headers
|
|
4755
5055
|
};
|
|
4756
5056
|
if (hook.secret) {
|
|
4757
|
-
headers["x-easy-cms-signature"] = `sha256=${
|
|
5057
|
+
headers["x-easy-cms-signature"] = `sha256=${createHmac5("sha256", hook.secret).update(body).digest("hex")}`;
|
|
4758
5058
|
}
|
|
4759
5059
|
try {
|
|
4760
5060
|
const response = await this.fetcher(hook.url, {
|
|
@@ -4780,7 +5080,9 @@ var Webhooks = class {
|
|
|
4780
5080
|
}
|
|
4781
5081
|
};
|
|
4782
5082
|
var later2 = (from, ms) => new Date(from.getTime() + ms).toISOString();
|
|
5083
|
+
var isContentEvent = (event) => WEBHOOK_EVENTS.includes(event);
|
|
4783
5084
|
function matches(hook, payload) {
|
|
5085
|
+
if (!isContentEvent(payload.event)) return hook.events?.includes(payload.event) === true;
|
|
4784
5086
|
if (hook.events && !hook.events.includes(payload.event)) return false;
|
|
4785
5087
|
if (payload.collection !== void 0)
|
|
4786
5088
|
return !hook.collections || hook.collections.includes(payload.collection);
|
|
@@ -4857,13 +5159,17 @@ function validateConfig(config) {
|
|
|
4857
5159
|
});
|
|
4858
5160
|
validateEndpoints(config, collectionSlugs, add);
|
|
4859
5161
|
validateCommands(config, add);
|
|
5162
|
+
validateJobs(config, add);
|
|
5163
|
+
validateEvents(config, add);
|
|
4860
5164
|
const apiKeys = config.apiKeys;
|
|
4861
5165
|
if (apiKeys !== void 0 && typeof apiKeys !== "boolean") add("apiKeys", "must be true or false");
|
|
5166
|
+
const nav = validateNav(config, add);
|
|
4862
5167
|
collections.forEach((collection2, i) => {
|
|
4863
5168
|
const path = `collections.${collection2.slug ?? `[${i}]`}`;
|
|
4864
5169
|
validateContainer(collection2, path, collectionSlugs, add, {
|
|
4865
5170
|
collection: true,
|
|
4866
|
-
rbac: config.auth?.rbac === true
|
|
5171
|
+
rbac: config.auth?.rbac === true,
|
|
5172
|
+
nav
|
|
4867
5173
|
});
|
|
4868
5174
|
validateUseAsTitle(collection2, path, add);
|
|
4869
5175
|
validateIcon(collection2, path, add);
|
|
@@ -4875,7 +5181,8 @@ function validateConfig(config) {
|
|
|
4875
5181
|
const path = `globals.${global2.slug ?? `[${i}]`}`;
|
|
4876
5182
|
validateContainer(global2, path, collectionSlugs, add, {
|
|
4877
5183
|
collection: false,
|
|
4878
|
-
rbac: config.auth?.rbac === true
|
|
5184
|
+
rbac: config.auth?.rbac === true,
|
|
5185
|
+
nav
|
|
4879
5186
|
});
|
|
4880
5187
|
validateIcon(global2, path, add);
|
|
4881
5188
|
});
|
|
@@ -5008,8 +5315,8 @@ function validateAdminViews(config, add) {
|
|
|
5008
5315
|
"e.g. label: { en: 'Stats', th: '\u0E2A\u0E16\u0E34\u0E15\u0E34' }"
|
|
5009
5316
|
);
|
|
5010
5317
|
validateIcon(p, path, add);
|
|
5011
|
-
if (p.group !==
|
|
5012
|
-
|
|
5318
|
+
if (p.group !== "content" && p.group !== false)
|
|
5319
|
+
validateGroup(p.group, navIds(navGroups(config.admin?.nav ?? [])), `${path}.group`, add);
|
|
5013
5320
|
access(p.access, path);
|
|
5014
5321
|
});
|
|
5015
5322
|
}
|
|
@@ -5118,6 +5425,7 @@ function validateAuth(config, add) {
|
|
|
5118
5425
|
add("auth.roles", 'must include "admin"', "e.g. roles: ['admin', 'editor']");
|
|
5119
5426
|
}
|
|
5120
5427
|
}
|
|
5428
|
+
validateMembers(auth.members, auth.roles ?? ["admin", "editor"], add);
|
|
5121
5429
|
for (const key2 of [
|
|
5122
5430
|
"tokenExpiration",
|
|
5123
5431
|
"maxLoginAttempts",
|
|
@@ -5176,6 +5484,31 @@ var LOCALIZABLE = /* @__PURE__ */ new Set([
|
|
|
5176
5484
|
"blocks",
|
|
5177
5485
|
"array"
|
|
5178
5486
|
]);
|
|
5487
|
+
function validateMembers(members, roles, add) {
|
|
5488
|
+
if (members === void 0) return;
|
|
5489
|
+
const { roles: memberRoles, signup, pages: pages2 } = members ?? {};
|
|
5490
|
+
if (!Array.isArray(memberRoles) || memberRoles.some((r) => typeof r !== "string")) {
|
|
5491
|
+
add("auth.members.roles", "must be an array of roles", "e.g. roles: ['customer']");
|
|
5492
|
+
return;
|
|
5493
|
+
}
|
|
5494
|
+
for (const role of memberRoles) {
|
|
5495
|
+
if (role === "admin") add("auth.members.roles", `can't include "admin"`);
|
|
5496
|
+
else if (!roles.includes(role))
|
|
5497
|
+
add("auth.members.roles", `"${role}" must be one of auth.roles too`);
|
|
5498
|
+
}
|
|
5499
|
+
if (signup !== void 0) {
|
|
5500
|
+
const { role, verifyEmail: verifyEmail2, turnstile } = signup ?? {};
|
|
5501
|
+
if (typeof role !== "string" || !memberRoles.includes(role))
|
|
5502
|
+
add("auth.members.signup.role", "must be one of auth.members.roles");
|
|
5503
|
+
if (verifyEmail2 !== void 0 && typeof verifyEmail2 !== "boolean")
|
|
5504
|
+
add("auth.members.signup.verifyEmail", "must be true or false");
|
|
5505
|
+
if (turnstile !== void 0 && (typeof turnstile?.siteKey !== "string" || typeof turnstile?.secretKey !== "string"))
|
|
5506
|
+
add("auth.members.signup.turnstile", "needs siteKey and secretKey");
|
|
5507
|
+
}
|
|
5508
|
+
for (const [key2, value] of Object.entries(pages2 ?? {}))
|
|
5509
|
+
if (value !== void 0 && typeof value !== "string")
|
|
5510
|
+
add(`auth.members.pages.${key2}`, "must be a path or URL", "e.g. '/account/verify'");
|
|
5511
|
+
}
|
|
5179
5512
|
function validateLocalization(config, add) {
|
|
5180
5513
|
const localization = config.localization;
|
|
5181
5514
|
if (localization !== void 0 && localization !== null) {
|
|
@@ -5257,14 +5590,19 @@ function validateWebhooks(config, add) {
|
|
|
5257
5590
|
...(config.collections ?? []).map((c) => c?.slug)
|
|
5258
5591
|
]);
|
|
5259
5592
|
const globals = new Set((config.globals ?? []).map((g) => g?.slug));
|
|
5593
|
+
const custom = new Set(config.events ?? []);
|
|
5260
5594
|
for (const [i, hook] of hooks.entries()) {
|
|
5261
5595
|
const path = `webhooks[${i}]`;
|
|
5262
5596
|
if (typeof hook?.url !== "string" || !/^https?:\/\/\S+$/.test(hook.url)) {
|
|
5263
5597
|
add(`${path}.url`, "must be an http(s) URL");
|
|
5264
5598
|
}
|
|
5265
5599
|
for (const event of hook?.events ?? []) {
|
|
5266
|
-
if (!WEBHOOK_EVENTS.includes(event))
|
|
5267
|
-
add(
|
|
5600
|
+
if (!WEBHOOK_EVENTS.includes(event) && !custom.has(event))
|
|
5601
|
+
add(
|
|
5602
|
+
`${path}.events`,
|
|
5603
|
+
`unknown event "${event}"`,
|
|
5604
|
+
`use: ${[...WEBHOOK_EVENTS, ...custom].join(", ")}`
|
|
5605
|
+
);
|
|
5268
5606
|
}
|
|
5269
5607
|
for (const slug2 of hook?.collections ?? [])
|
|
5270
5608
|
if (!collections.has(slug2)) add(`${path}.collections`, `unknown collection "${slug2}"`);
|
|
@@ -5425,7 +5763,142 @@ function checkSlug(container, path, seen, add) {
|
|
|
5425
5763
|
seen.add(slug2);
|
|
5426
5764
|
return slug2;
|
|
5427
5765
|
}
|
|
5428
|
-
|
|
5766
|
+
var isLabel = (value) => typeof value === "string" && value.trim() !== "" || typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).length > 0 && Object.values(value).every((v) => typeof v === "string");
|
|
5767
|
+
function validateNav(config, add) {
|
|
5768
|
+
const nav = config.admin?.nav;
|
|
5769
|
+
if (nav !== void 0) {
|
|
5770
|
+
if (!Array.isArray(nav))
|
|
5771
|
+
add("admin.nav", "must be a list of groups", "e.g. nav: [{ id: 'shop', label: 'Shop' }]");
|
|
5772
|
+
else
|
|
5773
|
+
nav.forEach((group, i) => {
|
|
5774
|
+
const check2 = (g, path, child) => {
|
|
5775
|
+
const { id, label, children, order } = g ?? {};
|
|
5776
|
+
if (typeof id !== "string" || !NAV_ID.test(id))
|
|
5777
|
+
add(`${path}.id`, "must be lowercase letters, digits and -", "e.g. 'shop'");
|
|
5778
|
+
if (!isLabel(label)) add(`${path}.label`, "must be a string or { en, th }");
|
|
5779
|
+
if (order !== void 0 && typeof order !== "number")
|
|
5780
|
+
add(`${path}.order`, "must be a number");
|
|
5781
|
+
validateIcon(g, path, add);
|
|
5782
|
+
if (children !== void 0) {
|
|
5783
|
+
if (child) add(`${path}.children`, "groups go one level deep");
|
|
5784
|
+
else if (!Array.isArray(children)) add(`${path}.children`, "must be a list of groups");
|
|
5785
|
+
else
|
|
5786
|
+
children.forEach((c, j) => {
|
|
5787
|
+
check2(c, `${path}.children[${j}]`, true);
|
|
5788
|
+
});
|
|
5789
|
+
}
|
|
5790
|
+
};
|
|
5791
|
+
check2(group, `admin.nav[${i}]`, false);
|
|
5792
|
+
});
|
|
5793
|
+
}
|
|
5794
|
+
const commands = config.admin?.commands;
|
|
5795
|
+
if (commands !== void 0) {
|
|
5796
|
+
if (!Array.isArray(commands)) add("admin.commands", "must be a list");
|
|
5797
|
+
else
|
|
5798
|
+
commands.forEach((c, i) => {
|
|
5799
|
+
const { label, to } = c ?? {};
|
|
5800
|
+
if (!isLabel(label)) add(`admin.commands[${i}].label`, "must be a string or { en, th }");
|
|
5801
|
+
if (typeof to !== "string" || !to.startsWith("/") || to.startsWith("//"))
|
|
5802
|
+
add(
|
|
5803
|
+
`admin.commands[${i}].to`,
|
|
5804
|
+
"must be a path in the admin",
|
|
5805
|
+
"e.g. '/collections/orders'"
|
|
5806
|
+
);
|
|
5807
|
+
});
|
|
5808
|
+
}
|
|
5809
|
+
return navIds(navGroups(Array.isArray(nav) ? nav : []));
|
|
5810
|
+
}
|
|
5811
|
+
function validateGroup(group, ids, path, add) {
|
|
5812
|
+
if (group === void 0 || group === "settings") return;
|
|
5813
|
+
if (typeof group === "string" && group.includes(".") && !ids.has(group)) {
|
|
5814
|
+
add(
|
|
5815
|
+
path,
|
|
5816
|
+
`no menu group "${group}" in admin.nav`,
|
|
5817
|
+
"declare it: nav: [{ id: 'shop', label: 'Shop', children: [{ id: 'catalog', label: 'Catalog' }] }]"
|
|
5818
|
+
);
|
|
5819
|
+
return;
|
|
5820
|
+
}
|
|
5821
|
+
if (!isLabel(group))
|
|
5822
|
+
add(
|
|
5823
|
+
path,
|
|
5824
|
+
`must be a group's id, 'settings' or a label (got ${JSON.stringify(group)})`,
|
|
5825
|
+
"e.g. group: 'shop' or { en: 'Shop', th: '\u0E23\u0E49\u0E32\u0E19\u0E04\u0E49\u0E32' }"
|
|
5826
|
+
);
|
|
5827
|
+
}
|
|
5828
|
+
function validateLayout(layout, container, path, add) {
|
|
5829
|
+
if (!Array.isArray(layout)) {
|
|
5830
|
+
add(path, "must be a list", "e.g. layout: [{ tab: 'Content', fields: ['title'] }]");
|
|
5831
|
+
return;
|
|
5832
|
+
}
|
|
5833
|
+
const fields = new Map((container.fields ?? []).map((f) => [f.name, f]));
|
|
5834
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5835
|
+
const name = (value, at) => {
|
|
5836
|
+
if (typeof value !== "string" || !fields.has(value)) {
|
|
5837
|
+
add(at, `no top-level field ${JSON.stringify(value)}`);
|
|
5838
|
+
return;
|
|
5839
|
+
}
|
|
5840
|
+
if (fields.get(value)?.position === "sidebar")
|
|
5841
|
+
add(at, `"${value}" is in the sidebar (position: 'sidebar')`);
|
|
5842
|
+
if (seen.has(value)) add(at, `"${value}" is placed twice`);
|
|
5843
|
+
seen.add(value);
|
|
5844
|
+
};
|
|
5845
|
+
const nodes = (list, at, top) => {
|
|
5846
|
+
if (!Array.isArray(list)) {
|
|
5847
|
+
add(at, "must be a list");
|
|
5848
|
+
return;
|
|
5849
|
+
}
|
|
5850
|
+
const tabs = list.filter((n) => typeof n === "object" && n !== null && "tab" in n).length;
|
|
5851
|
+
if (top && tabs > 0 && tabs !== list.length)
|
|
5852
|
+
add(at, "with tabs, every entry at the top is a tab");
|
|
5853
|
+
list.forEach((node, i) => {
|
|
5854
|
+
const p = `${at}[${i}]`;
|
|
5855
|
+
if (typeof node === "string") {
|
|
5856
|
+
name(node, p);
|
|
5857
|
+
return;
|
|
5858
|
+
}
|
|
5859
|
+
const n = node ?? {};
|
|
5860
|
+
if ("tab" in n) {
|
|
5861
|
+
if (!top) add(p, "tabs go at the top of the layout only");
|
|
5862
|
+
if (!isLabel(n.tab)) add(`${p}.tab`, "must be a string or { en, th }");
|
|
5863
|
+
nodes(n.fields, `${p}.fields`, false);
|
|
5864
|
+
} else if ("collapsible" in n) {
|
|
5865
|
+
if (!isLabel(n.collapsible)) add(`${p}.collapsible`, "must be a string or { en, th }");
|
|
5866
|
+
nodes(n.fields, `${p}.fields`, false);
|
|
5867
|
+
} else if ("row" in n) {
|
|
5868
|
+
if (!Array.isArray(n.row) || n.row.length === 0)
|
|
5869
|
+
add(`${p}.row`, "must be a list of field names");
|
|
5870
|
+
else
|
|
5871
|
+
n.row.forEach((f, j) => {
|
|
5872
|
+
name(f, `${p}.row[${j}]`);
|
|
5873
|
+
});
|
|
5874
|
+
} else add(p, "must be a field name, { tab }, { collapsible } or { row }");
|
|
5875
|
+
});
|
|
5876
|
+
};
|
|
5877
|
+
nodes(layout, path, true);
|
|
5878
|
+
}
|
|
5879
|
+
function validateCollectionAdmin(admin, path, add) {
|
|
5880
|
+
const { badge, count, empty } = admin;
|
|
5881
|
+
if (badge !== void 0) {
|
|
5882
|
+
const b = badge ?? {};
|
|
5883
|
+
if (typeof b.where !== "object" || b.where === null || Array.isArray(b.where))
|
|
5884
|
+
add(`${path}.badge.where`, "must be a where query", "e.g. { status: { equals: 'paid' } }");
|
|
5885
|
+
if (b.tone !== void 0 && !["accent", "warning", "danger"].includes(b.tone))
|
|
5886
|
+
add(`${path}.badge.tone`, "must be 'accent', 'warning' or 'danger'");
|
|
5887
|
+
}
|
|
5888
|
+
if (count !== void 0 && typeof count !== "boolean")
|
|
5889
|
+
add(`${path}.count`, "must be true or false");
|
|
5890
|
+
if (empty !== void 0) {
|
|
5891
|
+
const e = empty ?? {};
|
|
5892
|
+
if (e.description !== void 0 && !isLabel(e.description))
|
|
5893
|
+
add(`${path}.empty.description`, "must be a string or { en, th }");
|
|
5894
|
+
if (e.link !== void 0) {
|
|
5895
|
+
const l = e.link ?? {};
|
|
5896
|
+
if (!isLabel(l.label) || typeof l.href !== "string")
|
|
5897
|
+
add(`${path}.empty.link`, "needs label and href");
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5900
|
+
}
|
|
5901
|
+
function validateContainer(container, path, collectionSlugs, add, { collection: collection2, rbac, nav }) {
|
|
5429
5902
|
if (!Array.isArray(container.fields)) {
|
|
5430
5903
|
add(`${path}.fields`, "must be an array");
|
|
5431
5904
|
return;
|
|
@@ -5457,9 +5930,11 @@ function validateContainer(container, path, collectionSlugs, add, { collection:
|
|
|
5457
5930
|
} else {
|
|
5458
5931
|
const sidebar = admin.sidebar;
|
|
5459
5932
|
if (sidebar !== void 0) validateComponents(sidebar, `${path}.admin.sidebar`, add);
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5933
|
+
validateGroup(admin.group, nav, `${path}.admin.group`, add);
|
|
5934
|
+
const layout = admin.layout;
|
|
5935
|
+
if (layout !== void 0) validateLayout(layout, container, `${path}.admin.layout`, add);
|
|
5936
|
+
if (collection2)
|
|
5937
|
+
validateCollectionAdmin(admin, `${path}.admin`, add);
|
|
5463
5938
|
const list = admin.list;
|
|
5464
5939
|
if (list !== void 0) validateList(list, container, `${path}.admin.list`, add);
|
|
5465
5940
|
const owner = admin.ownerField;
|
|
@@ -5551,7 +6026,14 @@ function validateField(field, path, siblings, collectionSlugs, add) {
|
|
|
5551
6026
|
if (typeof admin !== "object" || admin === null) {
|
|
5552
6027
|
add(`${path}.admin`, "must be an object", "e.g. admin: { component: 'ecms-color-picker' }");
|
|
5553
6028
|
} else {
|
|
5554
|
-
const { component, after, cell } = admin;
|
|
6029
|
+
const { component, after, cell, width, condition, description } = admin;
|
|
6030
|
+
if (width !== void 0 && !["1/4", "1/3", "1/2", "2/3", "3/4", "full"].includes(width))
|
|
6031
|
+
add(`${path}.admin.width`, "must be '1/4', '1/3', '1/2', '2/3', '3/4' or 'full'");
|
|
6032
|
+
if (description !== void 0 && !isLabel(description))
|
|
6033
|
+
add(`${path}.admin.description`, "must be a string or { en, th }");
|
|
6034
|
+
if (condition !== void 0)
|
|
6035
|
+
for (const issue of conditionIssues(condition, [...siblings.keys()]))
|
|
6036
|
+
add(`${path}.admin.condition`, issue);
|
|
5555
6037
|
if (component !== void 0) validateComponent(component, `${path}.admin.component`, add);
|
|
5556
6038
|
if (after !== void 0) validateComponents(after, `${path}.admin.after`, add);
|
|
5557
6039
|
if (cell !== void 0) validateComponent(cell, `${path}.admin.cell`, add);
|
|
@@ -5730,6 +6212,38 @@ function validateCommands(config, add) {
|
|
|
5730
6212
|
if (typeof run !== "function") add(`${path}.run`, "must be a function");
|
|
5731
6213
|
});
|
|
5732
6214
|
}
|
|
6215
|
+
function validateJobs(config, add) {
|
|
6216
|
+
const jobs = config.jobs;
|
|
6217
|
+
if (jobs === void 0) return;
|
|
6218
|
+
if (!Array.isArray(jobs)) {
|
|
6219
|
+
add("jobs", "must be an array", "e.g. jobs: [{ name: 'shop:clean-up', every: 3600, run }]");
|
|
6220
|
+
return;
|
|
6221
|
+
}
|
|
6222
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6223
|
+
jobs.forEach((job, i) => {
|
|
6224
|
+
const path = `jobs[${i}]`;
|
|
6225
|
+
const { name, every, run } = job ?? {};
|
|
6226
|
+
if (typeof name !== "string" || !/^[a-z][a-z0-9-]*(:[a-z0-9-]+)*$/.test(name))
|
|
6227
|
+
add(`${path}.name`, "must be lowercase letters, digits, - and :", "e.g. 'shop:clean-up'");
|
|
6228
|
+
else if (seen.has(name)) add(`${path}.name`, `"${name}" is already used by another job`);
|
|
6229
|
+
else seen.add(name);
|
|
6230
|
+
if (every !== void 0 && (typeof every !== "number" || !(every > 0)))
|
|
6231
|
+
add(`${path}.every`, "must be a number of seconds above 0");
|
|
6232
|
+
if (typeof run !== "function") add(`${path}.run`, "must be a function");
|
|
6233
|
+
});
|
|
6234
|
+
}
|
|
6235
|
+
function validateEvents(config, add) {
|
|
6236
|
+
const events = config.events;
|
|
6237
|
+
if (events === void 0) return;
|
|
6238
|
+
if (!Array.isArray(events)) {
|
|
6239
|
+
add("events", "must be an array", "e.g. events: ['order.paid']");
|
|
6240
|
+
return;
|
|
6241
|
+
}
|
|
6242
|
+
events.forEach((event, i) => {
|
|
6243
|
+
if (typeof event !== "string" || !/^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$/.test(event))
|
|
6244
|
+
add(`events[${i}]`, "must be named <area>.<what> in lowercase", "e.g. 'order.paid'");
|
|
6245
|
+
});
|
|
6246
|
+
}
|
|
5733
6247
|
function validateEndpoints(config, collectionSlugs, add) {
|
|
5734
6248
|
const endpoints = config.endpoints;
|
|
5735
6249
|
if (endpoints === void 0) return;
|
|
@@ -5884,7 +6398,9 @@ async function resolveConfig(input) {
|
|
|
5884
6398
|
modules: config.admin?.modules ?? [],
|
|
5885
6399
|
pages: config.admin?.pages ?? [],
|
|
5886
6400
|
dashboard: config.admin?.dashboard ?? [],
|
|
5887
|
-
switcher: config.admin?.switcher ?? null
|
|
6401
|
+
switcher: config.admin?.switcher ?? null,
|
|
6402
|
+
nav: config.admin?.nav ?? [],
|
|
6403
|
+
commands: config.admin?.commands ?? []
|
|
5888
6404
|
},
|
|
5889
6405
|
upload: {
|
|
5890
6406
|
dir: config.upload?.dir ?? "uploads",
|
|
@@ -5909,7 +6425,8 @@ async function resolveConfig(input) {
|
|
|
5909
6425
|
trustedOrigins: config.auth?.trustedOrigins ?? [],
|
|
5910
6426
|
resetPasswordExpiration: config.auth?.resetPasswordExpiration ?? 60 * 60,
|
|
5911
6427
|
inviteExpiration: config.auth?.inviteExpiration ?? 7 * 24 * 60 * 60,
|
|
5912
|
-
emails: config.auth?.emails ?? {}
|
|
6428
|
+
emails: config.auth?.emails ?? {},
|
|
6429
|
+
members: config.auth?.members ?? { roles: [] }
|
|
5913
6430
|
},
|
|
5914
6431
|
audit: config.audit ? resolveAudit(config.audit === true ? {} : config.audit) : false,
|
|
5915
6432
|
collections: [
|
|
@@ -5929,6 +6446,8 @@ async function resolveConfig(input) {
|
|
|
5929
6446
|
globals: config.globals ?? [],
|
|
5930
6447
|
endpoints: config.endpoints ?? [],
|
|
5931
6448
|
commands: config.commands ?? [],
|
|
6449
|
+
events: config.events ?? [],
|
|
6450
|
+
jobs: config.jobs ?? [],
|
|
5932
6451
|
installedPlugins: (plugins ?? []).map(
|
|
5933
6452
|
(plugin) => typeof plugin === "function" && plugin.info ? { ...plugin.info } : {}
|
|
5934
6453
|
),
|
|
@@ -5968,7 +6487,7 @@ async function createEasyCMS(config, options = {}) {
|
|
|
5968
6487
|
interactive: options.interactive ?? false
|
|
5969
6488
|
});
|
|
5970
6489
|
const cms = new EasyCMS(resolved3, db, logger, storage, cwd);
|
|
5971
|
-
const scheduling = [...resolved3.collections, ...resolved3.globals].some((c) => c.schedule) || (resolved3.webhooks?.length ?? 0) > 0 || resolved3.email !== void 0;
|
|
6490
|
+
const scheduling = [...resolved3.collections, ...resolved3.globals].some((c) => c.schedule) || (resolved3.webhooks?.length ?? 0) > 0 || resolved3.email !== void 0 || resolved3.jobs.length > 0;
|
|
5972
6491
|
if (scheduling && options.scheduler !== false) cms.startScheduler();
|
|
5973
6492
|
return cms;
|
|
5974
6493
|
}
|
|
@@ -6050,8 +6569,10 @@ var EasyCMS = class {
|
|
|
6050
6569
|
user,
|
|
6051
6570
|
cms: this
|
|
6052
6571
|
});
|
|
6572
|
+
const changed = result?.user !== void 0 ? result.user : user;
|
|
6053
6573
|
return {
|
|
6054
|
-
|
|
6574
|
+
// `onRequest` may change the role (e.g. per tenant): membership follows it.
|
|
6575
|
+
user: changed && asMember(changed, this.config.auth.members.roles),
|
|
6055
6576
|
context: result?.context ?? NO_CONTEXT
|
|
6056
6577
|
};
|
|
6057
6578
|
}
|
|
@@ -6317,7 +6838,7 @@ var EasyCMS = class {
|
|
|
6317
6838
|
/** A value with its HMAC (`secret`), for `purpose` only: `<base64url JSON>.<signature>`. */
|
|
6318
6839
|
sign(purpose, value) {
|
|
6319
6840
|
const payload = Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
6320
|
-
const mac =
|
|
6841
|
+
const mac = createHmac6("sha256", this.config.secret).update(`${purpose}:${payload}`);
|
|
6321
6842
|
return `${payload}.${mac.digest("base64url")}`;
|
|
6322
6843
|
}
|
|
6323
6844
|
/** The value of a token made by `sign`, or `undefined` when it isn't genuine. */
|
|
@@ -6325,10 +6846,10 @@ var EasyCMS = class {
|
|
|
6325
6846
|
const [payload, signature] = String(token).split(".");
|
|
6326
6847
|
if (!payload || !signature) return void 0;
|
|
6327
6848
|
const expected = Buffer.from(
|
|
6328
|
-
|
|
6849
|
+
createHmac6("sha256", this.config.secret).update(`${purpose}:${payload}`).digest("base64url")
|
|
6329
6850
|
);
|
|
6330
6851
|
const given = Buffer.from(signature);
|
|
6331
|
-
if (expected.length !== given.length || !
|
|
6852
|
+
if (expected.length !== given.length || !timingSafeEqual5(expected, given)) return void 0;
|
|
6332
6853
|
try {
|
|
6333
6854
|
return JSON.parse(Buffer.from(payload, "base64url").toString());
|
|
6334
6855
|
} catch {
|
|
@@ -6447,10 +6968,10 @@ var EasyCMS = class {
|
|
|
6447
6968
|
if (!signature || !Number.isInteger(at) || at < Date.now() / 1e3) return false;
|
|
6448
6969
|
const expected = Buffer.from(this.mediaSignature(key2, at));
|
|
6449
6970
|
const given = Buffer.from(signature);
|
|
6450
|
-
return expected.length === given.length &&
|
|
6971
|
+
return expected.length === given.length && timingSafeEqual5(expected, given);
|
|
6451
6972
|
}
|
|
6452
6973
|
mediaSignature(key2, expires) {
|
|
6453
|
-
return
|
|
6974
|
+
return createHmac6("sha256", this.config.secret).update(`media:${key2}:${expires}`).digest("base64url");
|
|
6454
6975
|
}
|
|
6455
6976
|
/**
|
|
6456
6977
|
* How many documents use any of these files in a top-level upload field (rich text and fields
|
|
@@ -6574,6 +7095,17 @@ var EasyCMS = class {
|
|
|
6574
7095
|
async update(collection2, id, data, options = {}) {
|
|
6575
7096
|
return await this.updateDocument(collection2, id, asObject(data, collection2), options);
|
|
6576
7097
|
}
|
|
7098
|
+
/**
|
|
7099
|
+
* Adds `by` (negative to take away) to a top-level number field in one database statement, so
|
|
7100
|
+
* concurrent calls never lose a change, e.g. stock or a counter. Skips hooks, validation, access
|
|
7101
|
+
* and versions. Returns the new value, or `null` when `min` / `max` would be passed.
|
|
7102
|
+
*/
|
|
7103
|
+
async increment(collection2, id, field, by, options = {}) {
|
|
7104
|
+
this.collection(collection2);
|
|
7105
|
+
const parsed = parseId(id);
|
|
7106
|
+
if (parsed === void 0) throw new NotFoundError(collection2, id);
|
|
7107
|
+
return this.db.increment({ collection: collection2, id: parsed, field, by, ...options });
|
|
7108
|
+
}
|
|
6577
7109
|
/**
|
|
6578
7110
|
* Takes a document off the site: its status becomes `draft`. With versions and drafts, a plain
|
|
6579
7111
|
* `update` with `status: 'draft'` only saves a draft and leaves the published document live;
|
|
@@ -6846,7 +7378,7 @@ var EasyCMS = class {
|
|
|
6846
7378
|
previousDoc: current,
|
|
6847
7379
|
operation: "update"
|
|
6848
7380
|
});
|
|
6849
|
-
this.
|
|
7381
|
+
this.emitChange(config, "draft", draft);
|
|
6850
7382
|
await this.audited(config, "draft", draft, current, guard, mode);
|
|
6851
7383
|
const [out2] = await this.output(config, [draft], guard, {
|
|
6852
7384
|
...options,
|
|
@@ -6874,11 +7406,9 @@ var EasyCMS = class {
|
|
|
6874
7406
|
Object.assign(prepared, moved.patch);
|
|
6875
7407
|
}
|
|
6876
7408
|
}
|
|
6877
|
-
const
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
data: { ...prepared, createdAt: existing.createdAt, updatedAt: now }
|
|
6881
|
-
});
|
|
7409
|
+
const data = { ...prepared, createdAt: existing.createdAt, updatedAt: now };
|
|
7410
|
+
const doc = options.where ? await this.db.update({ collection: collection2, id: parsed, data, where: options.where }) : await this.db.update({ collection: collection2, id: parsed, data });
|
|
7411
|
+
if (!doc) return null;
|
|
6882
7412
|
if (moved) await moved.cleanUp();
|
|
6883
7413
|
if (collection2 === MEDIA_FOLDERS) {
|
|
6884
7414
|
this.folders.invalidate();
|
|
@@ -6893,7 +7423,7 @@ var EasyCMS = class {
|
|
|
6893
7423
|
previousDoc: current,
|
|
6894
7424
|
operation: "update"
|
|
6895
7425
|
});
|
|
6896
|
-
this.
|
|
7426
|
+
this.emitChange(config, "update", doc, existing.status);
|
|
6897
7427
|
await this.audited(config, "update", doc, current, guard, mode);
|
|
6898
7428
|
const [out] = await this.output(config, [doc], guard, { ...options, draft: true });
|
|
6899
7429
|
return out;
|
|
@@ -6948,7 +7478,7 @@ var EasyCMS = class {
|
|
|
6948
7478
|
id: parsed,
|
|
6949
7479
|
doc: existing
|
|
6950
7480
|
});
|
|
6951
|
-
this.
|
|
7481
|
+
this.emitChange(config, "delete", existing);
|
|
6952
7482
|
await this.audited(config, "delete", existing, existing, guard);
|
|
6953
7483
|
const [out] = await this.output(config, [existing], guard, { depth: 0, draft: true });
|
|
6954
7484
|
return out;
|
|
@@ -7106,7 +7636,7 @@ var EasyCMS = class {
|
|
|
7106
7636
|
previousDoc: current,
|
|
7107
7637
|
operation: "update"
|
|
7108
7638
|
});
|
|
7109
|
-
this.
|
|
7639
|
+
this.emitChange(config, "draft", draft);
|
|
7110
7640
|
await this.audited(config, "draft", draft, current, guard, mode);
|
|
7111
7641
|
return this.readGlobal(config, key2, guard, { ...options, draft: true });
|
|
7112
7642
|
}
|
|
@@ -7118,7 +7648,7 @@ var EasyCMS = class {
|
|
|
7118
7648
|
previousDoc: current,
|
|
7119
7649
|
operation: "update"
|
|
7120
7650
|
});
|
|
7121
|
-
this.
|
|
7651
|
+
this.emitChange(config, "update", doc, existing.status);
|
|
7122
7652
|
await this.audited(config, "update", doc, current, guard, mode);
|
|
7123
7653
|
return this.readGlobal(config, key2, guard, { ...options, draft: true });
|
|
7124
7654
|
}
|
|
@@ -7231,10 +7761,45 @@ var EasyCMS = class {
|
|
|
7231
7761
|
return {
|
|
7232
7762
|
...scheduled,
|
|
7233
7763
|
webhooks: await this.retryWebhooks(now),
|
|
7234
|
-
emails: await this.mailer.retry(now)
|
|
7764
|
+
emails: await this.mailer.retry(now),
|
|
7765
|
+
jobs: await this.runConfigJobs(now)
|
|
7235
7766
|
};
|
|
7236
7767
|
});
|
|
7237
7768
|
}
|
|
7769
|
+
/** `jobs` of the config that are due; when each last ran is kept in the database. */
|
|
7770
|
+
async runConfigJobs(now) {
|
|
7771
|
+
const { jobs } = this.config;
|
|
7772
|
+
if (jobs.length === 0) return { ran: 0, failed: 0 };
|
|
7773
|
+
const last = await this.db.findGlobal({ slug: JOB_RUNS }) ?? {};
|
|
7774
|
+
const runs = {};
|
|
7775
|
+
for (const [name, at] of Object.entries(last)) if (typeof at === "string") runs[name] = at;
|
|
7776
|
+
let ran = 0;
|
|
7777
|
+
let failed = 0;
|
|
7778
|
+
for (const job of jobs) {
|
|
7779
|
+
const previous = runs[job.name];
|
|
7780
|
+
if (job.every !== void 0 && previous !== void 0 && now.getTime() - Date.parse(previous) < job.every * 1e3)
|
|
7781
|
+
continue;
|
|
7782
|
+
runs[job.name] = now.toISOString();
|
|
7783
|
+
try {
|
|
7784
|
+
await job.run({ cms: this, now });
|
|
7785
|
+
ran++;
|
|
7786
|
+
} catch (error) {
|
|
7787
|
+
failed++;
|
|
7788
|
+
this.logger.error(`Job ${job.name} failed: ${error.message}`);
|
|
7789
|
+
}
|
|
7790
|
+
}
|
|
7791
|
+
await this.db.updateGlobal({ slug: JOB_RUNS, data: { ...runs, updatedAt: now.toISOString() } });
|
|
7792
|
+
return { ran, failed };
|
|
7793
|
+
}
|
|
7794
|
+
/**
|
|
7795
|
+
* Sends an event of the app or a plugin (`events` in the config, e.g. `order.paid`) to the
|
|
7796
|
+
* webhooks that list it. Returns at once; deliveries are queued and retried like content changes.
|
|
7797
|
+
*/
|
|
7798
|
+
emit(event, data, about) {
|
|
7799
|
+
if (!this.config.events.includes(event))
|
|
7800
|
+
throw new QueryError(`Unknown event "${event}": add it to \`events\` in the config`);
|
|
7801
|
+
this.webhooks.emit(event, about ?? {}, data);
|
|
7802
|
+
}
|
|
7238
7803
|
/**
|
|
7239
7804
|
* Tries webhook deliveries that are due for another attempt. Deliveries are kept in the
|
|
7240
7805
|
* database until they succeed, so they survive restarts.
|
|
@@ -7744,7 +8309,7 @@ var EasyCMS = class {
|
|
|
7744
8309
|
doc,
|
|
7745
8310
|
operation: "create"
|
|
7746
8311
|
});
|
|
7747
|
-
this.
|
|
8312
|
+
this.emitChange(config, "create", doc);
|
|
7748
8313
|
await this.audited(config, "create", doc, void 0, hookGuard ?? guard);
|
|
7749
8314
|
const [out] = await this.output(config, [doc], hookGuard ?? guard, { ...options, draft: true });
|
|
7750
8315
|
return out;
|
|
@@ -7802,7 +8367,7 @@ var EasyCMS = class {
|
|
|
7802
8367
|
});
|
|
7803
8368
|
}
|
|
7804
8369
|
/** Sends webhook events for a change; `before` is the stored status before it, if any. */
|
|
7805
|
-
|
|
8370
|
+
emitChange(config, event, doc, before) {
|
|
7806
8371
|
const isGlobal = !this.config.collections.includes(config);
|
|
7807
8372
|
if (!isGlobal && INTERNAL_COLLECTIONS.has(config.slug)) return;
|
|
7808
8373
|
const body = isGlobal ? { ...snapshotOf(config.fields, doc), updatedAt: doc.updatedAt } : {
|
|
@@ -8228,6 +8793,7 @@ var UPLOAD_URL_SECONDS = 15 * 60;
|
|
|
8228
8793
|
var UPLOAD_TICKET_SECONDS = 24 * 60 * 60;
|
|
8229
8794
|
var SNIFF_BYTES = 64 * 1024;
|
|
8230
8795
|
var MAX_FILES_TO_MOVE = 200;
|
|
8796
|
+
var JOB_RUNS = "easy-cms:jobs";
|
|
8231
8797
|
function durationSeconds(value) {
|
|
8232
8798
|
const match = typeof value === "string" ? /^(\d+)\s*([smhd])$/.exec(value.trim()) : null;
|
|
8233
8799
|
const seconds = typeof value === "number" ? value : match ? Number(match[1]) * { s: 1, m: 60, h: 3600, d: 86400 }[match[2]] : Number.NaN;
|
|
@@ -8476,7 +9042,7 @@ async function readAdminModule(cms, index) {
|
|
|
8476
9042
|
}
|
|
8477
9043
|
|
|
8478
9044
|
// src/rest/admin-schema.ts
|
|
8479
|
-
var
|
|
9045
|
+
var PEOPLE = /* @__PURE__ */ new Set([USERS, API_KEYS]);
|
|
8480
9046
|
function componentRef(component) {
|
|
8481
9047
|
if (typeof component === "string") return { tag: component };
|
|
8482
9048
|
return component.props ? { tag: component.tag, props: JSON.parse(JSON.stringify(component.props)) } : { tag: component.tag };
|
|
@@ -8512,6 +9078,9 @@ async function serializeFields(fields, checks, localized, folderId, who) {
|
|
|
8512
9078
|
};
|
|
8513
9079
|
}
|
|
8514
9080
|
if (field.customType) f.customType = field.customType;
|
|
9081
|
+
if (field.admin?.description !== void 0) f.description = field.admin.description;
|
|
9082
|
+
if (field.admin?.width) f.width = field.admin.width;
|
|
9083
|
+
if (field.admin?.condition) f.condition = JSON.parse(JSON.stringify(field.admin.condition));
|
|
8515
9084
|
switch (field.type) {
|
|
8516
9085
|
case "text":
|
|
8517
9086
|
case "textarea":
|
|
@@ -8618,8 +9187,13 @@ async function collection(cms, config, user, localized, roles, context) {
|
|
|
8618
9187
|
if (config.labels) result.labels = config.labels;
|
|
8619
9188
|
if (config.useAsTitle) result.useAsTitle = config.useAsTitle;
|
|
8620
9189
|
if (config.icon) result.icon = config.icon;
|
|
8621
|
-
if (config.admin?.sidebar?.length) result.sidebar = config.admin.sidebar.map(
|
|
8622
|
-
if (config.admin?.
|
|
9190
|
+
if (config.admin?.sidebar?.length) result.sidebar = config.admin.sidebar.map(panelRef);
|
|
9191
|
+
if (config.admin?.layout?.length) result.layout = layoutOf(config.admin.layout, fields);
|
|
9192
|
+
const badge = config.admin?.badge;
|
|
9193
|
+
if (badge)
|
|
9194
|
+
result.badge = { tone: badge.tone ?? "accent", ...badge.label ? { label: badge.label } : {} };
|
|
9195
|
+
if (config.admin?.count === false) result.noCount = true;
|
|
9196
|
+
if (config.admin?.empty) result.empty = JSON.parse(JSON.stringify(config.admin.empty));
|
|
8623
9197
|
if (config.admin?.list?.tree) result.tree = config.admin.list.tree;
|
|
8624
9198
|
if (config.admin?.list?.sort) result.defaultSort = config.admin.list.sort;
|
|
8625
9199
|
if (config.admin?.confirmDelete) result.confirmDelete = { ...config.admin.confirmDelete };
|
|
@@ -8650,7 +9224,8 @@ async function global(cms, config, user, localized, context) {
|
|
|
8650
9224
|
};
|
|
8651
9225
|
if (config.label !== void 0) result.label = config.label;
|
|
8652
9226
|
if (config.icon) result.icon = config.icon;
|
|
8653
|
-
if (config.admin?.sidebar?.length) result.sidebar = config.admin.sidebar.map(
|
|
9227
|
+
if (config.admin?.sidebar?.length) result.sidebar = config.admin.sidebar.map(panelRef);
|
|
9228
|
+
if (config.admin?.layout?.length) result.layout = layoutOf(config.admin.layout, result.fields);
|
|
8654
9229
|
return result;
|
|
8655
9230
|
}
|
|
8656
9231
|
async function adminSchema(cms, user, origin, context = {}) {
|
|
@@ -8671,18 +9246,23 @@ async function adminSchema(cms, user, origin, context = {}) {
|
|
|
8671
9246
|
audit: cms.audit.enabled && await cms.roles.canView(user, "audit")
|
|
8672
9247
|
};
|
|
8673
9248
|
const roles = await cms.roles.options();
|
|
9249
|
+
const adminCollections = await Promise.all(
|
|
9250
|
+
collections.map((c) => collection(cms, c, user, localized, roles, context))
|
|
9251
|
+
);
|
|
9252
|
+
const adminGlobals = await Promise.all(
|
|
9253
|
+
cms.config.globals.map((g) => global(cms, g, user, localized, context))
|
|
9254
|
+
);
|
|
9255
|
+
const adminPages = await pages(cms, user);
|
|
8674
9256
|
return {
|
|
8675
9257
|
locale: cms.config.admin.locale,
|
|
8676
9258
|
switcher: cms.config.admin.switcher,
|
|
8677
9259
|
passwordLinks: cms.auth.canSendPasswordLinks(origin),
|
|
8678
9260
|
menu: [...cms.config.admin.menu],
|
|
9261
|
+
nav: buildNav(cms, adminCollections, adminGlobals, adminPages, views),
|
|
9262
|
+
commands: cms.config.admin.commands.map((c) => JSON.parse(JSON.stringify(c))),
|
|
8679
9263
|
localization: localization ? { locales: [...localization.locales], defaultLocale: localization.defaultLocale } : null,
|
|
8680
|
-
collections:
|
|
8681
|
-
|
|
8682
|
-
),
|
|
8683
|
-
globals: await Promise.all(
|
|
8684
|
-
cms.config.globals.map((g) => global(cms, g, user, localized, context))
|
|
8685
|
-
),
|
|
9264
|
+
collections: adminCollections,
|
|
9265
|
+
globals: adminGlobals,
|
|
8686
9266
|
modules: adminModuleUrls(cms),
|
|
8687
9267
|
uploadFromURL: cms.config.upload.fromURL !== void 0,
|
|
8688
9268
|
upload: {
|
|
@@ -8705,7 +9285,7 @@ async function adminSchema(cms, user, origin, context = {}) {
|
|
|
8705
9285
|
email: cms.config.collections.some((c) => c.slug === EMAIL_DELIVERIES)
|
|
8706
9286
|
}
|
|
8707
9287
|
} : {},
|
|
8708
|
-
pages:
|
|
9288
|
+
pages: adminPages,
|
|
8709
9289
|
dashboard: await widgets(cms, user)
|
|
8710
9290
|
};
|
|
8711
9291
|
}
|
|
@@ -8726,7 +9306,6 @@ async function pages(cms, user) {
|
|
|
8726
9306
|
path: page.path,
|
|
8727
9307
|
label: page.label,
|
|
8728
9308
|
...page.icon ? { icon: page.icon } : {},
|
|
8729
|
-
group: page.group ?? "content",
|
|
8730
9309
|
component: componentRef(page.component)
|
|
8731
9310
|
});
|
|
8732
9311
|
}
|
|
@@ -8745,9 +9324,200 @@ async function widgets(cms, user) {
|
|
|
8745
9324
|
function folderIds(cms, user, context) {
|
|
8746
9325
|
return cms.folders.enabled ? (key2) => cms.keyedFolder(key2, { user, context }) : void 0;
|
|
8747
9326
|
}
|
|
9327
|
+
function panelRef(panel) {
|
|
9328
|
+
const ref = componentRef(panel);
|
|
9329
|
+
if (typeof panel === "object" && "position" in panel && panel.position === "top")
|
|
9330
|
+
ref.position = "top";
|
|
9331
|
+
return ref;
|
|
9332
|
+
}
|
|
9333
|
+
function layoutOf(layout, fields) {
|
|
9334
|
+
const shown2 = new Set(fields.filter((f) => f.position !== "sidebar").map((f) => f.name));
|
|
9335
|
+
const placed = /* @__PURE__ */ new Set();
|
|
9336
|
+
const convert = (nodes) => nodes.flatMap((node) => {
|
|
9337
|
+
if (typeof node === "string") {
|
|
9338
|
+
if (!shown2.has(node)) return [];
|
|
9339
|
+
placed.add(node);
|
|
9340
|
+
return [{ type: "field", name: node }];
|
|
9341
|
+
}
|
|
9342
|
+
if ("row" in node) {
|
|
9343
|
+
const names2 = node.row.filter((n) => shown2.has(n));
|
|
9344
|
+
for (const n of names2) placed.add(n);
|
|
9345
|
+
return names2.length ? [{ type: "row", fields: names2 }] : [];
|
|
9346
|
+
}
|
|
9347
|
+
if ("collapsible" in node) {
|
|
9348
|
+
const inner2 = convert(node.fields);
|
|
9349
|
+
return inner2.length ? [
|
|
9350
|
+
{
|
|
9351
|
+
type: "collapsible",
|
|
9352
|
+
label: node.collapsible,
|
|
9353
|
+
...node.collapsed ? { collapsed: true } : {},
|
|
9354
|
+
...node.description !== void 0 ? { description: node.description } : {},
|
|
9355
|
+
nodes: inner2
|
|
9356
|
+
}
|
|
9357
|
+
] : [];
|
|
9358
|
+
}
|
|
9359
|
+
const inner = convert(node.fields);
|
|
9360
|
+
return [
|
|
9361
|
+
{
|
|
9362
|
+
type: "tab",
|
|
9363
|
+
label: node.tab,
|
|
9364
|
+
...node.description !== void 0 ? { description: node.description } : {},
|
|
9365
|
+
nodes: inner
|
|
9366
|
+
}
|
|
9367
|
+
];
|
|
9368
|
+
});
|
|
9369
|
+
const out = convert(layout);
|
|
9370
|
+
const rest = [...shown2].filter((n) => !placed.has(n)).map((name) => ({ type: "field", name }));
|
|
9371
|
+
const first = out[0];
|
|
9372
|
+
if (first?.type === "tab") first.nodes.push(...rest);
|
|
9373
|
+
else out.push(...rest);
|
|
9374
|
+
return out.filter((n) => n.type !== "tab" || n.nodes.length > 0);
|
|
9375
|
+
}
|
|
9376
|
+
var VIEW_GROUPS = [
|
|
9377
|
+
["roles", "settings.people"],
|
|
9378
|
+
["sso", "settings.people"],
|
|
9379
|
+
["backups", "settings.system"],
|
|
9380
|
+
["email", "settings.system"],
|
|
9381
|
+
["deliveries", "settings.system"],
|
|
9382
|
+
["audit", "settings.system"]
|
|
9383
|
+
];
|
|
9384
|
+
function buildNav(cms, collections, globals, pageRefs, views) {
|
|
9385
|
+
const groups = navGroups(cms.config.admin.nav);
|
|
9386
|
+
const ids = navIds(groups);
|
|
9387
|
+
const items = /* @__PURE__ */ new Map();
|
|
9388
|
+
const labelGroups = /* @__PURE__ */ new Map();
|
|
9389
|
+
const put = (group, item) => {
|
|
9390
|
+
if (group.label !== void 0 && !labelGroups.has(group.id))
|
|
9391
|
+
labelGroups.set(group.id, group.label);
|
|
9392
|
+
const list = items.get(group.id) ?? [];
|
|
9393
|
+
list.push(item);
|
|
9394
|
+
items.set(group.id, list);
|
|
9395
|
+
};
|
|
9396
|
+
const configOf = (slug2) => cms.config.collections.find((c) => c.slug === slug2);
|
|
9397
|
+
const menu = cms.config.admin.menu;
|
|
9398
|
+
const rank2 = (slug2, index) => {
|
|
9399
|
+
const at = menu.indexOf(slug2);
|
|
9400
|
+
return at === -1 ? menu.length + index : at;
|
|
9401
|
+
};
|
|
9402
|
+
const ordered = collections.map((c, index) => ({ c, key: rank2(c.slug, index) })).sort((a, b) => a.key - b.key).map(({ c }) => c);
|
|
9403
|
+
let media = false;
|
|
9404
|
+
for (const c of ordered) {
|
|
9405
|
+
if (!c.permissions.read || c.slug === MEDIA_FOLDERS) continue;
|
|
9406
|
+
if (c.slug === MEDIA) {
|
|
9407
|
+
media = true;
|
|
9408
|
+
continue;
|
|
9409
|
+
}
|
|
9410
|
+
const group = PEOPLE.has(c.slug) ? { id: "settings.people" } : groupOf(configOf(c.slug)?.admin?.group, ids, "content");
|
|
9411
|
+
put(group, { kind: "collection", slug: c.slug });
|
|
9412
|
+
}
|
|
9413
|
+
for (const g of globals) {
|
|
9414
|
+
if (!g.permissions.read) continue;
|
|
9415
|
+
const config = cms.config.globals.find((x) => x.slug === g.slug);
|
|
9416
|
+
put(groupOf(config?.admin?.group, ids, "settings.site"), { kind: "global", slug: g.slug });
|
|
9417
|
+
}
|
|
9418
|
+
for (const p of pageRefs) {
|
|
9419
|
+
const config = cms.config.admin.pages.find((x) => x.path === p.path);
|
|
9420
|
+
const group = config?.group ?? "content";
|
|
9421
|
+
if (group === false) continue;
|
|
9422
|
+
put(groupOf(group, ids, "content"), { kind: "page", path: p.path });
|
|
9423
|
+
}
|
|
9424
|
+
for (const [view, group] of VIEW_GROUPS)
|
|
9425
|
+
if (views[view]) put({ id: group }, { kind: "view", view });
|
|
9426
|
+
const toGroup = (g) => {
|
|
9427
|
+
const children = g.children.map(toGroup).filter((c) => c !== null);
|
|
9428
|
+
const own = items.get(g.id) ?? [];
|
|
9429
|
+
if (own.length === 0 && children.length === 0) return null;
|
|
9430
|
+
return {
|
|
9431
|
+
kind: "group",
|
|
9432
|
+
id: g.id,
|
|
9433
|
+
label: g.label,
|
|
9434
|
+
...g.icon ? { icon: g.icon } : {},
|
|
9435
|
+
items: [...own, ...children]
|
|
9436
|
+
};
|
|
9437
|
+
};
|
|
9438
|
+
const root = [];
|
|
9439
|
+
for (const g of groups) {
|
|
9440
|
+
const node = toGroup(g);
|
|
9441
|
+
if (node) root.push({ order: g.order, node });
|
|
9442
|
+
}
|
|
9443
|
+
let i = 0;
|
|
9444
|
+
for (const [id, label] of labelGroups) {
|
|
9445
|
+
const own = items.get(id) ?? [];
|
|
9446
|
+
if (own.length)
|
|
9447
|
+
root.push({ order: LABEL_ORDER + i++, node: { kind: "group", id, label, items: own } });
|
|
9448
|
+
}
|
|
9449
|
+
if (media) root.push({ order: MEDIA_ORDER, node: { kind: "collection", slug: MEDIA } });
|
|
9450
|
+
return root.sort((a, b) => a.order - b.order).map((r) => r.node);
|
|
9451
|
+
}
|
|
9452
|
+
|
|
9453
|
+
// src/rest/admin-search.ts
|
|
9454
|
+
var listed = (cms) => cms.config.collections.filter(
|
|
9455
|
+
(c) => !INTERNAL_COLLECTIONS.has(c.slug) && c.slug !== MEDIA_FOLDERS
|
|
9456
|
+
);
|
|
9457
|
+
async function readable(cms, c, who) {
|
|
9458
|
+
try {
|
|
9459
|
+
await cms.count(c.slug, { ...who, overrideAccess: false, where: { id: { exists: false } } });
|
|
9460
|
+
return true;
|
|
9461
|
+
} catch {
|
|
9462
|
+
return false;
|
|
9463
|
+
}
|
|
9464
|
+
}
|
|
9465
|
+
async function adminCounts(cms, who) {
|
|
9466
|
+
const counts = {};
|
|
9467
|
+
const badges = {};
|
|
9468
|
+
await Promise.all(
|
|
9469
|
+
listed(cms).map(async (c) => {
|
|
9470
|
+
const read2 = { ...who, overrideAccess: false, draft: true };
|
|
9471
|
+
try {
|
|
9472
|
+
if (c.admin?.count !== false) counts[c.slug] = await cms.count(c.slug, read2);
|
|
9473
|
+
if (c.admin?.badge)
|
|
9474
|
+
badges[c.slug] = await cms.count(c.slug, { ...read2, where: c.admin.badge.where });
|
|
9475
|
+
} catch {
|
|
9476
|
+
}
|
|
9477
|
+
})
|
|
9478
|
+
);
|
|
9479
|
+
return { counts, badges };
|
|
9480
|
+
}
|
|
9481
|
+
async function adminSearch(cms, who, q, perCollection = 5) {
|
|
9482
|
+
const query = q.trim().slice(0, 100);
|
|
9483
|
+
if (query.length < 1) return [];
|
|
9484
|
+
const results = await Promise.all(
|
|
9485
|
+
listed(cms).map(async (c) => {
|
|
9486
|
+
const title = c.useAsTitle ?? (c.fields.some((f) => f.name === "title") ? "title" : void 0);
|
|
9487
|
+
const conditions = [];
|
|
9488
|
+
if (title) conditions.push({ [title]: { like: query } });
|
|
9489
|
+
if (/^\d+$/.test(query)) conditions.push({ id: { equals: Number(query) } });
|
|
9490
|
+
if (conditions.length === 0 || !await readable(cms, c, who)) return [];
|
|
9491
|
+
try {
|
|
9492
|
+
const found = await cms.find(c.slug, {
|
|
9493
|
+
...who,
|
|
9494
|
+
overrideAccess: false,
|
|
9495
|
+
draft: true,
|
|
9496
|
+
depth: 0,
|
|
9497
|
+
limit: perCollection,
|
|
9498
|
+
where: conditions.length === 1 ? conditions[0] : { or: conditions },
|
|
9499
|
+
sort: "-updatedAt"
|
|
9500
|
+
});
|
|
9501
|
+
return found.docs.map((doc) => {
|
|
9502
|
+
const d = doc;
|
|
9503
|
+
const value = title ? d[title] : void 0;
|
|
9504
|
+
return {
|
|
9505
|
+
collection: c.slug,
|
|
9506
|
+
id: d.id,
|
|
9507
|
+
title: typeof value === "string" && value ? value : `#${String(d.id)}`,
|
|
9508
|
+
...typeof d.status === "string" ? { status: d.status } : {}
|
|
9509
|
+
};
|
|
9510
|
+
});
|
|
9511
|
+
} catch {
|
|
9512
|
+
return [];
|
|
9513
|
+
}
|
|
9514
|
+
})
|
|
9515
|
+
);
|
|
9516
|
+
return results.flat();
|
|
9517
|
+
}
|
|
8748
9518
|
|
|
8749
9519
|
// src/version.ts
|
|
8750
|
-
var VERSION = true ? "0.
|
|
9520
|
+
var VERSION = true ? "0.47.0" : "0.0.0-dev";
|
|
8751
9521
|
|
|
8752
9522
|
// src/rest/admin-status.ts
|
|
8753
9523
|
var DAY2 = 864e5;
|
|
@@ -9120,8 +9890,9 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9120
9890
|
context: ctx.context,
|
|
9121
9891
|
...parseLocale(ctx.url)
|
|
9122
9892
|
};
|
|
9123
|
-
const draft = ctx.user !== null && ctx.url.searchParams.get("draft") === "true";
|
|
9893
|
+
const draft = ctx.user !== null && ctx.user.member !== true && ctx.url.searchParams.get("draft") === "true";
|
|
9124
9894
|
if (segments.length === 0) throw new HttpError("Not found", 404);
|
|
9895
|
+
if (first === "admin" && ctx.user?.member === true) throw new ForbiddenError();
|
|
9125
9896
|
if (first === "auth") return ssoRoute(cms, ctx, method, segments);
|
|
9126
9897
|
if (first === USERS && second !== void 0 && third === void 0 && !/^\d+$/.test(second)) {
|
|
9127
9898
|
switch (`${method} ${second}`) {
|
|
@@ -9194,6 +9965,45 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9194
9965
|
body: { user: session.user, exp: session.expiresAt, csrfToken: session.csrfToken }
|
|
9195
9966
|
};
|
|
9196
9967
|
}
|
|
9968
|
+
// Members signing up (`auth.members.signup`): the form's token, then the account.
|
|
9969
|
+
case "GET signup":
|
|
9970
|
+
return { body: cms.auth.signupForm() };
|
|
9971
|
+
case "POST signup": {
|
|
9972
|
+
const body = await readJson(ctx.request);
|
|
9973
|
+
const result = await cms.auth.signup({
|
|
9974
|
+
email: String(body.email ?? ""),
|
|
9975
|
+
password: String(body.password ?? ""),
|
|
9976
|
+
name: typeof body.name === "string" ? body.name : void 0,
|
|
9977
|
+
token: body.token,
|
|
9978
|
+
honeypot: body[SIGNUP_HONEYPOT],
|
|
9979
|
+
turnstile: body.turnstile,
|
|
9980
|
+
ip: options.getClientIp?.(ctx.request),
|
|
9981
|
+
origin: ctx.url.origin,
|
|
9982
|
+
locale: typeof body.locale === "string" ? body.locale : void 0,
|
|
9983
|
+
context: ctx.context
|
|
9984
|
+
});
|
|
9985
|
+
if (result.verify)
|
|
9986
|
+
return {
|
|
9987
|
+
status: 202,
|
|
9988
|
+
body: { verify: true, message: "Check your email for a link to confirm it." }
|
|
9989
|
+
};
|
|
9990
|
+
setSessionCookies(cms, ctx, result);
|
|
9991
|
+
return {
|
|
9992
|
+
status: 201,
|
|
9993
|
+
body: { user: result.user, exp: result.expiresAt, csrfToken: result.csrfToken }
|
|
9994
|
+
};
|
|
9995
|
+
}
|
|
9996
|
+
case "POST verify-email": {
|
|
9997
|
+
const body = await readJson(ctx.request);
|
|
9998
|
+
const session = await cms.auth.verifyEmail({
|
|
9999
|
+
token: String(body.token ?? ""),
|
|
10000
|
+
ip: options.getClientIp?.(ctx.request)
|
|
10001
|
+
});
|
|
10002
|
+
setSessionCookies(cms, ctx, session);
|
|
10003
|
+
return {
|
|
10004
|
+
body: { user: session.user, exp: session.expiresAt, csrfToken: session.csrfToken }
|
|
10005
|
+
};
|
|
10006
|
+
}
|
|
9197
10007
|
case "POST first-register": {
|
|
9198
10008
|
const body = await readJson(ctx.request);
|
|
9199
10009
|
const session = await cms.auth.registerFirstUser({
|
|
@@ -9430,6 +10240,18 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
9430
10240
|
if (!ctx.user) throw new UnauthorizedError();
|
|
9431
10241
|
if (second === "schema" && third === void 0)
|
|
9432
10242
|
return { body: await adminSchema(cms, ctx.user, ctx.url.origin, ctx.context) };
|
|
10243
|
+
if (second === "counts" && third === void 0)
|
|
10244
|
+
return { body: await adminCounts(cms, { user: ctx.user, context: ctx.context }) };
|
|
10245
|
+
if (second === "search" && third === void 0)
|
|
10246
|
+
return {
|
|
10247
|
+
body: {
|
|
10248
|
+
docs: await adminSearch(
|
|
10249
|
+
cms,
|
|
10250
|
+
{ user: ctx.user, context: ctx.context },
|
|
10251
|
+
ctx.url.searchParams.get("q") ?? ""
|
|
10252
|
+
)
|
|
10253
|
+
}
|
|
10254
|
+
};
|
|
9433
10255
|
if (second === "modules" && third !== void 0 && segments.length === 3) {
|
|
9434
10256
|
const index = /^(\d+)\.js$/.exec(third)?.[1];
|
|
9435
10257
|
const file = index === void 0 ? void 0 : await readAdminModule(cms, Number(index));
|
|
@@ -10276,6 +11098,7 @@ export {
|
|
|
10276
11098
|
AuditLog,
|
|
10277
11099
|
Auth,
|
|
10278
11100
|
BUILTIN_COLLECTIONS,
|
|
11101
|
+
BUILTIN_NAV,
|
|
10279
11102
|
CONFIG_FILE_NAMES,
|
|
10280
11103
|
CSRF_COOKIE,
|
|
10281
11104
|
CSRF_HEADER,
|
|
@@ -10318,6 +11141,8 @@ export {
|
|
|
10318
11141
|
anyone,
|
|
10319
11142
|
applyDefaults,
|
|
10320
11143
|
applyPlugins,
|
|
11144
|
+
checkFormToken,
|
|
11145
|
+
conditionIssues,
|
|
10321
11146
|
configSignature,
|
|
10322
11147
|
consoleEmail,
|
|
10323
11148
|
consoleLogger,
|
|
@@ -10332,24 +11157,29 @@ export {
|
|
|
10332
11157
|
expandMimeTypes,
|
|
10333
11158
|
fillMissing,
|
|
10334
11159
|
findConfigFile,
|
|
11160
|
+
formToken,
|
|
10335
11161
|
generateSlugs,
|
|
10336
11162
|
generateTypes,
|
|
10337
11163
|
hasRows,
|
|
10338
11164
|
hashPassword,
|
|
11165
|
+
honeypotName,
|
|
10339
11166
|
imageDimensions,
|
|
10340
11167
|
importConfig,
|
|
10341
11168
|
isAdmin,
|
|
10342
11169
|
isHasMany,
|
|
10343
11170
|
isLoggedIn,
|
|
11171
|
+
isSignedIn,
|
|
10344
11172
|
isSystemAdmin,
|
|
10345
11173
|
keyAllows,
|
|
10346
11174
|
loadConfig,
|
|
10347
11175
|
localStorage,
|
|
11176
|
+
matchesCondition,
|
|
10348
11177
|
mergeForUpdate,
|
|
10349
11178
|
mimeAllowed,
|
|
10350
11179
|
mimeAllowedBy,
|
|
10351
11180
|
parseId,
|
|
10352
11181
|
populate,
|
|
11182
|
+
rateKeys,
|
|
10353
11183
|
readCookie,
|
|
10354
11184
|
resolveAdminModule,
|
|
10355
11185
|
resolveConfig,
|
|
@@ -10363,5 +11193,6 @@ export {
|
|
|
10363
11193
|
validateConfig,
|
|
10364
11194
|
validateFields,
|
|
10365
11195
|
verifyPassword,
|
|
11196
|
+
verifyTurnstile,
|
|
10366
11197
|
writeBackupFile
|
|
10367
11198
|
};
|