@easy-cms/core 0.31.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +235 -5
- package/dist/index.js +1269 -425
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -116,10 +116,10 @@ function cleanPermissions(value, known) {
|
|
|
116
116
|
const pick = (group, slugs, operations) => {
|
|
117
117
|
const out = {};
|
|
118
118
|
if (typeof group !== "object" || group === null) return out;
|
|
119
|
-
for (const [
|
|
120
|
-
if (!slugs.includes(
|
|
119
|
+
for (const [slug2, ops] of Object.entries(group)) {
|
|
120
|
+
if (!slugs.includes(slug2) || API_KEY_EXCLUDED.has(slug2) || !Array.isArray(ops)) continue;
|
|
121
121
|
const valid = operations.filter((op) => ops.includes(op));
|
|
122
|
-
if (valid.length) out[
|
|
122
|
+
if (valid.length) out[slug2] = valid;
|
|
123
123
|
}
|
|
124
124
|
return out;
|
|
125
125
|
};
|
|
@@ -293,22 +293,22 @@ async function stripFields(fields, doc, read2) {
|
|
|
293
293
|
}
|
|
294
294
|
return result;
|
|
295
295
|
}
|
|
296
|
-
async function filterInput(fields, data,
|
|
296
|
+
async function filterInput(fields, data, update2) {
|
|
297
297
|
const result = { ...data };
|
|
298
298
|
for (const field of fields) {
|
|
299
299
|
if (!(field.name in result)) continue;
|
|
300
|
-
if (field.hidden ||
|
|
300
|
+
if (field.hidden || update2 && !await update2.allows(field)) {
|
|
301
301
|
delete result[field.name];
|
|
302
302
|
continue;
|
|
303
303
|
}
|
|
304
304
|
const value = result[field.name];
|
|
305
305
|
if (field.type === "group" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
306
|
-
result[field.name] = await filterInput(field.fields, value,
|
|
306
|
+
result[field.name] = await filterInput(field.fields, value, update2);
|
|
307
307
|
} else if (hasRows(field) && Array.isArray(value)) {
|
|
308
308
|
result[field.name] = await Promise.all(
|
|
309
309
|
value.map((row) => {
|
|
310
310
|
const fields2 = rowFields(field, row);
|
|
311
|
-
return fields2 && row && typeof row === "object" ? filterInput(fields2, row,
|
|
311
|
+
return fields2 && row && typeof row === "object" ? filterInput(fields2, row, update2) : row;
|
|
312
312
|
})
|
|
313
313
|
);
|
|
314
314
|
}
|
|
@@ -325,13 +325,17 @@ var VERSIONS = "document-versions";
|
|
|
325
325
|
var SCHEDULED_JOBS = "scheduled-jobs";
|
|
326
326
|
var WEBHOOK_DELIVERIES = "webhook-deliveries";
|
|
327
327
|
var EMAIL_DELIVERIES = "email-deliveries";
|
|
328
|
+
var DATABASE_BACKUPS = "database-backups";
|
|
329
|
+
var ROLES = "user-roles";
|
|
328
330
|
var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
329
331
|
SESSIONS,
|
|
330
332
|
LOGIN_ATTEMPTS,
|
|
331
333
|
VERSIONS,
|
|
332
334
|
SCHEDULED_JOBS,
|
|
333
335
|
WEBHOOK_DELIVERIES,
|
|
334
|
-
EMAIL_DELIVERIES
|
|
336
|
+
EMAIL_DELIVERIES,
|
|
337
|
+
DATABASE_BACKUPS,
|
|
338
|
+
ROLES
|
|
335
339
|
]);
|
|
336
340
|
var DEFAULT_ROLES = ["admin", "editor"];
|
|
337
341
|
var adminOrSelf = ({ user }) => {
|
|
@@ -342,7 +346,8 @@ var adminOrSelf = ({ user }) => {
|
|
|
342
346
|
var adminOnly = {
|
|
343
347
|
update: ({ user }) => user?.role === "admin"
|
|
344
348
|
};
|
|
345
|
-
function userFields(roles) {
|
|
349
|
+
function userFields(roles, rbac) {
|
|
350
|
+
const defaultRole = roles.includes("editor") ? "editor" : roles[roles.length - 1];
|
|
346
351
|
return [
|
|
347
352
|
{
|
|
348
353
|
name: "email",
|
|
@@ -352,12 +357,20 @@ function userFields(roles) {
|
|
|
352
357
|
label: { en: "Email", th: "\u0E2D\u0E35\u0E40\u0E21\u0E25" }
|
|
353
358
|
},
|
|
354
359
|
{ name: "name", type: "text", label: { en: "Name", th: "\u0E0A\u0E37\u0E48\u0E2D" } },
|
|
355
|
-
|
|
360
|
+
// With roles from the admin (`auth.rbac`), any role in Settings → Roles: checked on save.
|
|
361
|
+
rbac ? {
|
|
362
|
+
name: "role",
|
|
363
|
+
type: "text",
|
|
364
|
+
required: true,
|
|
365
|
+
defaultValue: defaultRole,
|
|
366
|
+
access: adminOnly,
|
|
367
|
+
label: { en: "Role", th: "\u0E1A\u0E17\u0E1A\u0E32\u0E17" }
|
|
368
|
+
} : {
|
|
356
369
|
name: "role",
|
|
357
370
|
type: "select",
|
|
358
371
|
options: roles,
|
|
359
372
|
required: true,
|
|
360
|
-
defaultValue:
|
|
373
|
+
defaultValue: defaultRole,
|
|
361
374
|
access: adminOnly,
|
|
362
375
|
label: { en: "Role", th: "\u0E1A\u0E17\u0E1A\u0E32\u0E17" }
|
|
363
376
|
},
|
|
@@ -380,7 +393,7 @@ function withUsers(config) {
|
|
|
380
393
|
useAsTitle: "email",
|
|
381
394
|
icon: "users",
|
|
382
395
|
...custom,
|
|
383
|
-
fields: [...userFields(roles), ...custom?.fields ?? []],
|
|
396
|
+
fields: [...userFields(roles, config.auth?.rbac === true), ...custom?.fields ?? []],
|
|
384
397
|
access: {
|
|
385
398
|
read: isLoggedIn,
|
|
386
399
|
create: isAdmin,
|
|
@@ -546,6 +559,25 @@ var webhookDeliveriesCollection = {
|
|
|
546
559
|
{ name: "error", type: "text" }
|
|
547
560
|
]
|
|
548
561
|
};
|
|
562
|
+
var databaseBackupsCollection = {
|
|
563
|
+
slug: DATABASE_BACKUPS,
|
|
564
|
+
access: { read: nobody, create: nobody, update: nobody, delete: nobody },
|
|
565
|
+
fields: [
|
|
566
|
+
// pending | running | done | failed
|
|
567
|
+
{ name: "state", type: "text", required: true, index: true },
|
|
568
|
+
// manual | scheduled
|
|
569
|
+
{ name: "trigger", type: "text", required: true },
|
|
570
|
+
{ name: "filename", type: "text" },
|
|
571
|
+
{ name: "size", type: "number" },
|
|
572
|
+
{ name: "startedAt", type: "text", index: true },
|
|
573
|
+
{ name: "finishedAt", type: "text" },
|
|
574
|
+
{ name: "error", type: "text" },
|
|
575
|
+
// Who started it (email), for manual ones.
|
|
576
|
+
{ name: "author", type: "text" },
|
|
577
|
+
{ name: "downloadedBy", type: "text" },
|
|
578
|
+
{ name: "downloadedAt", type: "text" }
|
|
579
|
+
]
|
|
580
|
+
};
|
|
549
581
|
var emailDeliveriesCollection = {
|
|
550
582
|
slug: EMAIL_DELIVERIES,
|
|
551
583
|
access: { read: nobody, create: nobody, update: nobody, delete: nobody },
|
|
@@ -1089,6 +1121,386 @@ var Auth = class {
|
|
|
1089
1121
|
}
|
|
1090
1122
|
};
|
|
1091
1123
|
|
|
1124
|
+
// src/backups.ts
|
|
1125
|
+
import { mkdtemp, readFile as readFile2, rm as rm2 } from "fs/promises";
|
|
1126
|
+
import { tmpdir } from "os";
|
|
1127
|
+
import { join as join2 } from "path";
|
|
1128
|
+
import { promisify } from "util";
|
|
1129
|
+
import { gzip } from "zlib";
|
|
1130
|
+
|
|
1131
|
+
// src/copy.ts
|
|
1132
|
+
var BATCH = 200;
|
|
1133
|
+
async function copyDatabase(source, target, options = {}) {
|
|
1134
|
+
const from = source.transfer;
|
|
1135
|
+
const to = target.transfer;
|
|
1136
|
+
if (!from || !to) throw new EasyCMSError("This database adapter cannot copy databases", 400);
|
|
1137
|
+
if (from.schemaHash !== to.schemaHash) {
|
|
1138
|
+
throw new EasyCMSError(
|
|
1139
|
+
"The two configs describe different collections or fields. Copy between configs with the same collections (import one config into the other and change only `db`).",
|
|
1140
|
+
400
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
const tables = from.tables();
|
|
1144
|
+
for (const table of tables) {
|
|
1145
|
+
if (await to.count(table) > 0) {
|
|
1146
|
+
throw new EasyCMSError(
|
|
1147
|
+
`The target database is not empty ("${table}" has rows). Copy into a new, empty database.`,
|
|
1148
|
+
400
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
let total = 0;
|
|
1153
|
+
for (const table of tables) {
|
|
1154
|
+
let copied = 0;
|
|
1155
|
+
for (; ; ) {
|
|
1156
|
+
const rows = await from.read(table, copied, BATCH);
|
|
1157
|
+
if (rows.length === 0) break;
|
|
1158
|
+
await to.write(table, rows);
|
|
1159
|
+
copied += rows.length;
|
|
1160
|
+
if (rows.length < BATCH) break;
|
|
1161
|
+
}
|
|
1162
|
+
total += copied;
|
|
1163
|
+
options.onTable?.({ table, rows: copied });
|
|
1164
|
+
}
|
|
1165
|
+
await to.finish();
|
|
1166
|
+
return { tables: tables.length, rows: total };
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// src/storage.ts
|
|
1170
|
+
import { mkdir, readFile, rm, stat, writeFile } from "fs/promises";
|
|
1171
|
+
import { dirname, isAbsolute, join, normalize, resolve, sep } from "path";
|
|
1172
|
+
function localStorage(options = {}) {
|
|
1173
|
+
let root = resolve(
|
|
1174
|
+
/* turbopackIgnore: true */
|
|
1175
|
+
options.dir ?? "uploads"
|
|
1176
|
+
);
|
|
1177
|
+
const pathOf = (key2) => {
|
|
1178
|
+
const path = normalize(join(
|
|
1179
|
+
/* turbopackIgnore: true */
|
|
1180
|
+
root,
|
|
1181
|
+
key2
|
|
1182
|
+
));
|
|
1183
|
+
if (!path.startsWith(root + sep)) throw new Error(`Invalid storage key "${key2}"`);
|
|
1184
|
+
return path;
|
|
1185
|
+
};
|
|
1186
|
+
return {
|
|
1187
|
+
name: "local",
|
|
1188
|
+
init({ cwd }) {
|
|
1189
|
+
const dir = options.dir ?? "uploads";
|
|
1190
|
+
root = isAbsolute(dir) ? dir : resolve(
|
|
1191
|
+
/* turbopackIgnore: true */
|
|
1192
|
+
cwd,
|
|
1193
|
+
dir
|
|
1194
|
+
);
|
|
1195
|
+
},
|
|
1196
|
+
async put(key2, data) {
|
|
1197
|
+
const path = pathOf(key2);
|
|
1198
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1199
|
+
await writeFile(path, data, { flag: "wx" });
|
|
1200
|
+
},
|
|
1201
|
+
async get(key2) {
|
|
1202
|
+
const path = pathOf(key2);
|
|
1203
|
+
const info = await stat(path).catch(() => void 0);
|
|
1204
|
+
if (!info?.isFile()) return null;
|
|
1205
|
+
return { body: new Uint8Array(await readFile(path)), size: info.size };
|
|
1206
|
+
},
|
|
1207
|
+
async delete(key2) {
|
|
1208
|
+
await rm(pathOf(key2), { force: true });
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// src/backups.ts
|
|
1214
|
+
var gzipAsync = promisify(gzip);
|
|
1215
|
+
var DEFAULT_BACKUP_TIME = "03:00";
|
|
1216
|
+
var DEFAULT_BACKUP_KEEP = 7;
|
|
1217
|
+
var DEFAULT_DIR = "backups";
|
|
1218
|
+
var STALE = 30 * 6e4;
|
|
1219
|
+
var PERIOD = { day: 864e5, week: 7 * 864e5 };
|
|
1220
|
+
var storages = /* @__PURE__ */ new WeakMap();
|
|
1221
|
+
function backupStorage(cms) {
|
|
1222
|
+
let storage = storages.get(cms);
|
|
1223
|
+
if (!storage) {
|
|
1224
|
+
storage = (async () => {
|
|
1225
|
+
const config = cms.config.backups;
|
|
1226
|
+
const adapter = config?.storage ?? localStorage({ dir: config?.dir ?? DEFAULT_DIR });
|
|
1227
|
+
await adapter.init?.({ cwd: cms.cwd });
|
|
1228
|
+
return adapter;
|
|
1229
|
+
})();
|
|
1230
|
+
storages.set(cms, storage);
|
|
1231
|
+
}
|
|
1232
|
+
return storage;
|
|
1233
|
+
}
|
|
1234
|
+
function backupUnavailable(cms) {
|
|
1235
|
+
if (cms.db.backup || cms.config.backups?.sqlite) return null;
|
|
1236
|
+
return `The ${cms.config.db.name} database needs the SQLite adapter to write backups: backups: { sqlite } with sqlite from @easy-cms/db-sqlite`;
|
|
1237
|
+
}
|
|
1238
|
+
async function writeBackupFile(cms, file) {
|
|
1239
|
+
if (cms.db.backup) return cms.db.backup(file);
|
|
1240
|
+
const factory = cms.config.backups?.sqlite;
|
|
1241
|
+
if (!factory) throw new EasyCMSError(backupUnavailable(cms), 400);
|
|
1242
|
+
const prefix = cms.db.tablePrefix;
|
|
1243
|
+
const scratch = `${file}.copy`;
|
|
1244
|
+
const adapter = factory({ url: `file:${scratch}`, ...prefix ? { tablePrefix: prefix } : {} });
|
|
1245
|
+
const target = await adapter.init({
|
|
1246
|
+
config: { ...cms.config, db: adapter },
|
|
1247
|
+
cwd: cms.cwd,
|
|
1248
|
+
schema: "push",
|
|
1249
|
+
logger: { ...cms.logger, info() {
|
|
1250
|
+
} }
|
|
1251
|
+
});
|
|
1252
|
+
try {
|
|
1253
|
+
await copyDatabase(cms.db, target);
|
|
1254
|
+
if (!target.backup) throw new EasyCMSError("backups.sqlite must be the SQLite adapter", 400);
|
|
1255
|
+
await target.backup(file);
|
|
1256
|
+
} finally {
|
|
1257
|
+
await target.destroy();
|
|
1258
|
+
for (const suffix of ["", "-wal", "-shm"])
|
|
1259
|
+
await rm2(`${scratch}${suffix}`, { force: true }).catch(() => {
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
var slug = (text) => text.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "easy-cms";
|
|
1264
|
+
function filenameFor(cms, date) {
|
|
1265
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1266
|
+
const stamp = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}`;
|
|
1267
|
+
return `${slug(cms.config.admin.brand.name || "easy-cms")}-${stamp}.db.gz`;
|
|
1268
|
+
}
|
|
1269
|
+
function toAdmin(row) {
|
|
1270
|
+
const text = (value) => typeof value === "string" && value !== "" ? value : null;
|
|
1271
|
+
return {
|
|
1272
|
+
id: row.id,
|
|
1273
|
+
state: ["pending", "running", "done", "failed"].includes(row.state) ? row.state : "failed",
|
|
1274
|
+
trigger: row.trigger === "scheduled" ? "scheduled" : "manual",
|
|
1275
|
+
filename: text(row.filename),
|
|
1276
|
+
size: typeof row.size === "number" ? row.size : null,
|
|
1277
|
+
startedAt: text(row.startedAt),
|
|
1278
|
+
finishedAt: text(row.finishedAt),
|
|
1279
|
+
error: text(row.error),
|
|
1280
|
+
author: text(row.author),
|
|
1281
|
+
downloadedBy: text(row.downloadedBy),
|
|
1282
|
+
downloadedAt: text(row.downloadedAt)
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
async function update(cms, row, data) {
|
|
1286
|
+
const { id, ...rest } = row;
|
|
1287
|
+
const next = { ...rest, ...data, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1288
|
+
await cms.db.update({ collection: DATABASE_BACKUPS, id, data: next });
|
|
1289
|
+
return { ...row, ...data };
|
|
1290
|
+
}
|
|
1291
|
+
async function listBackups(cms) {
|
|
1292
|
+
const config = cms.config.backups;
|
|
1293
|
+
const storage = await backupStorage(cms);
|
|
1294
|
+
const { docs } = await cms.db.find({
|
|
1295
|
+
collection: DATABASE_BACKUPS,
|
|
1296
|
+
sort: ["-createdAt"],
|
|
1297
|
+
limit: 0,
|
|
1298
|
+
page: 1
|
|
1299
|
+
});
|
|
1300
|
+
return {
|
|
1301
|
+
settings: {
|
|
1302
|
+
every: config?.every ?? null,
|
|
1303
|
+
at: config?.at ?? DEFAULT_BACKUP_TIME,
|
|
1304
|
+
keep: config?.keep ?? DEFAULT_BACKUP_KEEP,
|
|
1305
|
+
storage: storage.name,
|
|
1306
|
+
dir: config?.storage ? null : config?.dir ?? DEFAULT_DIR,
|
|
1307
|
+
database: cms.config.db.name,
|
|
1308
|
+
unavailable: backupUnavailable(cms)
|
|
1309
|
+
},
|
|
1310
|
+
backups: docs.map(toAdmin)
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
async function inProgress(cms, now) {
|
|
1314
|
+
const { docs } = await cms.db.find({
|
|
1315
|
+
collection: DATABASE_BACKUPS,
|
|
1316
|
+
where: { state: { in: ["pending", "running"] } },
|
|
1317
|
+
sort: ["-createdAt"],
|
|
1318
|
+
limit: 10,
|
|
1319
|
+
page: 1
|
|
1320
|
+
});
|
|
1321
|
+
return docs.find((row) => now - Date.parse(String(row.createdAt)) < STALE);
|
|
1322
|
+
}
|
|
1323
|
+
async function startBackup(cms, trigger, author) {
|
|
1324
|
+
const unavailable = backupUnavailable(cms);
|
|
1325
|
+
if (unavailable) throw new EasyCMSError(unavailable, 400);
|
|
1326
|
+
const now = /* @__PURE__ */ new Date();
|
|
1327
|
+
if (await inProgress(cms, now.getTime()))
|
|
1328
|
+
throw new EasyCMSError("A backup is already running", 409);
|
|
1329
|
+
const stamp = now.toISOString();
|
|
1330
|
+
const row = await cms.db.create({
|
|
1331
|
+
collection: DATABASE_BACKUPS,
|
|
1332
|
+
data: {
|
|
1333
|
+
state: "pending",
|
|
1334
|
+
trigger,
|
|
1335
|
+
filename: null,
|
|
1336
|
+
size: null,
|
|
1337
|
+
startedAt: null,
|
|
1338
|
+
finishedAt: null,
|
|
1339
|
+
error: null,
|
|
1340
|
+
author: author ?? null,
|
|
1341
|
+
downloadedBy: null,
|
|
1342
|
+
downloadedAt: null,
|
|
1343
|
+
createdAt: stamp,
|
|
1344
|
+
updatedAt: stamp
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
void runBackup(cms, row).catch(
|
|
1348
|
+
(error) => cms.logger.error(`Backup failed: ${error.message}`)
|
|
1349
|
+
);
|
|
1350
|
+
return toAdmin(row);
|
|
1351
|
+
}
|
|
1352
|
+
async function runBackup(cms, record) {
|
|
1353
|
+
const started = /* @__PURE__ */ new Date();
|
|
1354
|
+
let row = await update(cms, record, { state: "running", startedAt: started.toISOString() });
|
|
1355
|
+
const dir = await mkdtemp(join2(tmpdir(), "easy-cms-backup-"));
|
|
1356
|
+
try {
|
|
1357
|
+
const file = join2(dir, "backup.db");
|
|
1358
|
+
await writeBackupFile(cms, file);
|
|
1359
|
+
const data = new Uint8Array(await gzipAsync(await readFile2(file)));
|
|
1360
|
+
const storage = await backupStorage(cms);
|
|
1361
|
+
const filename = filenameFor(cms, started);
|
|
1362
|
+
const key2 = await storage.get(filename) ? filename.replace(/\.db\.gz$/, `-${row.id}.db.gz`) : filename;
|
|
1363
|
+
await storage.put(key2, data, { contentType: "application/gzip" });
|
|
1364
|
+
row = await update(cms, row, {
|
|
1365
|
+
state: "done",
|
|
1366
|
+
filename: key2,
|
|
1367
|
+
size: data.byteLength,
|
|
1368
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1369
|
+
});
|
|
1370
|
+
cms.logger.info(`Backup ${key2} written (${data.byteLength} bytes)`);
|
|
1371
|
+
await pruneBackups(cms);
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
await update(cms, row, {
|
|
1374
|
+
state: "failed",
|
|
1375
|
+
error: (error.message || String(error)).slice(0, 500),
|
|
1376
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1377
|
+
});
|
|
1378
|
+
throw error;
|
|
1379
|
+
} finally {
|
|
1380
|
+
await rm2(dir, { recursive: true, force: true });
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
async function pruneBackups(cms) {
|
|
1384
|
+
const keep = cms.config.backups?.keep ?? DEFAULT_BACKUP_KEEP;
|
|
1385
|
+
const storage = await backupStorage(cms);
|
|
1386
|
+
const { docs } = await cms.db.find({
|
|
1387
|
+
collection: DATABASE_BACKUPS,
|
|
1388
|
+
where: { state: { equals: "done" } },
|
|
1389
|
+
sort: ["-createdAt"],
|
|
1390
|
+
limit: 0,
|
|
1391
|
+
page: 1
|
|
1392
|
+
});
|
|
1393
|
+
for (const row of docs.slice(keep)) {
|
|
1394
|
+
if (typeof row.filename === "string") await storage.delete(row.filename).catch(() => {
|
|
1395
|
+
});
|
|
1396
|
+
await cms.db.delete({ collection: DATABASE_BACKUPS, id: row.id });
|
|
1397
|
+
}
|
|
1398
|
+
const old = new Date(Date.now() - 30 * 864e5).toISOString();
|
|
1399
|
+
const failed = await cms.db.find({
|
|
1400
|
+
collection: DATABASE_BACKUPS,
|
|
1401
|
+
where: { and: [{ state: { equals: "failed" } }, { createdAt: { lt: old } }] },
|
|
1402
|
+
sort: ["createdAt"],
|
|
1403
|
+
limit: 100,
|
|
1404
|
+
page: 1
|
|
1405
|
+
});
|
|
1406
|
+
for (const row of failed.docs) await cms.db.delete({ collection: DATABASE_BACKUPS, id: row.id });
|
|
1407
|
+
}
|
|
1408
|
+
function lastSlot(now, at) {
|
|
1409
|
+
const [hours2, minutes] = at.split(":").map(Number);
|
|
1410
|
+
const slot = new Date(now);
|
|
1411
|
+
slot.setHours(hours2, minutes, 0, 0);
|
|
1412
|
+
if (slot.getTime() > now.getTime()) slot.setDate(slot.getDate() - 1);
|
|
1413
|
+
return slot;
|
|
1414
|
+
}
|
|
1415
|
+
async function runDueBackups(cms, now = /* @__PURE__ */ new Date()) {
|
|
1416
|
+
if (backupUnavailable(cms)) return;
|
|
1417
|
+
const { docs } = await cms.db.find({
|
|
1418
|
+
collection: DATABASE_BACKUPS,
|
|
1419
|
+
where: { state: { in: ["pending", "running"] } },
|
|
1420
|
+
sort: ["createdAt"],
|
|
1421
|
+
limit: 10,
|
|
1422
|
+
page: 1
|
|
1423
|
+
});
|
|
1424
|
+
for (const row of docs) {
|
|
1425
|
+
const age = now.getTime() - Date.parse(String(row.updatedAt ?? row.createdAt));
|
|
1426
|
+
if (age < STALE) continue;
|
|
1427
|
+
if (row.state === "pending") await runBackup(cms, row).catch(() => {
|
|
1428
|
+
});
|
|
1429
|
+
else
|
|
1430
|
+
await update(cms, row, {
|
|
1431
|
+
state: "failed",
|
|
1432
|
+
error: "Interrupted: the server stopped during the backup",
|
|
1433
|
+
finishedAt: now.toISOString()
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
const every = cms.config.backups?.every;
|
|
1437
|
+
if (!every) return;
|
|
1438
|
+
const slot = lastSlot(now, cms.config.backups?.at ?? DEFAULT_BACKUP_TIME);
|
|
1439
|
+
const latest = await cms.db.find({
|
|
1440
|
+
collection: DATABASE_BACKUPS,
|
|
1441
|
+
where: { trigger: { equals: "scheduled" } },
|
|
1442
|
+
sort: ["-createdAt"],
|
|
1443
|
+
limit: 1,
|
|
1444
|
+
page: 1
|
|
1445
|
+
});
|
|
1446
|
+
const last = latest.docs[0] ? Date.parse(String(latest.docs[0].createdAt)) : 0;
|
|
1447
|
+
const due = every === "day" ? last < slot.getTime() : last < slot.getTime() - PERIOD.week + PERIOD.day;
|
|
1448
|
+
if (!due) return;
|
|
1449
|
+
if (await inProgress(cms, now.getTime())) return;
|
|
1450
|
+
await startBackup(cms, "scheduled").catch(
|
|
1451
|
+
(error) => cms.logger.error(`Scheduled backup could not start: ${error.message}`)
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
async function backupAttention(cms, now = Date.now()) {
|
|
1455
|
+
const every = cms.config.backups?.every;
|
|
1456
|
+
if (!every) return null;
|
|
1457
|
+
const [scheduled, done] = await Promise.all([
|
|
1458
|
+
cms.db.find({
|
|
1459
|
+
collection: DATABASE_BACKUPS,
|
|
1460
|
+
where: { trigger: { equals: "scheduled" } },
|
|
1461
|
+
sort: ["-createdAt"],
|
|
1462
|
+
limit: 1,
|
|
1463
|
+
page: 1
|
|
1464
|
+
}),
|
|
1465
|
+
cms.db.find({
|
|
1466
|
+
collection: DATABASE_BACKUPS,
|
|
1467
|
+
where: { state: { equals: "done" } },
|
|
1468
|
+
sort: ["-createdAt"],
|
|
1469
|
+
limit: 1,
|
|
1470
|
+
page: 1
|
|
1471
|
+
})
|
|
1472
|
+
]);
|
|
1473
|
+
const failed = scheduled.docs[0]?.state === "failed";
|
|
1474
|
+
const lastDone = done.docs[0] ? String(done.docs[0].createdAt) : null;
|
|
1475
|
+
let since = lastDone ? Date.parse(lastDone) : void 0;
|
|
1476
|
+
if (since === void 0) {
|
|
1477
|
+
const first = await cms.db.find({ collection: USERS, sort: ["createdAt"], limit: 1, page: 1 });
|
|
1478
|
+
since = first.docs[0] ? Date.parse(String(first.docs[0].createdAt)) : now;
|
|
1479
|
+
}
|
|
1480
|
+
const overdue = now - since > 2 * PERIOD[every];
|
|
1481
|
+
return failed || overdue ? { id: "backups", failed, lastDone } : null;
|
|
1482
|
+
}
|
|
1483
|
+
async function downloadBackup(cms, id, by) {
|
|
1484
|
+
const row = await cms.db.findById({ collection: DATABASE_BACKUPS, id });
|
|
1485
|
+
if (row?.state !== "done" || typeof row.filename !== "string")
|
|
1486
|
+
throw new NotFoundError(DATABASE_BACKUPS, id);
|
|
1487
|
+
const file = await (await backupStorage(cms)).get(row.filename);
|
|
1488
|
+
if (!file)
|
|
1489
|
+
throw new EasyCMSError(`The file ${row.filename} is no longer in the backup storage`, 404);
|
|
1490
|
+
await update(cms, row, { downloadedBy: by, downloadedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1491
|
+
return { filename: row.filename, body: file.body };
|
|
1492
|
+
}
|
|
1493
|
+
async function deleteBackup(cms, id) {
|
|
1494
|
+
const row = await cms.db.findById({ collection: DATABASE_BACKUPS, id });
|
|
1495
|
+
if (!row) throw new NotFoundError(DATABASE_BACKUPS, id);
|
|
1496
|
+
if (row.state === "running" || row.state === "pending")
|
|
1497
|
+
throw new EasyCMSError("A backup in progress cannot be deleted", 409);
|
|
1498
|
+
if (typeof row.filename === "string")
|
|
1499
|
+
await (await backupStorage(cms)).delete(row.filename).catch(() => {
|
|
1500
|
+
});
|
|
1501
|
+
await cms.db.delete({ collection: DATABASE_BACKUPS, id });
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1092
1504
|
// src/config.ts
|
|
1093
1505
|
var ADMIN_ICONS = [
|
|
1094
1506
|
"file-text",
|
|
@@ -1175,44 +1587,6 @@ function configSignature(config) {
|
|
|
1175
1587
|
}
|
|
1176
1588
|
var signatures = /* @__PURE__ */ new WeakMap();
|
|
1177
1589
|
|
|
1178
|
-
// src/copy.ts
|
|
1179
|
-
var BATCH = 200;
|
|
1180
|
-
async function copyDatabase(source, target, options = {}) {
|
|
1181
|
-
const from = source.transfer;
|
|
1182
|
-
const to = target.transfer;
|
|
1183
|
-
if (!from || !to) throw new EasyCMSError("This database adapter cannot copy databases", 400);
|
|
1184
|
-
if (from.schemaHash !== to.schemaHash) {
|
|
1185
|
-
throw new EasyCMSError(
|
|
1186
|
-
"The two configs describe different collections or fields. Copy between configs with the same collections (import one config into the other and change only `db`).",
|
|
1187
|
-
400
|
|
1188
|
-
);
|
|
1189
|
-
}
|
|
1190
|
-
const tables = from.tables();
|
|
1191
|
-
for (const table of tables) {
|
|
1192
|
-
if (await to.count(table) > 0) {
|
|
1193
|
-
throw new EasyCMSError(
|
|
1194
|
-
`The target database is not empty ("${table}" has rows). Copy into a new, empty database.`,
|
|
1195
|
-
400
|
|
1196
|
-
);
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
let total = 0;
|
|
1200
|
-
for (const table of tables) {
|
|
1201
|
-
let copied = 0;
|
|
1202
|
-
for (; ; ) {
|
|
1203
|
-
const rows = await from.read(table, copied, BATCH);
|
|
1204
|
-
if (rows.length === 0) break;
|
|
1205
|
-
await to.write(table, rows);
|
|
1206
|
-
copied += rows.length;
|
|
1207
|
-
if (rows.length < BATCH) break;
|
|
1208
|
-
}
|
|
1209
|
-
total += copied;
|
|
1210
|
-
options.onTable?.({ table, rows: copied });
|
|
1211
|
-
}
|
|
1212
|
-
await to.finish();
|
|
1213
|
-
return { tables: tables.length, rows: total };
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
1590
|
// src/document.ts
|
|
1217
1591
|
import { randomUUID } from "crypto";
|
|
1218
1592
|
var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -1327,7 +1701,7 @@ function parseId(value) {
|
|
|
1327
1701
|
async function validateFields(fields, input, options, prefix = "") {
|
|
1328
1702
|
const data = {};
|
|
1329
1703
|
const errors = [];
|
|
1330
|
-
const
|
|
1704
|
+
const references2 = [];
|
|
1331
1705
|
for (const field of fields) {
|
|
1332
1706
|
const path = prefix + field.name;
|
|
1333
1707
|
const raw = input[field.name];
|
|
@@ -1345,7 +1719,7 @@ async function validateFields(fields, input, options, prefix = "") {
|
|
|
1345
1719
|
);
|
|
1346
1720
|
data[field.name] = sub.data;
|
|
1347
1721
|
errors.push(...sub.errors);
|
|
1348
|
-
|
|
1722
|
+
references2.push(...sub.references);
|
|
1349
1723
|
continue;
|
|
1350
1724
|
}
|
|
1351
1725
|
const localization = options.localization;
|
|
@@ -1375,7 +1749,7 @@ async function validateFields(fields, input, options, prefix = "") {
|
|
|
1375
1749
|
options,
|
|
1376
1750
|
failLocale,
|
|
1377
1751
|
errors,
|
|
1378
|
-
|
|
1752
|
+
references2
|
|
1379
1753
|
);
|
|
1380
1754
|
if (normalized === void 0) continue;
|
|
1381
1755
|
out[locale] = normalized;
|
|
@@ -1392,12 +1766,12 @@ async function validateFields(fields, input, options, prefix = "") {
|
|
|
1392
1766
|
if (!field.required) await runCustom(field, null, options, fail);
|
|
1393
1767
|
continue;
|
|
1394
1768
|
}
|
|
1395
|
-
const value = await normalizeValue(field, raw, path, options, fail, errors,
|
|
1769
|
+
const value = await normalizeValue(field, raw, path, options, fail, errors, references2);
|
|
1396
1770
|
if (value === void 0) continue;
|
|
1397
1771
|
data[field.name] = value;
|
|
1398
1772
|
await runCustom(field, value, options, fail);
|
|
1399
1773
|
}
|
|
1400
|
-
return { data, errors, references };
|
|
1774
|
+
return { data, errors, references: references2 };
|
|
1401
1775
|
}
|
|
1402
1776
|
async function runCustom(field, value, options, fail) {
|
|
1403
1777
|
if (!field.validate) return;
|
|
@@ -1405,7 +1779,7 @@ async function runCustom(field, value, options, fail) {
|
|
|
1405
1779
|
const result = await validate(value, { data: options.root, operation: options.operation });
|
|
1406
1780
|
if (result !== true) fail(typeof result === "string" ? result : "is invalid");
|
|
1407
1781
|
}
|
|
1408
|
-
async function normalizeValue(field, raw, path, options, fail, errors,
|
|
1782
|
+
async function normalizeValue(field, raw, path, options, fail, errors, references2) {
|
|
1409
1783
|
switch (field.type) {
|
|
1410
1784
|
case "text":
|
|
1411
1785
|
case "textarea":
|
|
@@ -1467,7 +1841,7 @@ async function normalizeValue(field, raw, path, options, fail, errors, reference
|
|
|
1467
1841
|
if (id === void 0)
|
|
1468
1842
|
return fail(`must be ${many ? "ids" : "an id"} of "${target}" documents`);
|
|
1469
1843
|
ids.push(id);
|
|
1470
|
-
|
|
1844
|
+
references2.push({
|
|
1471
1845
|
field: path,
|
|
1472
1846
|
collection: target,
|
|
1473
1847
|
id,
|
|
@@ -1491,19 +1865,19 @@ async function normalizeValue(field, raw, path, options, fail, errors, reference
|
|
|
1491
1865
|
}
|
|
1492
1866
|
const sub = await validateFields(field.fields, row, options, `${path}.${i}.`);
|
|
1493
1867
|
errors.push(...sub.errors);
|
|
1494
|
-
|
|
1868
|
+
references2.push(...sub.references);
|
|
1495
1869
|
const id = typeof row.id === "string" && row.id !== "" ? row.id : randomUUID();
|
|
1496
1870
|
rows.push({ id, ...sub.data });
|
|
1497
1871
|
}
|
|
1498
1872
|
return rows;
|
|
1499
1873
|
}
|
|
1500
1874
|
case "blocks":
|
|
1501
|
-
return normalizeBlocks(field, raw, path, options, fail, errors,
|
|
1875
|
+
return normalizeBlocks(field, raw, path, options, fail, errors, references2);
|
|
1502
1876
|
case "group":
|
|
1503
1877
|
return raw;
|
|
1504
1878
|
}
|
|
1505
1879
|
}
|
|
1506
|
-
async function normalizeBlocks(field, raw, path, options, fail, errors,
|
|
1880
|
+
async function normalizeBlocks(field, raw, path, options, fail, errors, references2) {
|
|
1507
1881
|
if (!Array.isArray(raw)) return fail("must be an array of blocks");
|
|
1508
1882
|
if (field.minRows !== void 0 && raw.length < field.minRows)
|
|
1509
1883
|
return fail(`must have at least ${field.minRows} blocks`);
|
|
@@ -1521,7 +1895,7 @@ async function normalizeBlocks(field, raw, path, options, fail, errors, referenc
|
|
|
1521
1895
|
}
|
|
1522
1896
|
const sub = await validateFields(fields, row, options, `${path}.${i}.`);
|
|
1523
1897
|
errors.push(...sub.errors);
|
|
1524
|
-
|
|
1898
|
+
references2.push(...sub.references);
|
|
1525
1899
|
const id = typeof row.id === "string" && row.id !== "" ? row.id : randomUUID();
|
|
1526
1900
|
rows.push({ id, blockType: row.blockType, ...sub.data });
|
|
1527
1901
|
}
|
|
@@ -1779,32 +2153,513 @@ function applyFieldTypes(config) {
|
|
|
1779
2153
|
}
|
|
1780
2154
|
} : {}
|
|
1781
2155
|
};
|
|
1782
|
-
});
|
|
1783
|
-
return {
|
|
1784
|
-
config: {
|
|
1785
|
-
...config,
|
|
1786
|
-
...config.collections ? {
|
|
1787
|
-
collections: config.collections.map((c, i) => ({
|
|
1788
|
-
...c,
|
|
1789
|
-
fields: convert(c.fields, `collections[${i}].fields`)
|
|
1790
|
-
}))
|
|
1791
|
-
} : {},
|
|
1792
|
-
...config.globals ? {
|
|
1793
|
-
globals: config.globals.map((g, i) => ({
|
|
1794
|
-
...g,
|
|
1795
|
-
fields: convert(g.fields, `globals[${i}].fields`)
|
|
1796
|
-
}))
|
|
1797
|
-
} : {}
|
|
1798
|
-
},
|
|
1799
|
-
issues
|
|
1800
|
-
};
|
|
2156
|
+
});
|
|
2157
|
+
return {
|
|
2158
|
+
config: {
|
|
2159
|
+
...config,
|
|
2160
|
+
...config.collections ? {
|
|
2161
|
+
collections: config.collections.map((c, i) => ({
|
|
2162
|
+
...c,
|
|
2163
|
+
fields: convert(c.fields, `collections[${i}].fields`)
|
|
2164
|
+
}))
|
|
2165
|
+
} : {},
|
|
2166
|
+
...config.globals ? {
|
|
2167
|
+
globals: config.globals.map((g, i) => ({
|
|
2168
|
+
...g,
|
|
2169
|
+
fields: convert(g.fields, `globals[${i}].fields`)
|
|
2170
|
+
}))
|
|
2171
|
+
} : {}
|
|
2172
|
+
},
|
|
2173
|
+
issues
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// src/load-config.ts
|
|
2178
|
+
import { existsSync } from "fs";
|
|
2179
|
+
import { resolve as resolve2 } from "path";
|
|
2180
|
+
import { createJiti } from "jiti";
|
|
2181
|
+
|
|
2182
|
+
// src/versions.ts
|
|
2183
|
+
var DEFAULT_MAX_VERSIONS = 50;
|
|
2184
|
+
function versionLimit(config) {
|
|
2185
|
+
const versions = config.versions;
|
|
2186
|
+
if (!versions) return void 0;
|
|
2187
|
+
return versions === true ? DEFAULT_MAX_VERSIONS : versions.max ?? DEFAULT_MAX_VERSIONS;
|
|
2188
|
+
}
|
|
2189
|
+
var collectionParent = (slug2) => slug2;
|
|
2190
|
+
var globalParent = (slug2) => `global:${slug2}`;
|
|
2191
|
+
function snapshotOf(fields, doc) {
|
|
2192
|
+
const hidden = new Set(fields.filter((f) => f.hidden).map((f) => f.name));
|
|
2193
|
+
const snapshot = {};
|
|
2194
|
+
for (const [key2, value] of Object.entries(doc)) {
|
|
2195
|
+
if (key2 === "id" || key2 === "createdAt" || key2 === "updatedAt" || hidden.has(key2)) continue;
|
|
2196
|
+
snapshot[key2] = value;
|
|
2197
|
+
}
|
|
2198
|
+
return snapshot;
|
|
2199
|
+
}
|
|
2200
|
+
var toSummary = (row) => ({
|
|
2201
|
+
id: row.id,
|
|
2202
|
+
status: row.status ?? null,
|
|
2203
|
+
latest: row.latest === true,
|
|
2204
|
+
author: row.author ?? null,
|
|
2205
|
+
createdAt: String(row.createdAt)
|
|
2206
|
+
});
|
|
2207
|
+
var VersionStore = class {
|
|
2208
|
+
constructor(db) {
|
|
2209
|
+
this.db = db;
|
|
2210
|
+
}
|
|
2211
|
+
db;
|
|
2212
|
+
/** Saves a new latest version and deletes the oldest beyond `max`. */
|
|
2213
|
+
async save(args) {
|
|
2214
|
+
const { parent, doc } = args;
|
|
2215
|
+
const previous = await this.db.find({
|
|
2216
|
+
collection: VERSIONS,
|
|
2217
|
+
where: { and: [...this.of(parent, doc), { latest: { equals: true } }] },
|
|
2218
|
+
sort: [],
|
|
2219
|
+
limit: 0,
|
|
2220
|
+
page: 1
|
|
2221
|
+
});
|
|
2222
|
+
for (const row of previous.docs) {
|
|
2223
|
+
await this.db.update({ collection: VERSIONS, id: row.id, data: { ...row, latest: false } });
|
|
2224
|
+
}
|
|
2225
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2226
|
+
const created = await this.db.create({
|
|
2227
|
+
collection: VERSIONS,
|
|
2228
|
+
data: {
|
|
2229
|
+
parent,
|
|
2230
|
+
doc,
|
|
2231
|
+
status: args.status,
|
|
2232
|
+
latest: true,
|
|
2233
|
+
author: args.author,
|
|
2234
|
+
snapshot: args.snapshot,
|
|
2235
|
+
createdAt: now,
|
|
2236
|
+
updatedAt: now
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
await this.prune(parent, doc, args.max);
|
|
2240
|
+
return toSummary(created);
|
|
2241
|
+
}
|
|
2242
|
+
/** The latest version of each document, keyed by document id. */
|
|
2243
|
+
async latest(parent, docs) {
|
|
2244
|
+
const result = /* @__PURE__ */ new Map();
|
|
2245
|
+
if (docs.length === 0) return result;
|
|
2246
|
+
const rows = await this.db.find({
|
|
2247
|
+
collection: VERSIONS,
|
|
2248
|
+
where: {
|
|
2249
|
+
and: [
|
|
2250
|
+
{ parent: { equals: parent } },
|
|
2251
|
+
{ doc: { in: [...docs] } },
|
|
2252
|
+
{ latest: { equals: true } }
|
|
2253
|
+
]
|
|
2254
|
+
},
|
|
2255
|
+
sort: [],
|
|
2256
|
+
limit: 0,
|
|
2257
|
+
page: 1
|
|
2258
|
+
});
|
|
2259
|
+
for (const row of rows.docs) result.set(String(row.doc), this.toVersion(row));
|
|
2260
|
+
return result;
|
|
2261
|
+
}
|
|
2262
|
+
async list(parent, doc, limit, page) {
|
|
2263
|
+
const found = await this.db.find({
|
|
2264
|
+
collection: VERSIONS,
|
|
2265
|
+
where: { and: this.of(parent, doc) },
|
|
2266
|
+
sort: ["-id"],
|
|
2267
|
+
limit,
|
|
2268
|
+
page
|
|
2269
|
+
});
|
|
2270
|
+
return { ...found, docs: found.docs.map(toSummary) };
|
|
2271
|
+
}
|
|
2272
|
+
async get(parent, doc, id) {
|
|
2273
|
+
const found = await this.db.find({
|
|
2274
|
+
collection: VERSIONS,
|
|
2275
|
+
where: { and: [...this.of(parent, doc), { id: { equals: id } }] },
|
|
2276
|
+
sort: [],
|
|
2277
|
+
limit: 1,
|
|
2278
|
+
page: 1
|
|
2279
|
+
});
|
|
2280
|
+
const row = found.docs[0];
|
|
2281
|
+
return row ? this.toVersion(row) : null;
|
|
2282
|
+
}
|
|
2283
|
+
async deleteAll(parent, doc) {
|
|
2284
|
+
const rows = await this.db.find({
|
|
2285
|
+
collection: VERSIONS,
|
|
2286
|
+
where: { and: this.of(parent, doc) },
|
|
2287
|
+
sort: [],
|
|
2288
|
+
limit: 0,
|
|
2289
|
+
page: 1
|
|
2290
|
+
});
|
|
2291
|
+
for (const row of rows.docs) await this.db.delete({ collection: VERSIONS, id: row.id });
|
|
2292
|
+
}
|
|
2293
|
+
async prune(parent, doc, max) {
|
|
2294
|
+
const rows = await this.db.find({
|
|
2295
|
+
collection: VERSIONS,
|
|
2296
|
+
where: { and: this.of(parent, doc) },
|
|
2297
|
+
sort: ["-id"],
|
|
2298
|
+
limit: 0,
|
|
2299
|
+
page: 1
|
|
2300
|
+
});
|
|
2301
|
+
for (const row of rows.docs.slice(max)) {
|
|
2302
|
+
await this.db.delete({ collection: VERSIONS, id: row.id });
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
of(parent, doc) {
|
|
2306
|
+
return [{ parent: { equals: parent } }, { doc: { equals: doc } }];
|
|
2307
|
+
}
|
|
2308
|
+
toVersion(row) {
|
|
2309
|
+
return { ...toSummary(row), data: row.snapshot ?? {} };
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
|
|
2313
|
+
// src/roles.ts
|
|
2314
|
+
var ROLE_OPERATIONS = ["read", "create", "update", "delete", "publish"];
|
|
2315
|
+
var GLOBAL_OPERATIONS = ["read", "update", "publish"];
|
|
2316
|
+
var KEY = /^[a-z][a-z0-9-]{0,31}$/;
|
|
2317
|
+
var HISTORY = 50;
|
|
2318
|
+
var TTL = 3e4;
|
|
2319
|
+
var rolesCollection = {
|
|
2320
|
+
slug: ROLES,
|
|
2321
|
+
access: { read: () => false, create: () => false, update: () => false, delete: () => false },
|
|
2322
|
+
fields: [
|
|
2323
|
+
{ name: "key", type: "text", required: true, unique: true },
|
|
2324
|
+
{ name: "name", type: "text" },
|
|
2325
|
+
{ name: "permissions", type: "json" }
|
|
2326
|
+
]
|
|
2327
|
+
};
|
|
2328
|
+
function governed(slug2) {
|
|
2329
|
+
return !INTERNAL_COLLECTIONS.has(slug2) && slug2 !== API_KEYS;
|
|
2330
|
+
}
|
|
2331
|
+
function references(fields, out = /* @__PURE__ */ new Set()) {
|
|
2332
|
+
for (const field of fields) {
|
|
2333
|
+
if (field.type === "relationship") out.add(field.to);
|
|
2334
|
+
else if (field.type === "upload") out.add("media");
|
|
2335
|
+
else if (field.type === "group" || field.type === "array") references(field.fields, out);
|
|
2336
|
+
else if (field.type === "blocks") for (const b of field.blocks) references(b.fields, out);
|
|
2337
|
+
}
|
|
2338
|
+
return out;
|
|
2339
|
+
}
|
|
2340
|
+
var viewId = {
|
|
2341
|
+
page: (path) => `page:${path}`,
|
|
2342
|
+
widget: (tag) => `widget:${tag}`
|
|
2343
|
+
};
|
|
2344
|
+
var tagOf = (component) => typeof component === "string" ? component : component.tag;
|
|
2345
|
+
var Roles = class {
|
|
2346
|
+
constructor(config, db) {
|
|
2347
|
+
this.config = config;
|
|
2348
|
+
this.db = db;
|
|
2349
|
+
this.enabled = config.auth.rbac;
|
|
2350
|
+
this.versions = new VersionStore(db);
|
|
2351
|
+
}
|
|
2352
|
+
config;
|
|
2353
|
+
db;
|
|
2354
|
+
enabled;
|
|
2355
|
+
versions;
|
|
2356
|
+
cache;
|
|
2357
|
+
synced;
|
|
2358
|
+
/**
|
|
2359
|
+
* Whether the user's role allows an operation: `true`, `false`, or for their own account
|
|
2360
|
+
* (users without read or update on `users`) a constraint to it.
|
|
2361
|
+
*/
|
|
2362
|
+
async allows(user, target, operation) {
|
|
2363
|
+
if (!this.enabled || !user || user.role === "admin") return true;
|
|
2364
|
+
if ("collection" in target && !governed(target.collection)) return true;
|
|
2365
|
+
const permissions = await this.permissionsOf(user.role);
|
|
2366
|
+
const ops = "collection" in target ? permissions.collections?.[target.collection] : permissions.globals?.[target.global];
|
|
2367
|
+
if (ops?.includes(operation)) return true;
|
|
2368
|
+
if ("collection" in target && target.collection === USERS) {
|
|
2369
|
+
if (operation === "read" || operation === "update") return { id: { equals: user.id } };
|
|
2370
|
+
}
|
|
2371
|
+
return false;
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Whether the user may open an admin page: admins always; with roles, those whose role has
|
|
2375
|
+
* it; without roles, nobody else (`status`, `deliveries`) or everybody (plugin pages).
|
|
2376
|
+
*/
|
|
2377
|
+
async canView(user, id) {
|
|
2378
|
+
if (user.role === "admin" && !user.apiKey) return true;
|
|
2379
|
+
if (user.apiKey) return false;
|
|
2380
|
+
if (!this.enabled) return id.startsWith("page:") || id.startsWith("widget:");
|
|
2381
|
+
return (await this.permissionsOf(user.role)).admin?.includes(id) ?? false;
|
|
2382
|
+
}
|
|
2383
|
+
/** Forgets cached permissions, after a role changed. */
|
|
2384
|
+
invalidate() {
|
|
2385
|
+
this.cache = void 0;
|
|
2386
|
+
}
|
|
2387
|
+
/**
|
|
2388
|
+
* Adds the roles of `auth.roles` that are missing; on the first start, gives them everything.
|
|
2389
|
+
* Runs once, on first use (not on startup: `easy-cms migrate` opens the CMS before the table exists).
|
|
2390
|
+
*/
|
|
2391
|
+
sync() {
|
|
2392
|
+
this.synced ??= this.addMissing().catch((error) => {
|
|
2393
|
+
this.synced = void 0;
|
|
2394
|
+
throw error;
|
|
2395
|
+
});
|
|
2396
|
+
return this.synced;
|
|
2397
|
+
}
|
|
2398
|
+
async addMissing() {
|
|
2399
|
+
if (!this.enabled) return;
|
|
2400
|
+
const rows = await this.rows();
|
|
2401
|
+
const first = rows.length === 0;
|
|
2402
|
+
const known = new Set(rows.map((r) => String(r.key)));
|
|
2403
|
+
for (const key2 of this.config.auth.roles) {
|
|
2404
|
+
if (known.has(key2)) continue;
|
|
2405
|
+
try {
|
|
2406
|
+
await this.insert(key2, "", first && key2 !== "admin" ? this.everything() : {}, null);
|
|
2407
|
+
} catch (error) {
|
|
2408
|
+
if (!(await this.rows()).some((r) => r.key === key2)) throw error;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
this.invalidate();
|
|
2412
|
+
}
|
|
2413
|
+
/** Whether a role exists (users can only be given existing roles). */
|
|
2414
|
+
async exists(key2) {
|
|
2415
|
+
if (!this.enabled) return this.config.auth.roles.includes(key2);
|
|
2416
|
+
return (await this.load()).has(key2);
|
|
2417
|
+
}
|
|
2418
|
+
/** Roles a user can be given, those of `auth.roles` first; `name` may be empty. */
|
|
2419
|
+
async options() {
|
|
2420
|
+
if (!this.enabled) return this.config.auth.roles.map((key2) => ({ key: key2, name: "" }));
|
|
2421
|
+
await this.sync();
|
|
2422
|
+
return (await this.rows()).map((row) => ({ key: String(row.key), name: typeof row.name === "string" ? row.name : "" })).sort((a, b) => rank(a, this.config) - rank(b, this.config) || a.key.localeCompare(b.key));
|
|
2423
|
+
}
|
|
2424
|
+
// --- Settings → Roles ----------------------------------------------------
|
|
2425
|
+
async list() {
|
|
2426
|
+
await this.sync();
|
|
2427
|
+
const rows = await this.rows();
|
|
2428
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2429
|
+
for (const row of rows) {
|
|
2430
|
+
counts.set(
|
|
2431
|
+
String(row.key),
|
|
2432
|
+
await this.db.count({ collection: USERS, where: { role: { equals: row.key } } })
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
const roles = rows.map((row) => this.toRole(row, counts.get(String(row.key)) ?? 0)).sort((a, b) => rank(a, this.config) - rank(b, this.config) || a.key.localeCompare(b.key));
|
|
2436
|
+
const decided = roles.filter((r) => r.key !== "admin").map((r) => r.permissions);
|
|
2437
|
+
const seen = (group, slug2) => decided.some((p) => p[group]?.[slug2] !== void 0);
|
|
2438
|
+
return {
|
|
2439
|
+
roles,
|
|
2440
|
+
collections: this.config.collections.filter((c) => governed(c.slug)).map((c) => ({
|
|
2441
|
+
slug: c.slug,
|
|
2442
|
+
ops: ROLE_OPERATIONS.filter((op) => op !== "publish" || c.drafts === true),
|
|
2443
|
+
new: !seen("collections", c.slug),
|
|
2444
|
+
references: [...references(c.fields)].filter((s) => s !== c.slug && governed(s))
|
|
2445
|
+
})),
|
|
2446
|
+
globals: this.config.globals.map((g) => ({
|
|
2447
|
+
slug: g.slug,
|
|
2448
|
+
ops: GLOBAL_OPERATIONS.filter((op) => op !== "publish" || g.drafts === true),
|
|
2449
|
+
new: !seen("globals", g.slug)
|
|
2450
|
+
})),
|
|
2451
|
+
views: this.views()
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
async create(input, author) {
|
|
2455
|
+
await this.sync();
|
|
2456
|
+
const key2 = typeof input.key === "string" ? input.key.trim() : "";
|
|
2457
|
+
if (!KEY.test(key2)) {
|
|
2458
|
+
throw new ValidationError(ROLES, [
|
|
2459
|
+
{ field: "key", message: 'lowercase letters, digits and "-", starting with a letter' }
|
|
2460
|
+
]);
|
|
2461
|
+
}
|
|
2462
|
+
if ((await this.rows()).some((r) => r.key === key2))
|
|
2463
|
+
throw new ValidationError(ROLES, [{ field: "key", message: "is already used" }]);
|
|
2464
|
+
const row = await this.insert(
|
|
2465
|
+
key2,
|
|
2466
|
+
this.cleanName(input.name),
|
|
2467
|
+
this.clean(input.permissions),
|
|
2468
|
+
author.id
|
|
2469
|
+
);
|
|
2470
|
+
return this.toRole(row, 0);
|
|
2471
|
+
}
|
|
2472
|
+
async update(id, input, author) {
|
|
2473
|
+
const row = await this.row(id);
|
|
2474
|
+
const data = { ...row, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2475
|
+
if (input.name !== void 0) data.name = this.cleanName(input.name);
|
|
2476
|
+
if (input.permissions !== void 0) {
|
|
2477
|
+
if (row.key === "admin")
|
|
2478
|
+
throw new EasyCMSError("Admins may always do everything; their role cannot be changed", 400);
|
|
2479
|
+
data.permissions = this.clean(input.permissions);
|
|
2480
|
+
}
|
|
2481
|
+
const { id: _id, ...rest } = data;
|
|
2482
|
+
const saved = await this.db.update({ collection: ROLES, id: row.id, data: rest });
|
|
2483
|
+
await this.record(saved, author.id);
|
|
2484
|
+
this.invalidate();
|
|
2485
|
+
return this.toRole(saved, await this.users(String(row.key)));
|
|
2486
|
+
}
|
|
2487
|
+
async delete(id) {
|
|
2488
|
+
const row = await this.row(id);
|
|
2489
|
+
const key2 = String(row.key);
|
|
2490
|
+
if (this.config.auth.roles.includes(key2))
|
|
2491
|
+
throw new EasyCMSError(`"${key2}" is in auth.roles and cannot be deleted`, 400);
|
|
2492
|
+
const users = await this.users(key2);
|
|
2493
|
+
if (users > 0)
|
|
2494
|
+
throw new EasyCMSError(`${users} user(s) have this role; give them another role first`, 409);
|
|
2495
|
+
await this.db.delete({ collection: ROLES, id: row.id });
|
|
2496
|
+
await this.versions.deleteAll(ROLES, row.id);
|
|
2497
|
+
this.invalidate();
|
|
2498
|
+
}
|
|
2499
|
+
async history(id) {
|
|
2500
|
+
const row = await this.row(id);
|
|
2501
|
+
const list = await this.versions.list(ROLES, row.id, HISTORY, 1);
|
|
2502
|
+
const emails = /* @__PURE__ */ new Map();
|
|
2503
|
+
const out = [];
|
|
2504
|
+
for (const summary of list.docs) {
|
|
2505
|
+
const version = await this.versions.get(ROLES, row.id, summary.id);
|
|
2506
|
+
if (!version) continue;
|
|
2507
|
+
let author = null;
|
|
2508
|
+
if (summary.author !== null) {
|
|
2509
|
+
const k = String(summary.author);
|
|
2510
|
+
if (!emails.has(k)) {
|
|
2511
|
+
const user = await this.db.findById({ collection: USERS, id: summary.author });
|
|
2512
|
+
emails.set(k, typeof user?.email === "string" ? user.email : null);
|
|
2513
|
+
}
|
|
2514
|
+
author = emails.get(k) ?? null;
|
|
2515
|
+
}
|
|
2516
|
+
out.push({
|
|
2517
|
+
id: summary.id,
|
|
2518
|
+
author,
|
|
2519
|
+
createdAt: summary.createdAt,
|
|
2520
|
+
name: String(version.data.name ?? ""),
|
|
2521
|
+
permissions: version.data.permissions ?? {}
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
return out;
|
|
2525
|
+
}
|
|
2526
|
+
// -------------------------------------------------------------------------
|
|
2527
|
+
/** Pages and panels roles can be given. */
|
|
2528
|
+
views() {
|
|
2529
|
+
return [
|
|
2530
|
+
{ id: "status" },
|
|
2531
|
+
...this.config.collections.some(
|
|
2532
|
+
(c) => c.slug === WEBHOOK_DELIVERIES || c.slug === EMAIL_DELIVERIES
|
|
2533
|
+
) ? [{ id: "deliveries" }] : [],
|
|
2534
|
+
...this.config.admin.pages.map((page) => ({ id: viewId.page(page.path), label: page.label })),
|
|
2535
|
+
...this.config.admin.dashboard.map((widget) => ({
|
|
2536
|
+
id: viewId.widget(tagOf(widget.component)),
|
|
2537
|
+
...widget.label !== void 0 ? { label: widget.label } : {}
|
|
2538
|
+
}))
|
|
2539
|
+
];
|
|
2540
|
+
}
|
|
2541
|
+
/** Every collection and global, and the plugin pages and panels: what roles had before roles. */
|
|
2542
|
+
everything() {
|
|
2543
|
+
const collections = {};
|
|
2544
|
+
for (const c of this.config.collections) {
|
|
2545
|
+
if (governed(c.slug))
|
|
2546
|
+
collections[c.slug] = ROLE_OPERATIONS.filter((op) => op !== "publish" || c.drafts === true);
|
|
2547
|
+
}
|
|
2548
|
+
const globals = {};
|
|
2549
|
+
for (const g of this.config.globals)
|
|
2550
|
+
globals[g.slug] = GLOBAL_OPERATIONS.filter((op) => op !== "publish" || g.drafts === true);
|
|
2551
|
+
const admin = this.views().map((v) => v.id).filter((id) => id.startsWith("page:") || id.startsWith("widget:"));
|
|
2552
|
+
return { collections, globals, admin };
|
|
2553
|
+
}
|
|
2554
|
+
/** Keeps only known collections, globals, operations and pages; `read` comes with the others. */
|
|
2555
|
+
clean(value) {
|
|
2556
|
+
const input = typeof value === "object" && value !== null ? value : {};
|
|
2557
|
+
const pick = (group, allowed2) => {
|
|
2558
|
+
const out = {};
|
|
2559
|
+
if (typeof group !== "object" || group === null) return out;
|
|
2560
|
+
for (const [slug2, ops] of Object.entries(group)) {
|
|
2561
|
+
const valid = allowed2.get(slug2);
|
|
2562
|
+
if (!valid || !Array.isArray(ops)) continue;
|
|
2563
|
+
const picked = valid.filter((op) => ops.includes(op));
|
|
2564
|
+
if (picked.length > 0 && !picked.includes("read")) picked.unshift("read");
|
|
2565
|
+
out[slug2] = picked;
|
|
2566
|
+
}
|
|
2567
|
+
return out;
|
|
2568
|
+
};
|
|
2569
|
+
const collections = new Map(
|
|
2570
|
+
this.config.collections.filter((c) => governed(c.slug)).map((c) => [
|
|
2571
|
+
c.slug,
|
|
2572
|
+
ROLE_OPERATIONS.filter((op) => op !== "publish" || c.drafts === true)
|
|
2573
|
+
])
|
|
2574
|
+
);
|
|
2575
|
+
const globals = new Map(
|
|
2576
|
+
this.config.globals.map((g) => [
|
|
2577
|
+
g.slug,
|
|
2578
|
+
GLOBAL_OPERATIONS.filter((op) => op !== "publish" || g.drafts === true)
|
|
2579
|
+
])
|
|
2580
|
+
);
|
|
2581
|
+
const views = new Set(this.views().map((v) => v.id));
|
|
2582
|
+
const admin = Array.isArray(input.admin) ? [
|
|
2583
|
+
...new Set(
|
|
2584
|
+
input.admin.filter((id) => typeof id === "string" && views.has(id))
|
|
2585
|
+
)
|
|
2586
|
+
] : [];
|
|
2587
|
+
return {
|
|
2588
|
+
collections: pick(input.collections, collections),
|
|
2589
|
+
globals: pick(input.globals, globals),
|
|
2590
|
+
admin
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
cleanName(value) {
|
|
2594
|
+
if (value === void 0 || value === null) return "";
|
|
2595
|
+
if (typeof value !== "string" || value.trim().length > 60)
|
|
2596
|
+
throw new ValidationError(ROLES, [
|
|
2597
|
+
{ field: "name", message: "must be at most 60 characters" }
|
|
2598
|
+
]);
|
|
2599
|
+
return value.trim();
|
|
2600
|
+
}
|
|
2601
|
+
async permissionsOf(key2) {
|
|
2602
|
+
return (await this.load()).get(key2) ?? {};
|
|
2603
|
+
}
|
|
2604
|
+
async load() {
|
|
2605
|
+
if (this.cache && Date.now() - this.cache.at < TTL) return this.cache.byKey;
|
|
2606
|
+
await this.sync();
|
|
2607
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2608
|
+
for (const row of await this.rows())
|
|
2609
|
+
byKey.set(String(row.key), row.permissions ?? {});
|
|
2610
|
+
this.cache = { at: Date.now(), byKey };
|
|
2611
|
+
return byKey;
|
|
2612
|
+
}
|
|
2613
|
+
async rows() {
|
|
2614
|
+
return (await this.db.find({ collection: ROLES, sort: ["id"], limit: 0, page: 1 })).docs;
|
|
2615
|
+
}
|
|
2616
|
+
async row(id) {
|
|
2617
|
+
const row = await this.db.findById({ collection: ROLES, id });
|
|
2618
|
+
if (!row) throw new NotFoundError(ROLES, id);
|
|
2619
|
+
return row;
|
|
2620
|
+
}
|
|
2621
|
+
users(key2) {
|
|
2622
|
+
return this.db.count({ collection: USERS, where: { role: { equals: key2 } } });
|
|
2623
|
+
}
|
|
2624
|
+
async insert(key2, name, permissions, author) {
|
|
2625
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2626
|
+
const row = await this.db.create({
|
|
2627
|
+
collection: ROLES,
|
|
2628
|
+
data: { key: key2, name, permissions, createdAt: now, updatedAt: now }
|
|
2629
|
+
});
|
|
2630
|
+
await this.record(row, author);
|
|
2631
|
+
this.invalidate();
|
|
2632
|
+
return row;
|
|
2633
|
+
}
|
|
2634
|
+
/** Keeps the role as saved, for its history. */
|
|
2635
|
+
async record(row, author) {
|
|
2636
|
+
await this.versions.save({
|
|
2637
|
+
parent: ROLES,
|
|
2638
|
+
doc: row.id,
|
|
2639
|
+
status: null,
|
|
2640
|
+
snapshot: { name: row.name ?? "", permissions: row.permissions ?? {} },
|
|
2641
|
+
author,
|
|
2642
|
+
max: HISTORY
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
toRole(row, users) {
|
|
2646
|
+
const key2 = String(row.key);
|
|
2647
|
+
return {
|
|
2648
|
+
id: row.id,
|
|
2649
|
+
key: key2,
|
|
2650
|
+
name: typeof row.name === "string" ? row.name : "",
|
|
2651
|
+
system: this.config.auth.roles.includes(key2),
|
|
2652
|
+
users,
|
|
2653
|
+
permissions: row.permissions ?? {},
|
|
2654
|
+
updatedAt: String(row.updatedAt)
|
|
2655
|
+
};
|
|
2656
|
+
}
|
|
2657
|
+
};
|
|
2658
|
+
function rank(role, config) {
|
|
2659
|
+
const index = config.auth.roles.indexOf(role.key);
|
|
2660
|
+
return index === -1 ? config.auth.roles.length : index;
|
|
1801
2661
|
}
|
|
1802
2662
|
|
|
1803
|
-
// src/load-config.ts
|
|
1804
|
-
import { existsSync } from "fs";
|
|
1805
|
-
import { resolve } from "path";
|
|
1806
|
-
import { createJiti } from "jiti";
|
|
1807
|
-
|
|
1808
2663
|
// src/webhooks.ts
|
|
1809
2664
|
import { createHmac as createHmac2, randomUUID as randomUUID2 } from "crypto";
|
|
1810
2665
|
var WEBHOOK_EVENTS = [
|
|
@@ -1994,6 +2849,9 @@ var RESERVED_SLUGS = /* @__PURE__ */ new Set([
|
|
|
1994
2849
|
"document-versions",
|
|
1995
2850
|
"scheduled-jobs",
|
|
1996
2851
|
"webhook-deliveries",
|
|
2852
|
+
"email-deliveries",
|
|
2853
|
+
"database-backups",
|
|
2854
|
+
"user-roles",
|
|
1997
2855
|
"api-keys",
|
|
1998
2856
|
"jobs",
|
|
1999
2857
|
"migrations",
|
|
@@ -2016,6 +2874,7 @@ function validateConfig(config) {
|
|
|
2016
2874
|
validateAdmin(config, add);
|
|
2017
2875
|
validateAdminViews(config, add);
|
|
2018
2876
|
validateUpload(config, add);
|
|
2877
|
+
validateBackups(config, add);
|
|
2019
2878
|
validateAuth(config, add);
|
|
2020
2879
|
validateCors(config.cors, add);
|
|
2021
2880
|
validateLocalization(config, add);
|
|
@@ -2025,17 +2884,17 @@ function validateConfig(config) {
|
|
|
2025
2884
|
const collectionSlugs = /* @__PURE__ */ new Set([...BUILTIN_COLLECTIONS]);
|
|
2026
2885
|
const seenCollections = /* @__PURE__ */ new Set();
|
|
2027
2886
|
collections.forEach((collection2, i) => {
|
|
2028
|
-
const
|
|
2029
|
-
if (
|
|
2887
|
+
const slug2 = checkSlug(collection2, `collections[${i}]`, seenCollections, add);
|
|
2888
|
+
if (slug2) collectionSlugs.add(slug2);
|
|
2030
2889
|
});
|
|
2031
2890
|
const menu = config.admin?.menu;
|
|
2032
2891
|
if (menu !== void 0) {
|
|
2033
2892
|
if (!Array.isArray(menu)) {
|
|
2034
2893
|
add("admin.menu", "must be an array of collection slugs", "e.g. menu: ['posts', 'media']");
|
|
2035
2894
|
} else {
|
|
2036
|
-
for (const [i,
|
|
2037
|
-
if (typeof
|
|
2038
|
-
add(`admin.menu[${i}]`, `unknown collection ${JSON.stringify(
|
|
2895
|
+
for (const [i, slug2] of menu.entries()) {
|
|
2896
|
+
if (typeof slug2 !== "string" || !collectionSlugs.has(slug2))
|
|
2897
|
+
add(`admin.menu[${i}]`, `unknown collection ${JSON.stringify(slug2)}`);
|
|
2039
2898
|
}
|
|
2040
2899
|
}
|
|
2041
2900
|
}
|
|
@@ -2197,6 +3056,7 @@ function validateAdminViews(config, add) {
|
|
|
2197
3056
|
"e.g. dashboard: [{ component: 'ecms-stats-widget' }]"
|
|
2198
3057
|
);
|
|
2199
3058
|
} else {
|
|
3059
|
+
const tags = /* @__PURE__ */ new Set();
|
|
2200
3060
|
widgets2.forEach((widget, i) => {
|
|
2201
3061
|
const path = `admin.dashboard[${i}]`;
|
|
2202
3062
|
if (typeof widget !== "object" || widget === null) {
|
|
@@ -2207,6 +3067,19 @@ function validateAdminViews(config, add) {
|
|
|
2207
3067
|
validateComponent(w.component, `${path}.component`, add);
|
|
2208
3068
|
if (w.width !== void 0 && w.width !== "half" && w.width !== "full")
|
|
2209
3069
|
add(`${path}.width`, 'must be "half" or "full"');
|
|
3070
|
+
if (w.label !== void 0 && !(typeof w.label === "string" && w.label.trim() !== "") && !(typeof w.label === "object" && w.label !== null && !Array.isArray(w.label)))
|
|
3071
|
+
add(`${path}.label`, "must be a string or { en, th }");
|
|
3072
|
+
const component = w.component;
|
|
3073
|
+
const tag = typeof component === "string" ? component : component?.tag;
|
|
3074
|
+
if (config.auth?.rbac === true && typeof tag === "string") {
|
|
3075
|
+
if (tags.has(tag))
|
|
3076
|
+
add(
|
|
3077
|
+
`${path}.component`,
|
|
3078
|
+
`another panel uses "${tag}"`,
|
|
3079
|
+
"with auth.rbac, roles are given panels by their tag: use one tag per panel"
|
|
3080
|
+
);
|
|
3081
|
+
tags.add(tag);
|
|
3082
|
+
}
|
|
2210
3083
|
access(w.access, path);
|
|
2211
3084
|
});
|
|
2212
3085
|
}
|
|
@@ -2220,6 +3093,8 @@ function validateIcon(container, path, add) {
|
|
|
2220
3093
|
function validateAuth(config, add) {
|
|
2221
3094
|
const auth = config.auth;
|
|
2222
3095
|
if (auth === void 0) return;
|
|
3096
|
+
if (auth.rbac !== void 0 && typeof auth.rbac !== "boolean")
|
|
3097
|
+
add("auth.rbac", "must be true or false");
|
|
2223
3098
|
if (auth.roles !== void 0) {
|
|
2224
3099
|
const roles = auth.roles;
|
|
2225
3100
|
if (!Array.isArray(roles) || roles.some((r) => typeof r !== "string" || r === "")) {
|
|
@@ -2376,10 +3251,10 @@ function validateWebhooks(config, add) {
|
|
|
2376
3251
|
if (!WEBHOOK_EVENTS.includes(event))
|
|
2377
3252
|
add(`${path}.events`, `unknown event "${event}"`, `use: ${WEBHOOK_EVENTS.join(", ")}`);
|
|
2378
3253
|
}
|
|
2379
|
-
for (const
|
|
2380
|
-
if (!collections.has(
|
|
2381
|
-
for (const
|
|
2382
|
-
if (!globals.has(
|
|
3254
|
+
for (const slug2 of hook?.collections ?? [])
|
|
3255
|
+
if (!collections.has(slug2)) add(`${path}.collections`, `unknown collection "${slug2}"`);
|
|
3256
|
+
for (const slug2 of hook?.globals ?? [])
|
|
3257
|
+
if (!globals.has(slug2)) add(`${path}.globals`, `unknown global "${slug2}"`);
|
|
2383
3258
|
}
|
|
2384
3259
|
}
|
|
2385
3260
|
function validateCors(cors, add) {
|
|
@@ -2432,6 +3307,32 @@ function validateUpload(config, add) {
|
|
|
2432
3307
|
add("upload.fromURL.allowPrivate", "must be true or false");
|
|
2433
3308
|
}
|
|
2434
3309
|
var HOST_PATTERN = /^(\*|(\*\.)?[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*)$/i;
|
|
3310
|
+
function validateBackups(config, add) {
|
|
3311
|
+
const backups = config.backups;
|
|
3312
|
+
if (backups === void 0) return;
|
|
3313
|
+
if (typeof backups !== "object" || backups === null) {
|
|
3314
|
+
add("backups", "must be an object", "e.g. backups: { every: 'day', keep: 7 }");
|
|
3315
|
+
return;
|
|
3316
|
+
}
|
|
3317
|
+
const { every, at, keep, dir, storage, sqlite } = backups;
|
|
3318
|
+
if (every !== void 0 && every !== "day" && every !== "week")
|
|
3319
|
+
add("backups.every", 'must be "day" or "week"');
|
|
3320
|
+
if (at !== void 0 && (typeof at !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(at)))
|
|
3321
|
+
add("backups.at", 'must be a time like "03:00"');
|
|
3322
|
+
if (keep !== void 0 && !(Number.isInteger(keep) && keep >= 1))
|
|
3323
|
+
add("backups.keep", "must be a whole number, 1 or more");
|
|
3324
|
+
if (dir !== void 0 && (typeof dir !== "string" || dir.trim() === ""))
|
|
3325
|
+
add("backups.dir", "must be a folder name");
|
|
3326
|
+
if (typeof dir === "string" && dir.replace(/^\.\//, "").replace(/\/+$/, "") === (config.upload?.dir ?? "uploads").replace(/^\.\//, "").replace(/\/+$/, ""))
|
|
3327
|
+
add("backups.dir", "must not be the uploads folder: uploads are served publicly");
|
|
3328
|
+
if (storage !== void 0 && (typeof storage !== "object" || storage === null || typeof storage.put !== "function"))
|
|
3329
|
+
add("backups.storage", "must be a storage adapter, e.g. s3Storage()");
|
|
3330
|
+
if (sqlite !== void 0 && typeof sqlite !== "function")
|
|
3331
|
+
add(
|
|
3332
|
+
"backups.sqlite",
|
|
3333
|
+
"must be the sqlite adapter, e.g. import { sqlite } from '@easy-cms/db-sqlite'"
|
|
3334
|
+
);
|
|
3335
|
+
}
|
|
2435
3336
|
function asArray(value, path, add) {
|
|
2436
3337
|
if (value === void 0) return [];
|
|
2437
3338
|
if (!Array.isArray(value)) {
|
|
@@ -2441,24 +3342,24 @@ function asArray(value, path, add) {
|
|
|
2441
3342
|
return value;
|
|
2442
3343
|
}
|
|
2443
3344
|
function checkSlug(container, path, seen, add) {
|
|
2444
|
-
const
|
|
2445
|
-
if (typeof
|
|
3345
|
+
const slug2 = container.slug;
|
|
3346
|
+
if (typeof slug2 !== "string" || !SLUG_PATTERN.test(slug2)) {
|
|
2446
3347
|
add(
|
|
2447
3348
|
`${path}.slug`,
|
|
2448
|
-
`must be lowercase letters, digits, "-" or "_", starting with a letter (got ${JSON.stringify(
|
|
3349
|
+
`must be lowercase letters, digits, "-" or "_", starting with a letter (got ${JSON.stringify(slug2)})`
|
|
2449
3350
|
);
|
|
2450
3351
|
return void 0;
|
|
2451
3352
|
}
|
|
2452
|
-
if (RESERVED_SLUGS.has(
|
|
2453
|
-
add(`${path}.slug`, `"${
|
|
3353
|
+
if (RESERVED_SLUGS.has(slug2)) {
|
|
3354
|
+
add(`${path}.slug`, `"${slug2}" is reserved by Easy CMS`, "choose another slug");
|
|
2454
3355
|
return void 0;
|
|
2455
3356
|
}
|
|
2456
|
-
if (seen.has(
|
|
2457
|
-
add(`${path}.slug`, `duplicate slug "${
|
|
3357
|
+
if (seen.has(slug2)) {
|
|
3358
|
+
add(`${path}.slug`, `duplicate slug "${slug2}"`);
|
|
2458
3359
|
return void 0;
|
|
2459
3360
|
}
|
|
2460
|
-
seen.add(
|
|
2461
|
-
return
|
|
3361
|
+
seen.add(slug2);
|
|
3362
|
+
return slug2;
|
|
2462
3363
|
}
|
|
2463
3364
|
function validateContainer(container, path, collectionSlugs, add) {
|
|
2464
3365
|
if (!Array.isArray(container.fields)) {
|
|
@@ -2888,6 +3789,7 @@ async function resolveConfig(input) {
|
|
|
2888
3789
|
},
|
|
2889
3790
|
auth: {
|
|
2890
3791
|
roles: config.auth?.roles ?? DEFAULT_ROLES,
|
|
3792
|
+
rbac: config.auth?.rbac === true,
|
|
2891
3793
|
tokenExpiration: config.auth?.tokenExpiration ?? DEFAULT_TOKEN_EXPIRATION,
|
|
2892
3794
|
maxLoginAttempts: config.auth?.maxLoginAttempts ?? 5,
|
|
2893
3795
|
lockWindow: config.auth?.lockWindow ?? 15 * 60,
|
|
@@ -2899,11 +3801,14 @@ async function resolveConfig(input) {
|
|
|
2899
3801
|
collections: [
|
|
2900
3802
|
...config.collections ?? [],
|
|
2901
3803
|
...internalCollections,
|
|
2902
|
-
|
|
3804
|
+
// Roles keep their history as versions.
|
|
3805
|
+
...usesVersions(config) || config.auth?.rbac ? [versionsCollection] : [],
|
|
2903
3806
|
...usesSchedule(config) ? [scheduledJobsCollection] : [],
|
|
2904
3807
|
...(config.webhooks?.length ?? 0) > 0 ? [webhookDeliveriesCollection] : [],
|
|
2905
3808
|
...config.apiKeys ? [apiKeysCollection] : [],
|
|
2906
|
-
...config.email ? [emailDeliveriesCollection] : []
|
|
3809
|
+
...config.email ? [emailDeliveriesCollection] : [],
|
|
3810
|
+
databaseBackupsCollection,
|
|
3811
|
+
...config.auth?.rbac ? [rolesCollection] : []
|
|
2907
3812
|
],
|
|
2908
3813
|
globals: config.globals ?? [],
|
|
2909
3814
|
endpoints: config.endpoints ?? [],
|
|
@@ -2932,7 +3837,7 @@ var CONFIG_FILE_NAMES = [
|
|
|
2932
3837
|
];
|
|
2933
3838
|
function findConfigFile(cwd = process.cwd()) {
|
|
2934
3839
|
for (const name of CONFIG_FILE_NAMES) {
|
|
2935
|
-
const path =
|
|
3840
|
+
const path = resolve2(
|
|
2936
3841
|
/* turbopackIgnore: true */
|
|
2937
3842
|
cwd,
|
|
2938
3843
|
name
|
|
@@ -2951,7 +3856,7 @@ async function importConfig(file) {
|
|
|
2951
3856
|
}
|
|
2952
3857
|
async function loadConfig(options = {}) {
|
|
2953
3858
|
const cwd = options.cwd ?? process.cwd();
|
|
2954
|
-
const file = options.configFile ?
|
|
3859
|
+
const file = options.configFile ? resolve2(cwd, options.configFile) : findConfigFile(cwd);
|
|
2955
3860
|
if (!file || !existsSync(file)) {
|
|
2956
3861
|
const looked = options.configFile ?? CONFIG_FILE_NAMES.join(", ");
|
|
2957
3862
|
throw new Error(
|
|
@@ -3190,12 +4095,12 @@ async function populate(load, collections, fields, docs, depth) {
|
|
|
3190
4095
|
}
|
|
3191
4096
|
const found = /* @__PURE__ */ new Map();
|
|
3192
4097
|
await Promise.all(
|
|
3193
|
-
[...wanted].map(async ([
|
|
3194
|
-
const collection2 = bySlug.get(
|
|
4098
|
+
[...wanted].map(async ([slug2, ids]) => {
|
|
4099
|
+
const collection2 = bySlug.get(slug2);
|
|
3195
4100
|
if (!collection2) return;
|
|
3196
4101
|
const raw = await load(collection2, [...ids]);
|
|
3197
4102
|
const populated = await populate(load, collections, collection2.fields, raw, depth - 1);
|
|
3198
|
-
found.set(
|
|
4103
|
+
found.set(slug2, new Map(populated.map((d) => [d.id, d])));
|
|
3199
4104
|
})
|
|
3200
4105
|
);
|
|
3201
4106
|
for (const slot of slots) slot.assign(found.get(slot.collection) ?? /* @__PURE__ */ new Map());
|
|
@@ -3448,181 +4353,6 @@ function read(response, maxBytes, deadline) {
|
|
|
3448
4353
|
});
|
|
3449
4354
|
}
|
|
3450
4355
|
|
|
3451
|
-
// src/storage.ts
|
|
3452
|
-
import { mkdir, readFile, rm, stat, writeFile } from "fs/promises";
|
|
3453
|
-
import { dirname, isAbsolute, join, normalize, resolve as resolve2, sep } from "path";
|
|
3454
|
-
function localStorage(options = {}) {
|
|
3455
|
-
let root = resolve2(
|
|
3456
|
-
/* turbopackIgnore: true */
|
|
3457
|
-
options.dir ?? "uploads"
|
|
3458
|
-
);
|
|
3459
|
-
const pathOf = (key2) => {
|
|
3460
|
-
const path = normalize(join(
|
|
3461
|
-
/* turbopackIgnore: true */
|
|
3462
|
-
root,
|
|
3463
|
-
key2
|
|
3464
|
-
));
|
|
3465
|
-
if (!path.startsWith(root + sep)) throw new Error(`Invalid storage key "${key2}"`);
|
|
3466
|
-
return path;
|
|
3467
|
-
};
|
|
3468
|
-
return {
|
|
3469
|
-
name: "local",
|
|
3470
|
-
init({ cwd }) {
|
|
3471
|
-
const dir = options.dir ?? "uploads";
|
|
3472
|
-
root = isAbsolute(dir) ? dir : resolve2(
|
|
3473
|
-
/* turbopackIgnore: true */
|
|
3474
|
-
cwd,
|
|
3475
|
-
dir
|
|
3476
|
-
);
|
|
3477
|
-
},
|
|
3478
|
-
async put(key2, data) {
|
|
3479
|
-
const path = pathOf(key2);
|
|
3480
|
-
await mkdir(dirname(path), { recursive: true });
|
|
3481
|
-
await writeFile(path, data, { flag: "wx" });
|
|
3482
|
-
},
|
|
3483
|
-
async get(key2) {
|
|
3484
|
-
const path = pathOf(key2);
|
|
3485
|
-
const info = await stat(path).catch(() => void 0);
|
|
3486
|
-
if (!info?.isFile()) return null;
|
|
3487
|
-
return { body: new Uint8Array(await readFile(path)), size: info.size };
|
|
3488
|
-
},
|
|
3489
|
-
async delete(key2) {
|
|
3490
|
-
await rm(pathOf(key2), { force: true });
|
|
3491
|
-
}
|
|
3492
|
-
};
|
|
3493
|
-
}
|
|
3494
|
-
|
|
3495
|
-
// src/versions.ts
|
|
3496
|
-
var DEFAULT_MAX_VERSIONS = 50;
|
|
3497
|
-
function versionLimit(config) {
|
|
3498
|
-
const versions = config.versions;
|
|
3499
|
-
if (!versions) return void 0;
|
|
3500
|
-
return versions === true ? DEFAULT_MAX_VERSIONS : versions.max ?? DEFAULT_MAX_VERSIONS;
|
|
3501
|
-
}
|
|
3502
|
-
var collectionParent = (slug) => slug;
|
|
3503
|
-
var globalParent = (slug) => `global:${slug}`;
|
|
3504
|
-
function snapshotOf(fields, doc) {
|
|
3505
|
-
const hidden = new Set(fields.filter((f) => f.hidden).map((f) => f.name));
|
|
3506
|
-
const snapshot = {};
|
|
3507
|
-
for (const [key2, value] of Object.entries(doc)) {
|
|
3508
|
-
if (key2 === "id" || key2 === "createdAt" || key2 === "updatedAt" || hidden.has(key2)) continue;
|
|
3509
|
-
snapshot[key2] = value;
|
|
3510
|
-
}
|
|
3511
|
-
return snapshot;
|
|
3512
|
-
}
|
|
3513
|
-
var toSummary = (row) => ({
|
|
3514
|
-
id: row.id,
|
|
3515
|
-
status: row.status ?? null,
|
|
3516
|
-
latest: row.latest === true,
|
|
3517
|
-
author: row.author ?? null,
|
|
3518
|
-
createdAt: String(row.createdAt)
|
|
3519
|
-
});
|
|
3520
|
-
var VersionStore = class {
|
|
3521
|
-
constructor(db) {
|
|
3522
|
-
this.db = db;
|
|
3523
|
-
}
|
|
3524
|
-
db;
|
|
3525
|
-
/** Saves a new latest version and deletes the oldest beyond `max`. */
|
|
3526
|
-
async save(args) {
|
|
3527
|
-
const { parent, doc } = args;
|
|
3528
|
-
const previous = await this.db.find({
|
|
3529
|
-
collection: VERSIONS,
|
|
3530
|
-
where: { and: [...this.of(parent, doc), { latest: { equals: true } }] },
|
|
3531
|
-
sort: [],
|
|
3532
|
-
limit: 0,
|
|
3533
|
-
page: 1
|
|
3534
|
-
});
|
|
3535
|
-
for (const row of previous.docs) {
|
|
3536
|
-
await this.db.update({ collection: VERSIONS, id: row.id, data: { ...row, latest: false } });
|
|
3537
|
-
}
|
|
3538
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3539
|
-
const created = await this.db.create({
|
|
3540
|
-
collection: VERSIONS,
|
|
3541
|
-
data: {
|
|
3542
|
-
parent,
|
|
3543
|
-
doc,
|
|
3544
|
-
status: args.status,
|
|
3545
|
-
latest: true,
|
|
3546
|
-
author: args.author,
|
|
3547
|
-
snapshot: args.snapshot,
|
|
3548
|
-
createdAt: now,
|
|
3549
|
-
updatedAt: now
|
|
3550
|
-
}
|
|
3551
|
-
});
|
|
3552
|
-
await this.prune(parent, doc, args.max);
|
|
3553
|
-
return toSummary(created);
|
|
3554
|
-
}
|
|
3555
|
-
/** The latest version of each document, keyed by document id. */
|
|
3556
|
-
async latest(parent, docs) {
|
|
3557
|
-
const result = /* @__PURE__ */ new Map();
|
|
3558
|
-
if (docs.length === 0) return result;
|
|
3559
|
-
const rows = await this.db.find({
|
|
3560
|
-
collection: VERSIONS,
|
|
3561
|
-
where: {
|
|
3562
|
-
and: [
|
|
3563
|
-
{ parent: { equals: parent } },
|
|
3564
|
-
{ doc: { in: [...docs] } },
|
|
3565
|
-
{ latest: { equals: true } }
|
|
3566
|
-
]
|
|
3567
|
-
},
|
|
3568
|
-
sort: [],
|
|
3569
|
-
limit: 0,
|
|
3570
|
-
page: 1
|
|
3571
|
-
});
|
|
3572
|
-
for (const row of rows.docs) result.set(String(row.doc), this.toVersion(row));
|
|
3573
|
-
return result;
|
|
3574
|
-
}
|
|
3575
|
-
async list(parent, doc, limit, page) {
|
|
3576
|
-
const found = await this.db.find({
|
|
3577
|
-
collection: VERSIONS,
|
|
3578
|
-
where: { and: this.of(parent, doc) },
|
|
3579
|
-
sort: ["-id"],
|
|
3580
|
-
limit,
|
|
3581
|
-
page
|
|
3582
|
-
});
|
|
3583
|
-
return { ...found, docs: found.docs.map(toSummary) };
|
|
3584
|
-
}
|
|
3585
|
-
async get(parent, doc, id) {
|
|
3586
|
-
const found = await this.db.find({
|
|
3587
|
-
collection: VERSIONS,
|
|
3588
|
-
where: { and: [...this.of(parent, doc), { id: { equals: id } }] },
|
|
3589
|
-
sort: [],
|
|
3590
|
-
limit: 1,
|
|
3591
|
-
page: 1
|
|
3592
|
-
});
|
|
3593
|
-
const row = found.docs[0];
|
|
3594
|
-
return row ? this.toVersion(row) : null;
|
|
3595
|
-
}
|
|
3596
|
-
async deleteAll(parent, doc) {
|
|
3597
|
-
const rows = await this.db.find({
|
|
3598
|
-
collection: VERSIONS,
|
|
3599
|
-
where: { and: this.of(parent, doc) },
|
|
3600
|
-
sort: [],
|
|
3601
|
-
limit: 0,
|
|
3602
|
-
page: 1
|
|
3603
|
-
});
|
|
3604
|
-
for (const row of rows.docs) await this.db.delete({ collection: VERSIONS, id: row.id });
|
|
3605
|
-
}
|
|
3606
|
-
async prune(parent, doc, max) {
|
|
3607
|
-
const rows = await this.db.find({
|
|
3608
|
-
collection: VERSIONS,
|
|
3609
|
-
where: { and: this.of(parent, doc) },
|
|
3610
|
-
sort: ["-id"],
|
|
3611
|
-
limit: 0,
|
|
3612
|
-
page: 1
|
|
3613
|
-
});
|
|
3614
|
-
for (const row of rows.docs.slice(max)) {
|
|
3615
|
-
await this.db.delete({ collection: VERSIONS, id: row.id });
|
|
3616
|
-
}
|
|
3617
|
-
}
|
|
3618
|
-
of(parent, doc) {
|
|
3619
|
-
return [{ parent: { equals: parent } }, { doc: { equals: doc } }];
|
|
3620
|
-
}
|
|
3621
|
-
toVersion(row) {
|
|
3622
|
-
return { ...toSummary(row), data: row.snapshot ?? {} };
|
|
3623
|
-
}
|
|
3624
|
-
};
|
|
3625
|
-
|
|
3626
4356
|
// src/local-api.ts
|
|
3627
4357
|
async function createEasyCMS(config, options = {}) {
|
|
3628
4358
|
const resolved3 = await resolveConfig(config);
|
|
@@ -3660,6 +4390,8 @@ var EasyCMS = class {
|
|
|
3660
4390
|
storage;
|
|
3661
4391
|
/** Project root: relative paths in the config (e.g. `admin.modules`) start here. */
|
|
3662
4392
|
cwd;
|
|
4393
|
+
/** Roles and their permissions (`auth.rbac`, Settings → Roles). */
|
|
4394
|
+
roles;
|
|
3663
4395
|
versions;
|
|
3664
4396
|
webhooks;
|
|
3665
4397
|
mailer;
|
|
@@ -3670,6 +4402,7 @@ var EasyCMS = class {
|
|
|
3670
4402
|
this.storage = storage;
|
|
3671
4403
|
this.cwd = cwd;
|
|
3672
4404
|
this.versions = new VersionStore(db);
|
|
4405
|
+
this.roles = new Roles(config, db);
|
|
3673
4406
|
this.webhooks = new Webhooks(
|
|
3674
4407
|
config.webhooks ?? [],
|
|
3675
4408
|
logger,
|
|
@@ -3760,7 +4493,7 @@ var EasyCMS = class {
|
|
|
3760
4493
|
const config = this.collection(MEDIA);
|
|
3761
4494
|
const guard = guardOf(options);
|
|
3762
4495
|
if (guard.enforce) {
|
|
3763
|
-
this.
|
|
4496
|
+
await this.checkGrant(guard, { collection: MEDIA }, "create");
|
|
3764
4497
|
const allowed2 = await evaluateAccess(config.access?.create, { user: guard.user, data });
|
|
3765
4498
|
if (allowed2 !== true) throw deny(guard.user);
|
|
3766
4499
|
}
|
|
@@ -3820,7 +4553,7 @@ var EasyCMS = class {
|
|
|
3820
4553
|
const guard = guardOf(options);
|
|
3821
4554
|
const fromURL = this.config.upload.fromURL;
|
|
3822
4555
|
if (guard.enforce) {
|
|
3823
|
-
this.
|
|
4556
|
+
await this.checkGrant(guard, { collection: MEDIA }, "create");
|
|
3824
4557
|
const allowed2 = await evaluateAccess(config.access?.create, { user: guard.user, data });
|
|
3825
4558
|
if (allowed2 !== true) throw deny(guard.user);
|
|
3826
4559
|
if (!fromURL)
|
|
@@ -3975,7 +4708,7 @@ var EasyCMS = class {
|
|
|
3975
4708
|
let parsed;
|
|
3976
4709
|
if (id === null) {
|
|
3977
4710
|
if (guard.enforce) {
|
|
3978
|
-
this.
|
|
4711
|
+
await this.checkGrant(guard, { collection: collection2 }, "create");
|
|
3979
4712
|
const allowed2 = await evaluateAccess(config.access?.create, { user: guard.user, data: raw });
|
|
3980
4713
|
if (allowed2 !== true) throw deny(guard.user);
|
|
3981
4714
|
}
|
|
@@ -4022,17 +4755,17 @@ var EasyCMS = class {
|
|
|
4022
4755
|
return token ? verifyPreviewToken(this.config.secret, token) : null;
|
|
4023
4756
|
}
|
|
4024
4757
|
/** Live preview of a global. See `preview`. */
|
|
4025
|
-
async previewGlobal(
|
|
4026
|
-
const config = this.global(
|
|
4758
|
+
async previewGlobal(slug2, data, options = {}) {
|
|
4759
|
+
const config = this.global(slug2);
|
|
4027
4760
|
const guard = guardOf(options);
|
|
4028
4761
|
await this.checkGlobalAccess(config, "update", guard);
|
|
4029
|
-
const raw = asObject(data,
|
|
4762
|
+
const raw = asObject(data, slug2);
|
|
4030
4763
|
const input = await filterInput(
|
|
4031
4764
|
config.fields,
|
|
4032
4765
|
raw,
|
|
4033
4766
|
this.fieldChecker("update", guard, void 0, raw)
|
|
4034
4767
|
);
|
|
4035
|
-
const saved = await this.db.findGlobal({ slug }) ?? {};
|
|
4768
|
+
const saved = await this.db.findGlobal({ slug: slug2 }) ?? {};
|
|
4036
4769
|
const current = await this.globalDraft(config, saved);
|
|
4037
4770
|
const merged = generateSlugs(
|
|
4038
4771
|
config.fields,
|
|
@@ -4095,7 +4828,7 @@ var EasyCMS = class {
|
|
|
4095
4828
|
if (!existing || parsed === void 0) throw new NotFoundError(collection2, id);
|
|
4096
4829
|
await this.checkDocumentAccess(config, "update", guard, parsed, raw);
|
|
4097
4830
|
if (config.drafts && (mode === "unpublish" || raw.status === "published"))
|
|
4098
|
-
this.
|
|
4831
|
+
await this.checkGrant(guard, { collection: collection2 }, "publish");
|
|
4099
4832
|
const [drafted] = await this.withDrafts(config, [existing]);
|
|
4100
4833
|
const live = options.live === true && mode === "save";
|
|
4101
4834
|
const current = live ? existing : drafted;
|
|
@@ -4202,11 +4935,11 @@ var EasyCMS = class {
|
|
|
4202
4935
|
const [out] = await this.output(config, [existing], guard, { depth: 0, draft: true });
|
|
4203
4936
|
return out;
|
|
4204
4937
|
}
|
|
4205
|
-
async findGlobal(
|
|
4206
|
-
const config = this.global(
|
|
4938
|
+
async findGlobal(slug2, options = {}) {
|
|
4939
|
+
const config = this.global(slug2);
|
|
4207
4940
|
const guard = guardOf(options);
|
|
4208
4941
|
await this.checkGlobalAccess(config, "read", guard);
|
|
4209
|
-
const saved = await this.db.findGlobal({ slug });
|
|
4942
|
+
const saved = await this.db.findGlobal({ slug: slug2 });
|
|
4210
4943
|
const stored = saved && options.draft ? await this.globalDraft(config, saved) : saved;
|
|
4211
4944
|
const data = fillMissing(
|
|
4212
4945
|
config.fields,
|
|
@@ -4220,33 +4953,33 @@ var EasyCMS = class {
|
|
|
4220
4953
|
const { id: _id, ...doc } = out;
|
|
4221
4954
|
return doc;
|
|
4222
4955
|
}
|
|
4223
|
-
async updateGlobal(
|
|
4224
|
-
return this.saveGlobal(
|
|
4956
|
+
async updateGlobal(slug2, data, options = {}) {
|
|
4957
|
+
return this.saveGlobal(slug2, asObject(data, slug2), options);
|
|
4225
4958
|
}
|
|
4226
4959
|
/** Takes a global off the site (`status: 'draft'`). See `unpublish`. */
|
|
4227
|
-
async unpublishGlobal(
|
|
4228
|
-
if (!this.global(
|
|
4229
|
-
return this.saveGlobal(
|
|
4960
|
+
async unpublishGlobal(slug2, options = {}) {
|
|
4961
|
+
if (!this.global(slug2).drafts) throw new QueryError(`"${slug2}" has no drafts`);
|
|
4962
|
+
return this.saveGlobal(slug2, { status: "draft" }, options, "unpublish");
|
|
4230
4963
|
}
|
|
4231
4964
|
/** Throws away the unpublished draft of a published global. */
|
|
4232
|
-
async discardGlobalDraft(
|
|
4233
|
-
const config = this.global(
|
|
4965
|
+
async discardGlobalDraft(slug2, options = {}) {
|
|
4966
|
+
const config = this.global(slug2);
|
|
4234
4967
|
const guard = guardOf(options);
|
|
4235
4968
|
await this.checkGlobalAccess(config, "update", guard);
|
|
4236
|
-
const saved = await this.db.findGlobal({ slug });
|
|
4969
|
+
const saved = await this.db.findGlobal({ slug: slug2 });
|
|
4237
4970
|
if (saved && await this.globalDraft(config, saved) !== saved) {
|
|
4238
|
-
await this.saveVersion(config, globalParent(
|
|
4971
|
+
await this.saveVersion(config, globalParent(slug2), 0, saved, guard);
|
|
4239
4972
|
}
|
|
4240
|
-
return this.findGlobal(
|
|
4973
|
+
return this.findGlobal(slug2, { ...options, draft: true });
|
|
4241
4974
|
}
|
|
4242
4975
|
/** Saved versions of a global, newest first. Needs update access. */
|
|
4243
|
-
async findGlobalVersions(
|
|
4244
|
-
await this.globalVersionTarget(
|
|
4245
|
-
return this.versions.list(globalParent(
|
|
4976
|
+
async findGlobalVersions(slug2, options = {}) {
|
|
4977
|
+
await this.globalVersionTarget(slug2, options);
|
|
4978
|
+
return this.versions.list(globalParent(slug2), 0, options.limit ?? 20, options.page ?? 1);
|
|
4246
4979
|
}
|
|
4247
|
-
async findGlobalVersion(
|
|
4248
|
-
const config = await this.globalVersionTarget(
|
|
4249
|
-
const version = await this.versions.get(globalParent(
|
|
4980
|
+
async findGlobalVersion(slug2, versionId, options = {}) {
|
|
4981
|
+
const config = await this.globalVersionTarget(slug2, options);
|
|
4982
|
+
const version = await this.versions.get(globalParent(slug2), 0, versionId);
|
|
4250
4983
|
if (!version) return null;
|
|
4251
4984
|
const data = fillMissing(
|
|
4252
4985
|
config.fields,
|
|
@@ -4260,21 +4993,21 @@ var EasyCMS = class {
|
|
|
4260
4993
|
return { ...version, data: { ...doc, updatedAt: version.createdAt } };
|
|
4261
4994
|
}
|
|
4262
4995
|
/** Makes an old version of a global current (as a draft when the global has drafts). */
|
|
4263
|
-
async restoreGlobalVersion(
|
|
4264
|
-
const config = await this.globalVersionTarget(
|
|
4265
|
-
const version = await this.versions.get(globalParent(
|
|
4266
|
-
if (!version) throw new NotFoundError(`${
|
|
4996
|
+
async restoreGlobalVersion(slug2, versionId, options = {}) {
|
|
4997
|
+
const config = await this.globalVersionTarget(slug2, options);
|
|
4998
|
+
const version = await this.versions.get(globalParent(slug2), 0, versionId);
|
|
4999
|
+
if (!version) throw new NotFoundError(`${slug2} version`, versionId);
|
|
4267
5000
|
const { status: _status, ...data } = version.data;
|
|
4268
5001
|
return this.saveGlobal(
|
|
4269
|
-
|
|
5002
|
+
slug2,
|
|
4270
5003
|
config.drafts ? { ...data, status: "draft" } : data,
|
|
4271
5004
|
options,
|
|
4272
5005
|
"restore"
|
|
4273
5006
|
);
|
|
4274
5007
|
}
|
|
4275
|
-
async globalVersionTarget(
|
|
4276
|
-
const config = this.global(
|
|
4277
|
-
if (!versionLimit(config)) throw new QueryError(`"${
|
|
5008
|
+
async globalVersionTarget(slug2, options) {
|
|
5009
|
+
const config = this.global(slug2);
|
|
5010
|
+
if (!versionLimit(config)) throw new QueryError(`"${slug2}" has no versions`);
|
|
4278
5011
|
await this.checkGlobalAccess(config, "update", guardOf(options));
|
|
4279
5012
|
return config;
|
|
4280
5013
|
}
|
|
@@ -4285,18 +5018,18 @@ var EasyCMS = class {
|
|
|
4285
5018
|
if (version?.status !== "draft") return saved;
|
|
4286
5019
|
return { ...version.data, updatedAt: version.createdAt, status: "draft" };
|
|
4287
5020
|
}
|
|
4288
|
-
async saveGlobal(
|
|
4289
|
-
const config = this.global(
|
|
5021
|
+
async saveGlobal(slug2, raw, options, mode = "save") {
|
|
5022
|
+
const config = this.global(slug2);
|
|
4290
5023
|
const guard = guardOf(options);
|
|
4291
5024
|
await this.checkGlobalAccess(config, "update", guard);
|
|
4292
5025
|
if (config.drafts && (mode === "unpublish" || raw.status === "published"))
|
|
4293
|
-
this.
|
|
5026
|
+
await this.checkGrant(guard, { global: slug2 }, "publish");
|
|
4294
5027
|
const input = await filterInput(
|
|
4295
5028
|
config.fields,
|
|
4296
5029
|
raw,
|
|
4297
5030
|
this.fieldChecker("update", guard, void 0, raw)
|
|
4298
5031
|
);
|
|
4299
|
-
const existing = await this.db.findGlobal({ slug }) ?? {};
|
|
5032
|
+
const existing = await this.db.findGlobal({ slug: slug2 }) ?? {};
|
|
4300
5033
|
const current = await this.globalDraft(config, existing);
|
|
4301
5034
|
const localized = mode === "restore" ? input : this.toMaps(config, input, current, options);
|
|
4302
5035
|
const merged = generateSlugs(
|
|
@@ -4320,26 +5053,26 @@ var EasyCMS = class {
|
|
|
4320
5053
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4321
5054
|
if (mode !== "unpublish" && this.separateDrafts(config) && existing.status === "published" && prepared.status === "draft") {
|
|
4322
5055
|
const draft = { ...prepared, updatedAt: now };
|
|
4323
|
-
await this.saveVersion(config, globalParent(
|
|
4324
|
-
await this.notify(config.hooks?.afterChange, "afterChange",
|
|
5056
|
+
await this.saveVersion(config, globalParent(slug2), 0, draft, guard);
|
|
5057
|
+
await this.notify(config.hooks?.afterChange, "afterChange", slug2, {
|
|
4325
5058
|
...base,
|
|
4326
5059
|
doc: draft,
|
|
4327
5060
|
previousDoc: current,
|
|
4328
5061
|
operation: "update"
|
|
4329
5062
|
});
|
|
4330
5063
|
this.emit(config, "draft", draft);
|
|
4331
|
-
return this.findGlobal(
|
|
5064
|
+
return this.findGlobal(slug2, { ...options, draft: true });
|
|
4332
5065
|
}
|
|
4333
|
-
const doc = await this.db.updateGlobal({ slug, data: { ...prepared, updatedAt: now } });
|
|
4334
|
-
await this.saveVersion(config, globalParent(
|
|
4335
|
-
await this.notify(config.hooks?.afterChange, "afterChange",
|
|
5066
|
+
const doc = await this.db.updateGlobal({ slug: slug2, data: { ...prepared, updatedAt: now } });
|
|
5067
|
+
await this.saveVersion(config, globalParent(slug2), 0, doc, guard);
|
|
5068
|
+
await this.notify(config.hooks?.afterChange, "afterChange", slug2, {
|
|
4336
5069
|
...base,
|
|
4337
5070
|
doc,
|
|
4338
5071
|
previousDoc: current,
|
|
4339
5072
|
operation: "update"
|
|
4340
5073
|
});
|
|
4341
5074
|
this.emit(config, "update", doc, existing.status);
|
|
4342
|
-
return this.findGlobal(
|
|
5075
|
+
return this.findGlobal(slug2, { ...options, draft: true });
|
|
4343
5076
|
}
|
|
4344
5077
|
/**
|
|
4345
5078
|
* What `user` may do with one document. Resolves `where`-style access
|
|
@@ -4442,6 +5175,9 @@ var EasyCMS = class {
|
|
|
4442
5175
|
async runJobs(now = /* @__PURE__ */ new Date()) {
|
|
4443
5176
|
const scheduled = await this.runScheduled(now);
|
|
4444
5177
|
await this.pruneFailedDeliveries(now);
|
|
5178
|
+
await runDueBackups(this, now).catch(
|
|
5179
|
+
(error) => this.logger.error(`Backups: ${error.message}`)
|
|
5180
|
+
);
|
|
4445
5181
|
return {
|
|
4446
5182
|
...scheduled,
|
|
4447
5183
|
webhooks: await this.retryWebhooks(now),
|
|
@@ -4511,9 +5247,9 @@ var EasyCMS = class {
|
|
|
4511
5247
|
const action = row.action;
|
|
4512
5248
|
try {
|
|
4513
5249
|
if (parent.startsWith("global:")) {
|
|
4514
|
-
const
|
|
4515
|
-
if (action === "publish") await this.saveGlobal(
|
|
4516
|
-
else await this.saveGlobal(
|
|
5250
|
+
const slug2 = parent.slice("global:".length);
|
|
5251
|
+
if (action === "publish") await this.saveGlobal(slug2, { status: "published" }, {});
|
|
5252
|
+
else await this.saveGlobal(slug2, { status: "draft" }, {}, "unpublish");
|
|
4517
5253
|
} else if (action === "publish") {
|
|
4518
5254
|
await this.updateDocument(parent, row.doc, { status: "published" }, { depth: 0 });
|
|
4519
5255
|
} else {
|
|
@@ -4557,17 +5293,17 @@ var EasyCMS = class {
|
|
|
4557
5293
|
const { parsed } = await this.scheduleTarget(collection2, id, options);
|
|
4558
5294
|
await this.removeJob(collection2, parsed, jobId);
|
|
4559
5295
|
}
|
|
4560
|
-
async scheduleGlobal(
|
|
4561
|
-
await this.globalScheduleTarget(
|
|
4562
|
-
return this.addJob(`global:${
|
|
5296
|
+
async scheduleGlobal(slug2, job, options = {}) {
|
|
5297
|
+
await this.globalScheduleTarget(slug2, options);
|
|
5298
|
+
return this.addJob(`global:${slug2}`, 0, job, options);
|
|
4563
5299
|
}
|
|
4564
|
-
async scheduledGlobal(
|
|
4565
|
-
await this.globalScheduleTarget(
|
|
4566
|
-
return this.pendingJobs(`global:${
|
|
5300
|
+
async scheduledGlobal(slug2, options = {}) {
|
|
5301
|
+
await this.globalScheduleTarget(slug2, options);
|
|
5302
|
+
return this.pendingJobs(`global:${slug2}`, 0);
|
|
4567
5303
|
}
|
|
4568
|
-
async cancelGlobalSchedule(
|
|
4569
|
-
await this.globalScheduleTarget(
|
|
4570
|
-
await this.removeJob(`global:${
|
|
5304
|
+
async cancelGlobalSchedule(slug2, jobId, options = {}) {
|
|
5305
|
+
await this.globalScheduleTarget(slug2, options);
|
|
5306
|
+
await this.removeJob(`global:${slug2}`, 0, jobId);
|
|
4571
5307
|
}
|
|
4572
5308
|
async scheduleTarget(collection2, id, options) {
|
|
4573
5309
|
const config = this.collection(collection2);
|
|
@@ -4576,14 +5312,14 @@ var EasyCMS = class {
|
|
|
4576
5312
|
const existing = parsed === void 0 ? null : await this.db.findById({ collection: collection2, id: parsed });
|
|
4577
5313
|
if (!existing || parsed === void 0) throw new NotFoundError(collection2, id);
|
|
4578
5314
|
await this.checkDocumentAccess(config, "update", guardOf(options), parsed, void 0);
|
|
4579
|
-
this.
|
|
5315
|
+
await this.checkGrant(guardOf(options), { collection: collection2 }, "publish");
|
|
4580
5316
|
return { config, parsed };
|
|
4581
5317
|
}
|
|
4582
|
-
async globalScheduleTarget(
|
|
4583
|
-
const config = this.global(
|
|
4584
|
-
if (!config.schedule) throw new QueryError(`"${
|
|
5318
|
+
async globalScheduleTarget(slug2, options) {
|
|
5319
|
+
const config = this.global(slug2);
|
|
5320
|
+
if (!config.schedule) throw new QueryError(`"${slug2}" has no schedule`);
|
|
4585
5321
|
await this.checkGlobalAccess(config, "update", guardOf(options));
|
|
4586
|
-
this.
|
|
5322
|
+
await this.checkGrant(guardOf(options), { global: slug2 }, "publish");
|
|
4587
5323
|
}
|
|
4588
5324
|
async addJob(parent, doc, job, options) {
|
|
4589
5325
|
const runAt = new Date(job.at);
|
|
@@ -4629,20 +5365,21 @@ var EasyCMS = class {
|
|
|
4629
5365
|
page: 1
|
|
4630
5366
|
});
|
|
4631
5367
|
const allowed2 = /* @__PURE__ */ new Map();
|
|
4632
|
-
const
|
|
5368
|
+
const may2 = async (parent) => {
|
|
4633
5369
|
if (!guard.enforce) return true;
|
|
4634
5370
|
const cached = allowed2.get(parent);
|
|
4635
5371
|
if (cached !== void 0) return cached;
|
|
4636
5372
|
const config = parent.startsWith("global:") ? this.config.globals.find((g) => g.slug === parent.slice("global:".length)) : this.config.collections.find((c) => c.slug === parent);
|
|
4637
5373
|
const access = config ? await evaluateAccess(config.access?.update, { user: guard.user }) : false;
|
|
4638
|
-
const
|
|
5374
|
+
const target = parent.startsWith("global:") ? { global: parent.slice("global:".length) } : { collection: parent };
|
|
5375
|
+
const result = access !== false && await this.roles.allows(guard.user, target, "update") !== false;
|
|
4639
5376
|
allowed2.set(parent, result);
|
|
4640
5377
|
return result;
|
|
4641
5378
|
};
|
|
4642
5379
|
const jobs = [];
|
|
4643
5380
|
for (const row of rows.docs) {
|
|
4644
5381
|
const parent = String(row.parent);
|
|
4645
|
-
if (!await
|
|
5382
|
+
if (!await may2(parent)) continue;
|
|
4646
5383
|
const target = parent.startsWith("global:") ? { global: parent.slice("global:".length) } : { collection: parent };
|
|
4647
5384
|
jobs.push({ ...toJob(row), ...target, doc: row.doc });
|
|
4648
5385
|
if (jobs.length === limit) break;
|
|
@@ -4717,38 +5454,46 @@ var EasyCMS = class {
|
|
|
4717
5454
|
}
|
|
4718
5455
|
// -------------------------------------------------------------------------
|
|
4719
5456
|
/** @internal */
|
|
4720
|
-
collection(
|
|
4721
|
-
const config = this.config.collections.find((c) => c.slug ===
|
|
4722
|
-
if (!config) throw new QueryError(`Unknown collection "${
|
|
5457
|
+
collection(slug2) {
|
|
5458
|
+
const config = this.config.collections.find((c) => c.slug === slug2);
|
|
5459
|
+
if (!config) throw new QueryError(`Unknown collection "${slug2}"`);
|
|
4723
5460
|
return config;
|
|
4724
5461
|
}
|
|
4725
|
-
global(
|
|
4726
|
-
const config = this.config.globals.find((g) => g.slug ===
|
|
4727
|
-
if (!config) throw new QueryError(`Unknown global "${
|
|
5462
|
+
global(slug2) {
|
|
5463
|
+
const config = this.config.globals.find((g) => g.slug === slug2);
|
|
5464
|
+
if (!config) throw new QueryError(`Unknown global "${slug2}"`);
|
|
4728
5465
|
return config;
|
|
4729
5466
|
}
|
|
4730
|
-
/**
|
|
4731
|
-
|
|
4732
|
-
|
|
5467
|
+
/**
|
|
5468
|
+
* Refuses what a request's API key (see `ApiKeyPermissions`) or the user's role (`auth.rbac`)
|
|
5469
|
+
* does not allow. Returns a constraint when the role allows only the user's own account.
|
|
5470
|
+
*/
|
|
5471
|
+
async checkGrant(guard, target, operation) {
|
|
5472
|
+
if (!guard.enforce) return true;
|
|
4733
5473
|
const name = "collection" in target ? target.collection : target.global;
|
|
4734
|
-
|
|
5474
|
+
if (!keyAllows(guard.user, target, operation))
|
|
5475
|
+
throw new ForbiddenError(`This API key may not ${operation} "${name}"`);
|
|
5476
|
+
const role = await this.roles.allows(guard.user, target, operation);
|
|
5477
|
+
if (role === false) throw deny(guard.user);
|
|
5478
|
+
return role;
|
|
4735
5479
|
}
|
|
4736
5480
|
/** The query constraint read access adds, or `undefined` when there is none. Throws when denied. */
|
|
4737
5481
|
async readWhere(config, guard, where) {
|
|
4738
5482
|
if (!guard.enforce) return where;
|
|
4739
|
-
this.
|
|
5483
|
+
const own = await this.checkGrant(guard, { collection: config.slug }, "read");
|
|
4740
5484
|
const access = await evaluateAccess(config.access?.read, { user: guard.user });
|
|
4741
5485
|
if (access === false) throw deny(guard.user);
|
|
4742
|
-
return andWhere(where, access);
|
|
5486
|
+
return andWhere(andWhere(where, access), own);
|
|
4743
5487
|
}
|
|
4744
5488
|
async checkDocumentAccess(config, operation, guard, id, data) {
|
|
4745
5489
|
if (!guard.enforce) return;
|
|
4746
|
-
this.
|
|
4747
|
-
const
|
|
5490
|
+
const own = await this.checkGrant(guard, { collection: config.slug }, operation);
|
|
5491
|
+
const allowed2 = await evaluateAccess(config.access?.[operation], {
|
|
4748
5492
|
user: guard.user,
|
|
4749
5493
|
id,
|
|
4750
5494
|
...data ? { data } : {}
|
|
4751
5495
|
});
|
|
5496
|
+
const access = allowed2 === false ? false : andWhere(own === true ? void 0 : own, allowed2) ?? true;
|
|
4752
5497
|
if (access === true) return;
|
|
4753
5498
|
if (access !== false) {
|
|
4754
5499
|
const matches2 = await this.db.count({
|
|
@@ -4761,7 +5506,7 @@ var EasyCMS = class {
|
|
|
4761
5506
|
}
|
|
4762
5507
|
async checkGlobalAccess(config, operation, guard) {
|
|
4763
5508
|
if (!guard.enforce) return;
|
|
4764
|
-
this.
|
|
5509
|
+
await this.checkGrant(guard, { global: config.slug }, operation);
|
|
4765
5510
|
const access = await evaluateAccess(config.access?.[operation], { user: guard.user });
|
|
4766
5511
|
if (typeof access === "object")
|
|
4767
5512
|
throw new QueryError(`${operation} access of global "${config.slug}" must return a boolean`);
|
|
@@ -4815,9 +5560,9 @@ var EasyCMS = class {
|
|
|
4815
5560
|
const guard = guardOf(options);
|
|
4816
5561
|
const collection2 = config.slug;
|
|
4817
5562
|
if (guard.enforce) {
|
|
4818
|
-
this.
|
|
5563
|
+
await this.checkGrant(guard, { collection: collection2 }, "create");
|
|
4819
5564
|
if (config.drafts && raw.status === "published")
|
|
4820
|
-
this.
|
|
5565
|
+
await this.checkGrant(guard, { collection: collection2 }, "publish");
|
|
4821
5566
|
const allowed2 = await evaluateAccess(config.access?.create, { user: guard.user, data: raw });
|
|
4822
5567
|
if (typeof allowed2 === "object")
|
|
4823
5568
|
throw new QueryError(`create access of "${collection2}" must return a boolean`);
|
|
@@ -4974,13 +5719,13 @@ var EasyCMS = class {
|
|
|
4974
5719
|
return current;
|
|
4975
5720
|
}
|
|
4976
5721
|
/** Runs hooks after the change is saved: failures are logged, never undo the save (NFR-REL-03). */
|
|
4977
|
-
async notify(hooks, name,
|
|
5722
|
+
async notify(hooks, name, slug2, args) {
|
|
4978
5723
|
for (const hook of hooks ?? []) {
|
|
4979
5724
|
try {
|
|
4980
5725
|
await hook(args);
|
|
4981
5726
|
} catch (error) {
|
|
4982
5727
|
this.logger.error(
|
|
4983
|
-
`${name} hook of "${
|
|
5728
|
+
`${name} hook of "${slug2}" failed: ${error instanceof Error ? error.message : String(error)}`
|
|
4984
5729
|
);
|
|
4985
5730
|
}
|
|
4986
5731
|
}
|
|
@@ -5036,6 +5781,8 @@ var EasyCMS = class {
|
|
|
5036
5781
|
}
|
|
5037
5782
|
clean2.status = status;
|
|
5038
5783
|
}
|
|
5784
|
+
if (config.slug === USERS && this.roles.enabled && typeof clean2.role === "string" && !await this.roles.exists(clean2.role))
|
|
5785
|
+
errors.push({ field: "role", message: `there is no role "${clean2.role}"` });
|
|
5039
5786
|
const isCollection = this.config.collections.includes(config);
|
|
5040
5787
|
if (isCollection && errors.length === 0) {
|
|
5041
5788
|
await this.makeSlugsUnique(config, clean2, selfId);
|
|
@@ -5082,9 +5829,9 @@ var EasyCMS = class {
|
|
|
5082
5829
|
const value = data[field.name];
|
|
5083
5830
|
if (field.localized && value && typeof value === "object") {
|
|
5084
5831
|
const map = { ...value };
|
|
5085
|
-
for (const [locale,
|
|
5086
|
-
if (typeof
|
|
5087
|
-
map[locale] = await unique(`${field.name}.${locale}`,
|
|
5832
|
+
for (const [locale, slug2] of Object.entries(map)) {
|
|
5833
|
+
if (typeof slug2 === "string" && slug2 !== "")
|
|
5834
|
+
map[locale] = await unique(`${field.name}.${locale}`, slug2);
|
|
5088
5835
|
}
|
|
5089
5836
|
data[field.name] = map;
|
|
5090
5837
|
} else if (typeof value === "string" && value !== "") {
|
|
@@ -5122,11 +5869,11 @@ var EasyCMS = class {
|
|
|
5122
5869
|
return await this.db.count({ collection: collection2, where }) > 0;
|
|
5123
5870
|
}
|
|
5124
5871
|
/** Each reference is one its relationship's `filterOptions` allow. */
|
|
5125
|
-
async checkFilterOptions(
|
|
5872
|
+
async checkFilterOptions(references2, selfId, user) {
|
|
5126
5873
|
const errors = [];
|
|
5127
5874
|
const cms = this;
|
|
5128
5875
|
const allowed2 = /* @__PURE__ */ new Map();
|
|
5129
|
-
for (const ref of
|
|
5876
|
+
for (const ref of references2) {
|
|
5130
5877
|
const filter = ref.filterOptions;
|
|
5131
5878
|
if (!filter) continue;
|
|
5132
5879
|
if (!allowed2.has(filter)) allowed2.set(filter, await filter({ id: selfId, user, cms }));
|
|
@@ -5144,9 +5891,9 @@ var EasyCMS = class {
|
|
|
5144
5891
|
}
|
|
5145
5892
|
return errors;
|
|
5146
5893
|
}
|
|
5147
|
-
async checkReferences(
|
|
5894
|
+
async checkReferences(references2) {
|
|
5148
5895
|
const byCollection = /* @__PURE__ */ new Map();
|
|
5149
|
-
for (const ref of
|
|
5896
|
+
for (const ref of references2) {
|
|
5150
5897
|
if (!this.config.collections.some((c) => c.slug === ref.collection)) continue;
|
|
5151
5898
|
byCollection.set(ref.collection, [...byCollection.get(ref.collection) ?? [], ref]);
|
|
5152
5899
|
}
|
|
@@ -5223,11 +5970,11 @@ function asObject(data, name) {
|
|
|
5223
5970
|
|
|
5224
5971
|
// src/rest/admin-modules.ts
|
|
5225
5972
|
import { createHash as createHash3 } from "crypto";
|
|
5226
|
-
import { readFile as
|
|
5973
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
5227
5974
|
import { createRequire } from "module";
|
|
5228
|
-
import { join as
|
|
5975
|
+
import { join as join3 } from "path";
|
|
5229
5976
|
function resolveAdminModule(specifier, cwd = process.cwd()) {
|
|
5230
|
-
const bases = [
|
|
5977
|
+
const bases = [join3(cwd, "package.json")];
|
|
5231
5978
|
const entry = process.argv[1];
|
|
5232
5979
|
if (entry && !specifier.startsWith(".")) bases.push(entry);
|
|
5233
5980
|
for (const base of bases) {
|
|
@@ -5261,7 +6008,7 @@ function adminModuleUrls(cms) {
|
|
|
5261
6008
|
async function readAdminModule(cms, index) {
|
|
5262
6009
|
const file = files(cms)[index];
|
|
5263
6010
|
if (!file) return void 0;
|
|
5264
|
-
const body = await
|
|
6011
|
+
const body = await readFile3(file, "utf8").catch(() => void 0);
|
|
5265
6012
|
if (body === void 0) return void 0;
|
|
5266
6013
|
return { body, etag: `"${createHash3("sha1").update(body).digest("base64url")}"` };
|
|
5267
6014
|
}
|
|
@@ -5279,7 +6026,7 @@ function parse(text) {
|
|
|
5279
6026
|
return {};
|
|
5280
6027
|
}
|
|
5281
6028
|
}
|
|
5282
|
-
function
|
|
6029
|
+
function toAdmin2(kind, row) {
|
|
5283
6030
|
const base = {
|
|
5284
6031
|
id: row.id,
|
|
5285
6032
|
state: row.state === "failed" ? "failed" : "pending",
|
|
@@ -5327,7 +6074,7 @@ async function listDeliveries(cms, kind, state, page) {
|
|
|
5327
6074
|
cms.db.count({ collection: collection2, where: { state: { equals: "pending" } } })
|
|
5328
6075
|
]);
|
|
5329
6076
|
return {
|
|
5330
|
-
docs: found.docs.map((row) =>
|
|
6077
|
+
docs: found.docs.map((row) => toAdmin2(kind, row)),
|
|
5331
6078
|
totalDocs: found.totalDocs,
|
|
5332
6079
|
page: found.page,
|
|
5333
6080
|
totalPages: found.totalPages,
|
|
@@ -5468,7 +6215,7 @@ function componentRef(component) {
|
|
|
5468
6215
|
async function allowed(access, user) {
|
|
5469
6216
|
return await evaluateAccess(access, { user }) !== false;
|
|
5470
6217
|
}
|
|
5471
|
-
async function serializeFields(fields,
|
|
6218
|
+
async function serializeFields(fields, update2, localized) {
|
|
5472
6219
|
const out = [];
|
|
5473
6220
|
for (const field of fields) {
|
|
5474
6221
|
if (field.hidden) continue;
|
|
@@ -5477,7 +6224,7 @@ async function serializeFields(fields, update, localized) {
|
|
|
5477
6224
|
if (field.required) f.required = true;
|
|
5478
6225
|
if (field.unique) f.unique = true;
|
|
5479
6226
|
if (field.defaultValue !== void 0) f.defaultValue = field.defaultValue;
|
|
5480
|
-
if (!await
|
|
6227
|
+
if (!await update2.allows(field)) f.readOnly = true;
|
|
5481
6228
|
if (field.localized && localized) f.localized = true;
|
|
5482
6229
|
if (field.position === "sidebar") f.position = "sidebar";
|
|
5483
6230
|
if (field.admin?.component || field.admin?.after?.length || field.admin?.cell) {
|
|
@@ -5524,7 +6271,7 @@ async function serializeFields(fields, update, localized) {
|
|
|
5524
6271
|
case "array":
|
|
5525
6272
|
if (field.minRows !== void 0) f.minRows = field.minRows;
|
|
5526
6273
|
if (field.maxRows !== void 0) f.maxRows = field.maxRows;
|
|
5527
|
-
f.fields = await serializeFields(field.fields,
|
|
6274
|
+
f.fields = await serializeFields(field.fields, update2, localized);
|
|
5528
6275
|
break;
|
|
5529
6276
|
case "blocks":
|
|
5530
6277
|
if (field.minRows !== void 0) f.minRows = field.minRows;
|
|
@@ -5533,35 +6280,48 @@ async function serializeFields(fields, update, localized) {
|
|
|
5533
6280
|
field.blocks.map(async (block) => ({
|
|
5534
6281
|
slug: block.slug,
|
|
5535
6282
|
...block.labels ? { labels: block.labels } : {},
|
|
5536
|
-
fields: await serializeFields(block.fields,
|
|
6283
|
+
fields: await serializeFields(block.fields, update2, localized)
|
|
5537
6284
|
}))
|
|
5538
6285
|
);
|
|
5539
6286
|
break;
|
|
5540
6287
|
case "group":
|
|
5541
|
-
f.fields = await serializeFields(field.fields,
|
|
6288
|
+
f.fields = await serializeFields(field.fields, update2, localized);
|
|
5542
6289
|
break;
|
|
5543
6290
|
}
|
|
5544
6291
|
out.push(f);
|
|
5545
6292
|
}
|
|
5546
6293
|
return out;
|
|
5547
6294
|
}
|
|
5548
|
-
async function
|
|
6295
|
+
async function may(cms, access, user, target, operation) {
|
|
6296
|
+
return await allowed(access, user) && await cms.roles.allows(user, target, operation) !== false;
|
|
6297
|
+
}
|
|
6298
|
+
async function collection(cms, config, user, localized, roles) {
|
|
6299
|
+
const target = { collection: config.slug };
|
|
6300
|
+
const fields = await serializeFields(
|
|
6301
|
+
config.fields,
|
|
6302
|
+
new FieldAccessChecker("update", { user }),
|
|
6303
|
+
localized
|
|
6304
|
+
);
|
|
6305
|
+
if (config.slug === USERS && cms.roles.enabled) {
|
|
6306
|
+
const role = fields.find((f) => f.name === "role");
|
|
6307
|
+
if (role) {
|
|
6308
|
+
role.type = "select";
|
|
6309
|
+
role.options = roles.map((r) => ({ label: r.name || r.key, value: r.key }));
|
|
6310
|
+
}
|
|
6311
|
+
}
|
|
5549
6312
|
const result = {
|
|
5550
6313
|
slug: config.slug,
|
|
5551
6314
|
drafts: config.drafts === true,
|
|
5552
6315
|
versions: Boolean(config.versions),
|
|
5553
6316
|
preview: typeof config.preview === "function",
|
|
5554
6317
|
schedule: config.schedule === true,
|
|
5555
|
-
fields
|
|
5556
|
-
config.fields,
|
|
5557
|
-
new FieldAccessChecker("update", { user }),
|
|
5558
|
-
localized
|
|
5559
|
-
),
|
|
6318
|
+
fields,
|
|
5560
6319
|
permissions: {
|
|
5561
|
-
read: await
|
|
5562
|
-
create: await
|
|
5563
|
-
update: await
|
|
5564
|
-
delete: await
|
|
6320
|
+
read: await may(cms, config.access?.read, user, target, "read"),
|
|
6321
|
+
create: await may(cms, config.access?.create, user, target, "create"),
|
|
6322
|
+
update: await may(cms, config.access?.update, user, target, "update"),
|
|
6323
|
+
delete: await may(cms, config.access?.delete, user, target, "delete"),
|
|
6324
|
+
publish: config.drafts === true && await may(cms, config.access?.update, user, target, "publish")
|
|
5565
6325
|
}
|
|
5566
6326
|
};
|
|
5567
6327
|
if (config.labels) result.labels = config.labels;
|
|
@@ -5575,7 +6335,8 @@ async function collection(config, user, localized) {
|
|
|
5575
6335
|
result.editIn = "drawer";
|
|
5576
6336
|
return result;
|
|
5577
6337
|
}
|
|
5578
|
-
async function global(config, user, localized) {
|
|
6338
|
+
async function global(cms, config, user, localized) {
|
|
6339
|
+
const target = { global: config.slug };
|
|
5579
6340
|
const result = {
|
|
5580
6341
|
slug: config.slug,
|
|
5581
6342
|
drafts: config.drafts === true,
|
|
@@ -5588,8 +6349,9 @@ async function global(config, user, localized) {
|
|
|
5588
6349
|
localized
|
|
5589
6350
|
),
|
|
5590
6351
|
permissions: {
|
|
5591
|
-
read: await
|
|
5592
|
-
update: await
|
|
6352
|
+
read: await may(cms, config.access?.read, user, target, "read"),
|
|
6353
|
+
update: await may(cms, config.access?.update, user, target, "update"),
|
|
6354
|
+
publish: config.drafts === true && await may(cms, config.access?.update, user, target, "publish")
|
|
5593
6355
|
}
|
|
5594
6356
|
};
|
|
5595
6357
|
if (config.label !== void 0) result.label = config.label;
|
|
@@ -5601,16 +6363,31 @@ async function adminSchema(cms, user, origin) {
|
|
|
5601
6363
|
const collections = cms.config.collections.filter((c) => !INTERNAL_COLLECTIONS.has(c.slug));
|
|
5602
6364
|
const localization = cms.config.localization;
|
|
5603
6365
|
const localized = localization !== null;
|
|
6366
|
+
const admin = user.role === "admin" && !user.apiKey;
|
|
6367
|
+
const deliveries = cms.config.collections.some(
|
|
6368
|
+
(c) => c.slug === WEBHOOK_DELIVERIES || c.slug === EMAIL_DELIVERIES
|
|
6369
|
+
);
|
|
6370
|
+
const views = {
|
|
6371
|
+
status: await cms.roles.canView(user, "status"),
|
|
6372
|
+
deliveries: deliveries && await cms.roles.canView(user, "deliveries"),
|
|
6373
|
+
backups: admin,
|
|
6374
|
+
email: admin,
|
|
6375
|
+
roles: admin && cms.roles.enabled
|
|
6376
|
+
};
|
|
6377
|
+
const roles = await cms.roles.options();
|
|
5604
6378
|
return {
|
|
5605
6379
|
locale: cms.config.admin.locale,
|
|
5606
6380
|
passwordLinks: cms.auth.canSendPasswordLinks(origin),
|
|
5607
6381
|
menu: [...cms.config.admin.menu],
|
|
5608
6382
|
localization: localization ? { locales: [...localization.locales], defaultLocale: localization.defaultLocale } : null,
|
|
5609
|
-
collections: await Promise.all(
|
|
5610
|
-
|
|
6383
|
+
collections: await Promise.all(
|
|
6384
|
+
collections.map((c) => collection(cms, c, user, localized, roles))
|
|
6385
|
+
),
|
|
6386
|
+
globals: await Promise.all(cms.config.globals.map((g) => global(cms, g, user, localized))),
|
|
5611
6387
|
modules: adminModuleUrls(cms),
|
|
5612
6388
|
uploadFromURL: cms.config.upload.fromURL !== void 0,
|
|
5613
|
-
|
|
6389
|
+
views,
|
|
6390
|
+
...views.deliveries ? {
|
|
5614
6391
|
deliveries: {
|
|
5615
6392
|
webhook: cms.config.collections.some((c) => c.slug === WEBHOOK_DELIVERIES),
|
|
5616
6393
|
email: cms.config.collections.some((c) => c.slug === EMAIL_DELIVERIES)
|
|
@@ -5631,7 +6408,8 @@ async function shown(access, user) {
|
|
|
5631
6408
|
async function pages(cms, user) {
|
|
5632
6409
|
const out = [];
|
|
5633
6410
|
for (const page of cms.config.admin.pages) {
|
|
5634
|
-
if (!await shown(page.access, user))
|
|
6411
|
+
if (!await shown(page.access, user) || !await cms.roles.canView(user, `page:${page.path}`))
|
|
6412
|
+
continue;
|
|
5635
6413
|
out.push({
|
|
5636
6414
|
path: page.path,
|
|
5637
6415
|
label: page.label,
|
|
@@ -5645,18 +6423,20 @@ async function pages(cms, user) {
|
|
|
5645
6423
|
async function widgets(cms, user) {
|
|
5646
6424
|
const out = [];
|
|
5647
6425
|
for (const widget of cms.config.admin.dashboard) {
|
|
5648
|
-
|
|
6426
|
+
const tag = typeof widget.component === "string" ? widget.component : widget.component.tag;
|
|
6427
|
+
if (!await shown(widget.access, user) || !await cms.roles.canView(user, `widget:${tag}`))
|
|
6428
|
+
continue;
|
|
5649
6429
|
out.push({ component: componentRef(widget.component), width: widget.width ?? "half" });
|
|
5650
6430
|
}
|
|
5651
6431
|
return out;
|
|
5652
6432
|
}
|
|
5653
6433
|
|
|
5654
6434
|
// src/version.ts
|
|
5655
|
-
var VERSION = true ? "0.
|
|
6435
|
+
var VERSION = true ? "0.33.0" : "0.0.0-dev";
|
|
5656
6436
|
|
|
5657
6437
|
// src/rest/admin-status.ts
|
|
5658
6438
|
var DAY = 864e5;
|
|
5659
|
-
var has = (cms,
|
|
6439
|
+
var has = (cms, slug2) => cms.config.collections.some((c) => c.slug === slug2);
|
|
5660
6440
|
async function adminStatus(cms) {
|
|
5661
6441
|
const config = cms.config;
|
|
5662
6442
|
const attention = [];
|
|
@@ -5709,6 +6489,8 @@ async function adminStatus(cms) {
|
|
|
5709
6489
|
});
|
|
5710
6490
|
if (count > 0) attention.push({ id: "scheduled", count });
|
|
5711
6491
|
}
|
|
6492
|
+
const backups = await backupAttention(cms);
|
|
6493
|
+
if (backups) attention.push(backups);
|
|
5712
6494
|
if (!config.email) attention.push({ id: "no-email" });
|
|
5713
6495
|
if (!config.serverURL && process.env.NODE_ENV === "production")
|
|
5714
6496
|
attention.push({ id: "no-server-url" });
|
|
@@ -5743,8 +6525,8 @@ async function pickerFilter(cms, url, target, user) {
|
|
|
5743
6525
|
const spec = forCollection ?? forGlobal;
|
|
5744
6526
|
if (spec === null) return void 0;
|
|
5745
6527
|
const dot = spec.indexOf(".");
|
|
5746
|
-
const
|
|
5747
|
-
const owner = forCollection !== null ? cms.config.collections.find((c) => c.slug ===
|
|
6528
|
+
const slug2 = dot > 0 ? spec.slice(0, dot) : "";
|
|
6529
|
+
const owner = forCollection !== null ? cms.config.collections.find((c) => c.slug === slug2) : cms.config.globals.find((g) => g.slug === slug2);
|
|
5748
6530
|
const field = owner ? fieldAt(owner.fields, spec.slice(dot + 1)) : void 0;
|
|
5749
6531
|
if (field?.type !== "relationship" || field.to !== target)
|
|
5750
6532
|
throw new QueryError(`filterFor: no relationship to "${target}" at "${spec}"`);
|
|
@@ -6092,6 +6874,38 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
6092
6874
|
}
|
|
6093
6875
|
return { body: await cms.runJobs() };
|
|
6094
6876
|
}
|
|
6877
|
+
if (first === "admin" && second === "backups") {
|
|
6878
|
+
if (!ctx.user) throw new UnauthorizedError();
|
|
6879
|
+
if (ctx.user.role !== "admin" || ctx.user.apiKey) throw new ForbiddenError();
|
|
6880
|
+
const [, , id2, action, extra] = segments;
|
|
6881
|
+
if (extra !== void 0) throw new HttpError("Not found", 404);
|
|
6882
|
+
if (id2 === void 0) {
|
|
6883
|
+
if (method === "GET") return { body: await listBackups(cms) };
|
|
6884
|
+
if (method === "POST")
|
|
6885
|
+
return { status: 202, body: await startBackup(cms, "manual", ctx.user.email) };
|
|
6886
|
+
throw methodNotAllowed(ctx, "GET, POST");
|
|
6887
|
+
}
|
|
6888
|
+
const parsed = parseId(id2);
|
|
6889
|
+
if (parsed === void 0) throw new HttpError("Not found", 404);
|
|
6890
|
+
if (action === "download") {
|
|
6891
|
+
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
6892
|
+
const file = await downloadBackup(cms, parsed, ctx.user.email);
|
|
6893
|
+
return {
|
|
6894
|
+
body: new Response(file.body, {
|
|
6895
|
+
headers: {
|
|
6896
|
+
"content-type": "application/gzip",
|
|
6897
|
+
"content-disposition": `attachment; filename="${file.filename}"`,
|
|
6898
|
+
"cache-control": "no-store",
|
|
6899
|
+
"x-content-type-options": "nosniff"
|
|
6900
|
+
}
|
|
6901
|
+
})
|
|
6902
|
+
};
|
|
6903
|
+
}
|
|
6904
|
+
if (action !== void 0) throw new HttpError("Not found", 404);
|
|
6905
|
+
if (method !== "DELETE") throw methodNotAllowed(ctx, "DELETE");
|
|
6906
|
+
await deleteBackup(cms, parsed);
|
|
6907
|
+
return { body: { deleted: 1 } };
|
|
6908
|
+
}
|
|
6095
6909
|
if (first === "admin" && second === "email") {
|
|
6096
6910
|
if (!ctx.user) throw new UnauthorizedError();
|
|
6097
6911
|
if (ctx.user.role !== "admin" || ctx.user.apiKey) throw new ForbiddenError();
|
|
@@ -6109,9 +6923,36 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
6109
6923
|
}
|
|
6110
6924
|
throw new HttpError("Not found", 404);
|
|
6111
6925
|
}
|
|
6112
|
-
if (first === "admin" && second === "
|
|
6926
|
+
if (first === "admin" && second === "roles") {
|
|
6113
6927
|
if (!ctx.user) throw new UnauthorizedError();
|
|
6114
6928
|
if (ctx.user.role !== "admin" || ctx.user.apiKey) throw new ForbiddenError();
|
|
6929
|
+
if (!cms.roles.enabled) throw new HttpError("Not found", 404);
|
|
6930
|
+
const [, , id2, action, extra] = segments;
|
|
6931
|
+
if (extra !== void 0) throw new HttpError("Not found", 404);
|
|
6932
|
+
if (id2 === void 0) {
|
|
6933
|
+
if (method === "GET") return { body: await cms.roles.list() };
|
|
6934
|
+
if (method === "POST")
|
|
6935
|
+
return { status: 201, body: await cms.roles.create(await readJson(ctx.request), ctx.user) };
|
|
6936
|
+
throw methodNotAllowed(ctx, "GET, POST");
|
|
6937
|
+
}
|
|
6938
|
+
const parsed = parseId(id2);
|
|
6939
|
+
if (parsed === void 0) throw new HttpError("Not found", 404);
|
|
6940
|
+
if (action === "history") {
|
|
6941
|
+
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
6942
|
+
return { body: await cms.roles.history(parsed) };
|
|
6943
|
+
}
|
|
6944
|
+
if (action !== void 0) throw new HttpError("Not found", 404);
|
|
6945
|
+
if (method === "PATCH")
|
|
6946
|
+
return { body: await cms.roles.update(parsed, await readJson(ctx.request), ctx.user) };
|
|
6947
|
+
if (method === "DELETE") {
|
|
6948
|
+
await cms.roles.delete(parsed);
|
|
6949
|
+
return { body: { deleted: 1 } };
|
|
6950
|
+
}
|
|
6951
|
+
throw methodNotAllowed(ctx, "PATCH, DELETE");
|
|
6952
|
+
}
|
|
6953
|
+
if (first === "admin" && second === "deliveries") {
|
|
6954
|
+
if (!ctx.user) throw new UnauthorizedError();
|
|
6955
|
+
if (!await cms.roles.canView(ctx.user, "deliveries")) throw new ForbiddenError();
|
|
6115
6956
|
const [, , kind, id2, action, extra] = segments;
|
|
6116
6957
|
if (kind === void 0) {
|
|
6117
6958
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
@@ -6161,7 +7002,7 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
6161
7002
|
return { body: new Response(file.body, { headers }) };
|
|
6162
7003
|
}
|
|
6163
7004
|
if (second === "status" && third === void 0) {
|
|
6164
|
-
if (ctx.user
|
|
7005
|
+
if (!await cms.roles.canView(ctx.user, "status")) throw new ForbiddenError();
|
|
6165
7006
|
return { body: await adminStatus(cms) };
|
|
6166
7007
|
}
|
|
6167
7008
|
if (second === "scheduled" && third === void 0)
|
|
@@ -6348,47 +7189,47 @@ async function documentAction(cms, ctx, method, collection2, id, path) {
|
|
|
6348
7189
|
}
|
|
6349
7190
|
throw new HttpError("Not found", 404);
|
|
6350
7191
|
}
|
|
6351
|
-
async function globalAction(cms, ctx, method,
|
|
7192
|
+
async function globalAction(cms, ctx, method, slug2, path) {
|
|
6352
7193
|
const access = { overrideAccess: false, user: ctx.user, ...parseLocale(ctx.url) };
|
|
6353
7194
|
const depth = parseDepth(ctx.url);
|
|
6354
7195
|
const [action, versionId, extra] = path;
|
|
6355
7196
|
if (action === "versions" && versionId === void 0) {
|
|
6356
7197
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
6357
|
-
return { body: await cms.findGlobalVersions(
|
|
7198
|
+
return { body: await cms.findGlobalVersions(slug2, { ...access, ...parsePage(ctx.url) }) };
|
|
6358
7199
|
}
|
|
6359
7200
|
if (action === "versions" && versionId !== void 0 && extra === void 0) {
|
|
6360
7201
|
if (method !== "GET") throw methodNotAllowed(ctx, "GET");
|
|
6361
|
-
const version = await cms.findGlobalVersion(
|
|
6362
|
-
if (!version) throw new NotFoundError(`${
|
|
7202
|
+
const version = await cms.findGlobalVersion(slug2, versionId, { ...access, ...depth });
|
|
7203
|
+
if (!version) throw new NotFoundError(`${slug2} version`, versionId);
|
|
6363
7204
|
return { body: version };
|
|
6364
7205
|
}
|
|
6365
7206
|
if (action === "versions" && versionId !== void 0 && extra === "restore" && path.length === 3) {
|
|
6366
7207
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
6367
|
-
return { body: await cms.restoreGlobalVersion(
|
|
7208
|
+
return { body: await cms.restoreGlobalVersion(slug2, versionId, { ...access, ...depth }) };
|
|
6368
7209
|
}
|
|
6369
7210
|
if (action === "schedule" && path.length <= 2) {
|
|
6370
7211
|
if (path.length === 2) {
|
|
6371
7212
|
if (method !== "DELETE") throw methodNotAllowed(ctx, "DELETE");
|
|
6372
|
-
await cms.cancelGlobalSchedule(
|
|
7213
|
+
await cms.cancelGlobalSchedule(slug2, versionId, access);
|
|
6373
7214
|
return { body: { message: "Cancelled" } };
|
|
6374
7215
|
}
|
|
6375
|
-
if (method === "GET") return { body: await cms.scheduledGlobal(
|
|
7216
|
+
if (method === "GET") return { body: await cms.scheduledGlobal(slug2, access) };
|
|
6376
7217
|
if (method !== "POST") throw methodNotAllowed(ctx, "GET, POST");
|
|
6377
7218
|
const body = await readJson(ctx.request);
|
|
6378
|
-
return { status: 201, body: await cms.scheduleGlobal(
|
|
7219
|
+
return { status: 201, body: await cms.scheduleGlobal(slug2, scheduleJob(body), access) };
|
|
6379
7220
|
}
|
|
6380
7221
|
if (path.length === 1 && action === "preview") {
|
|
6381
7222
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
6382
7223
|
const body = await readJson(ctx.request);
|
|
6383
|
-
const result = await cms.previewGlobal(
|
|
6384
|
-
const token = cms.createPreviewToken({ global:
|
|
7224
|
+
const result = await cms.previewGlobal(slug2, body, { ...access, ...depth });
|
|
7225
|
+
const token = cms.createPreviewToken({ global: slug2 });
|
|
6385
7226
|
return { body: { ...result, url: result.url && withPreviewToken(result.url, token) } };
|
|
6386
7227
|
}
|
|
6387
7228
|
if (path.length === 1 && (action === "unpublish" || action === "discard-draft")) {
|
|
6388
7229
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
6389
7230
|
const options = { ...access, ...depth };
|
|
6390
7231
|
return {
|
|
6391
|
-
body: action === "unpublish" ? await cms.unpublishGlobal(
|
|
7232
|
+
body: action === "unpublish" ? await cms.unpublishGlobal(slug2, options) : await cms.discardGlobalDraft(slug2, options)
|
|
6392
7233
|
};
|
|
6393
7234
|
}
|
|
6394
7235
|
throw new HttpError("Not found", 404);
|
|
@@ -6721,7 +7562,7 @@ function generateTypes(config) {
|
|
|
6721
7562
|
for (const global2 of config.globals) {
|
|
6722
7563
|
parts.push("", globalInterface(global2, n.globals.get(global2.slug), n));
|
|
6723
7564
|
}
|
|
6724
|
-
const map = (entries) => [...entries].map(([
|
|
7565
|
+
const map = (entries) => [...entries].map(([slug2, name]) => ` ${key(slug2)}: ${name}`).join("\n");
|
|
6725
7566
|
parts.push(
|
|
6726
7567
|
"",
|
|
6727
7568
|
"/** Document type by collection slug. */",
|
|
@@ -6771,6 +7612,8 @@ export {
|
|
|
6771
7612
|
NotFoundError,
|
|
6772
7613
|
PayloadTooLargeError,
|
|
6773
7614
|
QueryError,
|
|
7615
|
+
ROLE_OPERATIONS,
|
|
7616
|
+
Roles,
|
|
6774
7617
|
SESSION_COOKIE,
|
|
6775
7618
|
SchemaError,
|
|
6776
7619
|
TooManyRequestsError,
|
|
@@ -6820,5 +7663,6 @@ export {
|
|
|
6820
7663
|
sniffMimeType,
|
|
6821
7664
|
validateConfig,
|
|
6822
7665
|
validateFields,
|
|
6823
|
-
verifyPassword
|
|
7666
|
+
verifyPassword,
|
|
7667
|
+
writeBackupFile
|
|
6824
7668
|
};
|