@azlib/cms 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1573 -53
- package/dist/index.d.cts +376 -19
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +376 -19
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1561 -54
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -3
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
21
|
enumerable: true
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
|
+
let _azlib_validator = require("@azlib/validator");
|
|
25
|
+
let node_fs = require("node:fs");
|
|
26
|
+
let node_path = require("node:path");
|
|
27
|
+
node_path = __toESM(node_path, 1);
|
|
28
|
+
let _azlib_persistence = require("@azlib/persistence");
|
|
24
29
|
let exceljs = require("exceljs");
|
|
25
30
|
exceljs = __toESM(exceljs, 1);
|
|
26
31
|
//#region src/content/schema.ts
|
|
@@ -38,7 +43,10 @@ const fields = {
|
|
|
38
43
|
label: options.label ?? "Slug",
|
|
39
44
|
fromField: options.from ?? "title",
|
|
40
45
|
required: options.required ?? false,
|
|
41
|
-
unique: options.unique ?? true
|
|
46
|
+
unique: options.unique ?? true,
|
|
47
|
+
readRoles: options.readRoles,
|
|
48
|
+
writeRoles: options.writeRoles,
|
|
49
|
+
localized: options.localized
|
|
42
50
|
};
|
|
43
51
|
},
|
|
44
52
|
richText(options) {
|
|
@@ -101,6 +109,31 @@ const fields = {
|
|
|
101
109
|
type: "repeater",
|
|
102
110
|
...options
|
|
103
111
|
};
|
|
112
|
+
},
|
|
113
|
+
blocks(options) {
|
|
114
|
+
return {
|
|
115
|
+
type: "blocks",
|
|
116
|
+
...options
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
email(options) {
|
|
120
|
+
return {
|
|
121
|
+
type: "email",
|
|
122
|
+
...options
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
url(options) {
|
|
126
|
+
return {
|
|
127
|
+
type: "url",
|
|
128
|
+
...options
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
array(options) {
|
|
132
|
+
return {
|
|
133
|
+
type: "array",
|
|
134
|
+
defaultValue: [],
|
|
135
|
+
...options
|
|
136
|
+
};
|
|
104
137
|
}
|
|
105
138
|
};
|
|
106
139
|
/**
|
|
@@ -133,19 +166,121 @@ function validateAndNormalizeData(fieldsList, inputData) {
|
|
|
133
166
|
for (const field of fieldsList) {
|
|
134
167
|
let val = normalized[field.name];
|
|
135
168
|
if (val === void 0 && field.defaultValue !== void 0) {
|
|
136
|
-
val = field.defaultValue;
|
|
169
|
+
val = typeof field.defaultValue === "object" && field.defaultValue !== null ? JSON.parse(JSON.stringify(field.defaultValue)) : field.defaultValue;
|
|
137
170
|
normalized[field.name] = val;
|
|
138
171
|
}
|
|
139
|
-
|
|
172
|
+
const isBlank = val === void 0 || val === null || val === "" || Array.isArray(val) && val.length === 0;
|
|
173
|
+
if (field.required && isBlank) {
|
|
140
174
|
errors[field.name] = `${field.label || field.name} is required.`;
|
|
141
175
|
continue;
|
|
142
176
|
}
|
|
143
|
-
if (val !== void 0 && val !== null) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
177
|
+
if (val !== void 0 && val !== null && val !== "") switch (field.type) {
|
|
178
|
+
case "number": {
|
|
179
|
+
let num = val;
|
|
180
|
+
if (typeof num !== "number") {
|
|
181
|
+
const parsed = Number(num);
|
|
182
|
+
if (Number.isNaN(parsed)) {
|
|
183
|
+
errors[field.name] = `${field.label || field.name} must be a valid number.`;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
num = parsed;
|
|
187
|
+
normalized[field.name] = num;
|
|
188
|
+
}
|
|
189
|
+
if (field.min !== void 0 && num < field.min) errors[field.name] = `${field.label || field.name} must be at least ${field.min}.`;
|
|
190
|
+
if (field.max !== void 0 && num > field.max) errors[field.name] = `${field.label || field.name} must be at most ${field.max}.`;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case "boolean":
|
|
194
|
+
if (typeof val !== "boolean") normalized[field.name] = Boolean(val);
|
|
195
|
+
break;
|
|
196
|
+
case "text":
|
|
197
|
+
case "slug":
|
|
198
|
+
case "richText": {
|
|
199
|
+
const strVal = String(val);
|
|
200
|
+
if (field.min !== void 0 && strVal.length < field.min) errors[field.name] = `${field.label || field.name} must have at least ${field.min} characters.`;
|
|
201
|
+
if (field.max !== void 0 && strVal.length > field.max) errors[field.name] = `${field.label || field.name} must have at most ${field.max} characters.`;
|
|
202
|
+
if (field.pattern) try {
|
|
203
|
+
if (!new RegExp(field.pattern).test(strVal)) errors[field.name] = `${field.label || field.name} format is invalid.`;
|
|
204
|
+
} catch {}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case "email": {
|
|
208
|
+
const emailStr = String(val).trim();
|
|
209
|
+
if (!(0, _azlib_validator.isEmail)(emailStr)) errors[field.name] = `${field.label || field.name} must be a valid email address.`;
|
|
210
|
+
else normalized[field.name] = emailStr.toLowerCase();
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case "url": {
|
|
214
|
+
const urlStr = String(val).trim();
|
|
215
|
+
if (!(0, _azlib_validator.isURL)(urlStr)) errors[field.name] = `${field.label || field.name} must be a valid URL.`;
|
|
216
|
+
else normalized[field.name] = urlStr;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
case "select":
|
|
220
|
+
if (field.options && field.options.length > 0) {
|
|
221
|
+
const validValues = field.options.map((opt) => typeof opt === "object" && opt !== null ? opt.value : opt);
|
|
222
|
+
if (!validValues.includes(val)) errors[field.name] = `${field.label || field.name} must be one of: ${validValues.join(", ")}.`;
|
|
223
|
+
}
|
|
224
|
+
break;
|
|
225
|
+
case "repeater":
|
|
226
|
+
if (!Array.isArray(val)) {
|
|
227
|
+
errors[field.name] = `${field.label || field.name} must be an array.`;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
if (field.min !== void 0 && val.length < field.min) errors[field.name] = `${field.label || field.name} must have at least ${field.min} items.`;
|
|
231
|
+
if (field.max !== void 0 && val.length > field.max) errors[field.name] = `${field.label || field.name} must have at most ${field.max} items.`;
|
|
232
|
+
if (field.fields && field.fields.length > 0) {
|
|
233
|
+
const normalizedItems = [];
|
|
234
|
+
for (let i = 0; i < val.length; i++) {
|
|
235
|
+
const item = val[i];
|
|
236
|
+
if (typeof item !== "object" || item === null) {
|
|
237
|
+
errors[`${field.name}[${i}]`] = `Item must be an object.`;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const { data: itemData, errors: itemErrors } = validateAndNormalizeData(field.fields, item);
|
|
241
|
+
for (const [errKey, errMsg] of Object.entries(itemErrors)) errors[`${field.name}[${i}].${errKey}`] = errMsg;
|
|
242
|
+
normalizedItems.push(itemData);
|
|
243
|
+
}
|
|
244
|
+
normalized[field.name] = normalizedItems;
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
case "blocks": {
|
|
248
|
+
if (!Array.isArray(val)) {
|
|
249
|
+
errors[field.name] = `${field.label || field.name} must be an array of blocks.`;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
if (field.min !== void 0 && val.length < field.min) errors[field.name] = `${field.label || field.name} must have at least ${field.min} blocks.`;
|
|
253
|
+
if (field.max !== void 0 && val.length > field.max) errors[field.name] = `${field.label || field.name} must have at most ${field.max} blocks.`;
|
|
254
|
+
const normalizedBlocks = [];
|
|
255
|
+
for (let i = 0; i < val.length; i++) {
|
|
256
|
+
const block = val[i];
|
|
257
|
+
if (typeof block !== "object" || block === null) {
|
|
258
|
+
errors[`${field.name}[${i}]`] = `Block must be an object.`;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const blockType = block.blockType;
|
|
262
|
+
if (typeof blockType !== "string" || !blockType) {
|
|
263
|
+
errors[`${field.name}[${i}].blockType`] = `Block missing 'blockType' identifier.`;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const blockDef = field.blocks?.find((b) => b.slug === blockType);
|
|
267
|
+
if (!blockDef) {
|
|
268
|
+
errors[`${field.name}[${i}].blockType`] = `Unknown block type '${blockType}'.`;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const { data: blockData, errors: blockErrors } = validateAndNormalizeData(blockDef.fields, block);
|
|
272
|
+
for (const [errKey, errMsg] of Object.entries(blockErrors)) errors[`${field.name}[${i}].${errKey}`] = errMsg;
|
|
273
|
+
normalizedBlocks.push({
|
|
274
|
+
...blockData,
|
|
275
|
+
blockType
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
normalized[field.name] = normalizedBlocks;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case "array":
|
|
282
|
+
if (!Array.isArray(val)) errors[field.name] = `${field.label || field.name} must be an array.`;
|
|
283
|
+
break;
|
|
149
284
|
}
|
|
150
285
|
if (field.validate && val !== void 0) {
|
|
151
286
|
const res = field.validate(val, normalized);
|
|
@@ -259,7 +394,11 @@ function normalizeConfig(config) {
|
|
|
259
394
|
route: config.admin?.route ?? "/admin",
|
|
260
395
|
enableRegistration: config.admin?.enableRegistration ?? false,
|
|
261
396
|
...config.admin
|
|
262
|
-
}
|
|
397
|
+
},
|
|
398
|
+
auth: config.auth,
|
|
399
|
+
webhooks: config.webhooks ?? [],
|
|
400
|
+
media: config.media,
|
|
401
|
+
i18n: config.i18n
|
|
263
402
|
};
|
|
264
403
|
}
|
|
265
404
|
//#endregion
|
|
@@ -802,17 +941,21 @@ var MediaManager = class {
|
|
|
802
941
|
storage;
|
|
803
942
|
hooks;
|
|
804
943
|
publicBaseUrl;
|
|
944
|
+
driver;
|
|
805
945
|
allowedMimePrefixes = [
|
|
806
946
|
"image/",
|
|
807
947
|
"video/",
|
|
808
948
|
"audio/",
|
|
809
949
|
"application/pdf",
|
|
810
|
-
"text/"
|
|
950
|
+
"text/",
|
|
951
|
+
"application/msword",
|
|
952
|
+
"application/vnd."
|
|
811
953
|
];
|
|
812
|
-
constructor(storage, hooks, publicBaseUrl = "/uploads") {
|
|
954
|
+
constructor(storage, hooks, publicBaseUrl = "/uploads", driver) {
|
|
813
955
|
this.storage = storage;
|
|
814
956
|
this.hooks = hooks;
|
|
815
957
|
this.publicBaseUrl = publicBaseUrl;
|
|
958
|
+
this.driver = driver;
|
|
816
959
|
}
|
|
817
960
|
/**
|
|
818
961
|
* Upload / register a new media item.
|
|
@@ -822,13 +965,26 @@ var MediaManager = class {
|
|
|
822
965
|
const extMatch = input.filename.match(/\.([a-zA-Z0-9]+)$/);
|
|
823
966
|
const ext = extMatch ? `.${extMatch[1].toLowerCase()}` : "";
|
|
824
967
|
const cleanFilename = `${slugify(input.filename.replace(/\.[^/.]+$/, ""))}${ext}`;
|
|
825
|
-
|
|
968
|
+
let url = input.url ?? `${this.publicBaseUrl}/${cleanFilename}`;
|
|
969
|
+
let path = input.path;
|
|
970
|
+
let sizeBytes = input.sizeBytes ?? (input.buffer ? input.buffer.byteLength : 0);
|
|
971
|
+
if (input.buffer && this.driver) {
|
|
972
|
+
const written = await this.driver.write({
|
|
973
|
+
filename: cleanFilename,
|
|
974
|
+
buffer: input.buffer,
|
|
975
|
+
mimeType: input.mimeType
|
|
976
|
+
});
|
|
977
|
+
url = written.url;
|
|
978
|
+
path = written.path;
|
|
979
|
+
sizeBytes = written.sizeBytes;
|
|
980
|
+
}
|
|
826
981
|
const media = await this.storage.createMedia({
|
|
827
982
|
filename: cleanFilename,
|
|
828
983
|
originalName: input.filename,
|
|
829
984
|
mimeType: input.mimeType,
|
|
830
|
-
sizeBytes
|
|
985
|
+
sizeBytes,
|
|
831
986
|
url,
|
|
987
|
+
path,
|
|
832
988
|
width: input.width,
|
|
833
989
|
height: input.height,
|
|
834
990
|
altText: input.altText,
|
|
@@ -866,7 +1022,10 @@ var MediaManager = class {
|
|
|
866
1022
|
const media = await this.storage.getMedia(id);
|
|
867
1023
|
if (!media) return false;
|
|
868
1024
|
const deleted = await this.storage.deleteMedia(id);
|
|
869
|
-
if (deleted
|
|
1025
|
+
if (deleted) {
|
|
1026
|
+
if (media.path && this.driver) await this.driver.delete(media.path);
|
|
1027
|
+
if (this.hooks) await this.hooks.doAction("cms.media_deleted", media);
|
|
1028
|
+
}
|
|
870
1029
|
return deleted;
|
|
871
1030
|
}
|
|
872
1031
|
};
|
|
@@ -959,10 +1118,481 @@ var RBACManager = class {
|
|
|
959
1118
|
}
|
|
960
1119
|
};
|
|
961
1120
|
//#endregion
|
|
1121
|
+
//#region src/auth/jwt.ts
|
|
1122
|
+
/**
|
|
1123
|
+
* @azlib/cms - Universal Web Crypto JWT & Password Hashing
|
|
1124
|
+
*/
|
|
1125
|
+
function base64UrlEncode(buffer) {
|
|
1126
|
+
let binary = "";
|
|
1127
|
+
for (let i = 0; i < buffer.byteLength; i++) binary += String.fromCharCode(buffer[i]);
|
|
1128
|
+
return btoa(binary).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
1129
|
+
}
|
|
1130
|
+
function base64UrlDecode(str) {
|
|
1131
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
1132
|
+
while (base64.length % 4) base64 += "=";
|
|
1133
|
+
const binary = atob(base64);
|
|
1134
|
+
const bytes = new Uint8Array(binary.length);
|
|
1135
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
1136
|
+
return bytes;
|
|
1137
|
+
}
|
|
1138
|
+
async function signJwt(payload, secret, expiresInSeconds = 86400) {
|
|
1139
|
+
const header = {
|
|
1140
|
+
alg: "HS256",
|
|
1141
|
+
typ: "JWT"
|
|
1142
|
+
};
|
|
1143
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1144
|
+
const fullPayload = {
|
|
1145
|
+
...payload,
|
|
1146
|
+
iat: now,
|
|
1147
|
+
exp: now + expiresInSeconds
|
|
1148
|
+
};
|
|
1149
|
+
const enc = new TextEncoder();
|
|
1150
|
+
const dataToSign = `${base64UrlEncode(enc.encode(JSON.stringify(header)))}.${base64UrlEncode(enc.encode(JSON.stringify(fullPayload)))}`;
|
|
1151
|
+
const key = await crypto.subtle.importKey("raw", enc.encode(secret), {
|
|
1152
|
+
name: "HMAC",
|
|
1153
|
+
hash: "SHA-256"
|
|
1154
|
+
}, false, ["sign"]);
|
|
1155
|
+
const signature = await crypto.subtle.sign("HMAC", key, enc.encode(dataToSign));
|
|
1156
|
+
return `${dataToSign}.${base64UrlEncode(new Uint8Array(signature))}`;
|
|
1157
|
+
}
|
|
1158
|
+
async function verifyJwt(token, secret) {
|
|
1159
|
+
if (!token || typeof token !== "string") return null;
|
|
1160
|
+
const parts = token.split(".");
|
|
1161
|
+
if (parts.length !== 3) return null;
|
|
1162
|
+
const [headerEncoded, payloadEncoded, signatureEncoded] = parts;
|
|
1163
|
+
const dataToVerify = `${headerEncoded}.${payloadEncoded}`;
|
|
1164
|
+
try {
|
|
1165
|
+
const enc = new TextEncoder();
|
|
1166
|
+
const key = await crypto.subtle.importKey("raw", enc.encode(secret), {
|
|
1167
|
+
name: "HMAC",
|
|
1168
|
+
hash: "SHA-256"
|
|
1169
|
+
}, false, ["verify"]);
|
|
1170
|
+
const signature = base64UrlDecode(signatureEncoded);
|
|
1171
|
+
if (!await crypto.subtle.verify("HMAC", key, signature, enc.encode(dataToVerify))) return null;
|
|
1172
|
+
const payloadJson = new TextDecoder().decode(base64UrlDecode(payloadEncoded));
|
|
1173
|
+
const payload = JSON.parse(payloadJson);
|
|
1174
|
+
if (payload.exp && Math.floor(Date.now() / 1e3) > payload.exp) return null;
|
|
1175
|
+
return payload;
|
|
1176
|
+
} catch {
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
async function hashPassword(password) {
|
|
1181
|
+
const enc = new TextEncoder();
|
|
1182
|
+
const saltBytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
1183
|
+
const saltHex = Array.from(saltBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1184
|
+
const keyMaterial = await crypto.subtle.importKey("raw", enc.encode(password), { name: "PBKDF2" }, false, ["deriveBits"]);
|
|
1185
|
+
const derived = await crypto.subtle.deriveBits({
|
|
1186
|
+
name: "PBKDF2",
|
|
1187
|
+
salt: saltBytes,
|
|
1188
|
+
iterations: 1e5,
|
|
1189
|
+
hash: "SHA-256"
|
|
1190
|
+
}, keyMaterial, 256);
|
|
1191
|
+
return `pbkdf2$100000$${saltHex}$${Array.from(new Uint8Array(derived)).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
1192
|
+
}
|
|
1193
|
+
async function verifyPassword(password, storedHash) {
|
|
1194
|
+
const parts = storedHash.split("$");
|
|
1195
|
+
if (parts.length !== 4 || parts[0] !== "pbkdf2") return false;
|
|
1196
|
+
const iterations = Number(parts[1]);
|
|
1197
|
+
const saltHex = parts[2];
|
|
1198
|
+
const expectedHash = parts[3];
|
|
1199
|
+
const saltBytes = new Uint8Array(saltHex.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) || []);
|
|
1200
|
+
const enc = new TextEncoder();
|
|
1201
|
+
const keyMaterial = await crypto.subtle.importKey("raw", enc.encode(password), { name: "PBKDF2" }, false, ["deriveBits"]);
|
|
1202
|
+
const derived = await crypto.subtle.deriveBits({
|
|
1203
|
+
name: "PBKDF2",
|
|
1204
|
+
salt: saltBytes,
|
|
1205
|
+
iterations,
|
|
1206
|
+
hash: "SHA-256"
|
|
1207
|
+
}, keyMaterial, 256);
|
|
1208
|
+
return Array.from(new Uint8Array(derived)).map((b) => b.toString(16).padStart(2, "0")).join("") === expectedHash;
|
|
1209
|
+
}
|
|
1210
|
+
//#endregion
|
|
1211
|
+
//#region src/auth/auth-service.ts
|
|
1212
|
+
var AuthService = class {
|
|
1213
|
+
config;
|
|
1214
|
+
rbac;
|
|
1215
|
+
users = /* @__PURE__ */ new Map();
|
|
1216
|
+
defaultSecret;
|
|
1217
|
+
constructor(config = {}, rbac) {
|
|
1218
|
+
this.config = config;
|
|
1219
|
+
this.rbac = rbac;
|
|
1220
|
+
this.defaultSecret = config.jwtSecret || "azlib_cms_secret_key_change_in_production";
|
|
1221
|
+
}
|
|
1222
|
+
get enabled() {
|
|
1223
|
+
return Boolean(this.config.enabled);
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Register a new user account.
|
|
1227
|
+
*/
|
|
1228
|
+
async register(input) {
|
|
1229
|
+
if (Array.from(this.users.values()).find((u) => u.username.toLowerCase() === input.username.toLowerCase())) throw new Error(`Username '${input.username}' is already taken.`);
|
|
1230
|
+
if (Array.from(this.users.values()).find((u) => u.email.toLowerCase() === input.email.toLowerCase())) throw new Error(`Email '${input.email}' is already registered.`);
|
|
1231
|
+
const id = `usr_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
|
|
1232
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1233
|
+
const passwordHash = await hashPassword(input.password);
|
|
1234
|
+
const role = input.role || "subscriber";
|
|
1235
|
+
const record = {
|
|
1236
|
+
id,
|
|
1237
|
+
username: input.username,
|
|
1238
|
+
email: input.email.toLowerCase(),
|
|
1239
|
+
displayName: input.displayName || input.username,
|
|
1240
|
+
role,
|
|
1241
|
+
passwordHash,
|
|
1242
|
+
active: true,
|
|
1243
|
+
createdAt: now,
|
|
1244
|
+
updatedAt: now
|
|
1245
|
+
};
|
|
1246
|
+
this.users.set(id, record);
|
|
1247
|
+
return this.sanitizeUser(record);
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Authenticate with email/username and password.
|
|
1251
|
+
*/
|
|
1252
|
+
async login(identifier, password) {
|
|
1253
|
+
const normalized = identifier.toLowerCase().trim();
|
|
1254
|
+
const record = Array.from(this.users.values()).find((u) => u.username.toLowerCase() === normalized || u.email.toLowerCase() === normalized);
|
|
1255
|
+
if (!record || record.active === false) throw new Error("Invalid username or password.");
|
|
1256
|
+
if (!await verifyPassword(password, record.passwordHash)) throw new Error("Invalid username or password.");
|
|
1257
|
+
const user = this.sanitizeUser(record);
|
|
1258
|
+
return {
|
|
1259
|
+
user,
|
|
1260
|
+
token: await signJwt({
|
|
1261
|
+
sub: user.id,
|
|
1262
|
+
role: user.role,
|
|
1263
|
+
username: user.username,
|
|
1264
|
+
email: user.email
|
|
1265
|
+
}, this.defaultSecret, typeof this.config.tokenExpiresIn === "number" ? this.config.tokenExpiresIn : 86400 * 7)
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Extract and authenticate user from Request (Bearer JWT, ApiKey, or X-API-Key).
|
|
1270
|
+
*/
|
|
1271
|
+
async authenticateRequest(request) {
|
|
1272
|
+
const authHeader = request.headers.get("authorization") || request.headers.get("Authorization");
|
|
1273
|
+
const apiKeyHeader = request.headers.get("x-api-key") || request.headers.get("X-API-Key");
|
|
1274
|
+
if (apiKeyHeader && this.config.apiKeys) {
|
|
1275
|
+
const match = this.config.apiKeys.find((k) => k.key === apiKeyHeader);
|
|
1276
|
+
if (match) return {
|
|
1277
|
+
id: `api_key_${match.key.substring(0, 8)}`,
|
|
1278
|
+
username: match.name || "API Key User",
|
|
1279
|
+
email: `${match.role}@system.local`,
|
|
1280
|
+
displayName: match.name || "API Key",
|
|
1281
|
+
role: match.role,
|
|
1282
|
+
capabilities: match.capabilities,
|
|
1283
|
+
active: true
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
if (authHeader) {
|
|
1287
|
+
const [type, token] = authHeader.trim().split(/\s+/);
|
|
1288
|
+
if (type?.toLowerCase() === "apikey" && this.config.apiKeys) {
|
|
1289
|
+
const match = this.config.apiKeys.find((k) => k.key === token);
|
|
1290
|
+
if (match) return {
|
|
1291
|
+
id: `api_key_${match.key.substring(0, 8)}`,
|
|
1292
|
+
username: match.name || "API Key User",
|
|
1293
|
+
email: `${match.role}@system.local`,
|
|
1294
|
+
displayName: match.name || "API Key",
|
|
1295
|
+
role: match.role,
|
|
1296
|
+
capabilities: match.capabilities,
|
|
1297
|
+
active: true
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
if (type?.toLowerCase() === "bearer" && token) {
|
|
1301
|
+
const payload = await verifyJwt(token, this.defaultSecret);
|
|
1302
|
+
if (payload) {
|
|
1303
|
+
const registeredUser = this.users.get(payload.sub);
|
|
1304
|
+
if (registeredUser) return this.sanitizeUser(registeredUser);
|
|
1305
|
+
return {
|
|
1306
|
+
id: payload.sub,
|
|
1307
|
+
username: payload.username,
|
|
1308
|
+
email: payload.email,
|
|
1309
|
+
displayName: payload.username,
|
|
1310
|
+
role: payload.role,
|
|
1311
|
+
active: true
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
const cookieHeader = request.headers.get("cookie");
|
|
1317
|
+
if (cookieHeader) {
|
|
1318
|
+
const cookies = Object.fromEntries(cookieHeader.split(";").map((c) => {
|
|
1319
|
+
const [k, v] = c.trim().split("=");
|
|
1320
|
+
return [k, decodeURIComponent(v || "")];
|
|
1321
|
+
}));
|
|
1322
|
+
const token = cookies.access_token || cookies.at;
|
|
1323
|
+
if (token) {
|
|
1324
|
+
const payload = await verifyJwt(token, this.defaultSecret);
|
|
1325
|
+
if (payload) {
|
|
1326
|
+
const registeredUser = this.users.get(payload.sub);
|
|
1327
|
+
if (registeredUser) return this.sanitizeUser(registeredUser);
|
|
1328
|
+
return {
|
|
1329
|
+
id: payload.sub,
|
|
1330
|
+
username: payload.username,
|
|
1331
|
+
email: payload.email,
|
|
1332
|
+
displayName: payload.username,
|
|
1333
|
+
role: payload.role,
|
|
1334
|
+
active: true
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
return null;
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Find a user by ID.
|
|
1343
|
+
*/
|
|
1344
|
+
getUserById(id) {
|
|
1345
|
+
const u = this.users.get(id);
|
|
1346
|
+
return u ? this.sanitizeUser(u) : null;
|
|
1347
|
+
}
|
|
1348
|
+
sanitizeUser(record) {
|
|
1349
|
+
return {
|
|
1350
|
+
id: record.id,
|
|
1351
|
+
username: record.username,
|
|
1352
|
+
email: record.email,
|
|
1353
|
+
displayName: record.displayName,
|
|
1354
|
+
role: record.role,
|
|
1355
|
+
capabilities: record.capabilities,
|
|
1356
|
+
active: record.active
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
//#endregion
|
|
1361
|
+
//#region src/core/webhooks.ts
|
|
1362
|
+
function bufferToHex(buffer) {
|
|
1363
|
+
return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1364
|
+
}
|
|
1365
|
+
async function computeHmacSignature(payload, secret) {
|
|
1366
|
+
const enc = new TextEncoder();
|
|
1367
|
+
const key = await crypto.subtle.importKey("raw", enc.encode(secret), {
|
|
1368
|
+
name: "HMAC",
|
|
1369
|
+
hash: "SHA-256"
|
|
1370
|
+
}, false, ["sign"]);
|
|
1371
|
+
return `sha256=${bufferToHex(await crypto.subtle.sign("HMAC", key, enc.encode(payload)))}`;
|
|
1372
|
+
}
|
|
1373
|
+
var WebhookManager = class {
|
|
1374
|
+
hooks;
|
|
1375
|
+
webhooks = /* @__PURE__ */ new Map();
|
|
1376
|
+
fetchFn;
|
|
1377
|
+
constructor(initialWebhooks = [], hooks, fetchFn) {
|
|
1378
|
+
this.hooks = hooks;
|
|
1379
|
+
this.fetchFn = fetchFn ?? globalThis.fetch?.bind(globalThis);
|
|
1380
|
+
for (const wh of initialWebhooks) this.registerWebhook(wh);
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Register a new webhook endpoint.
|
|
1384
|
+
*/
|
|
1385
|
+
registerWebhook(webhook) {
|
|
1386
|
+
const id = webhook.id || `wh_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 7)}`;
|
|
1387
|
+
this.webhooks.set(id, {
|
|
1388
|
+
...webhook,
|
|
1389
|
+
id,
|
|
1390
|
+
enabled: webhook.enabled ?? true
|
|
1391
|
+
});
|
|
1392
|
+
return id;
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Get all registered webhooks.
|
|
1396
|
+
*/
|
|
1397
|
+
getWebhooks() {
|
|
1398
|
+
return Array.from(this.webhooks.values());
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Delete a webhook by ID.
|
|
1402
|
+
*/
|
|
1403
|
+
deleteWebhook(id) {
|
|
1404
|
+
return this.webhooks.delete(id);
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Dispatch an event to all matching webhooks asynchronously.
|
|
1408
|
+
*/
|
|
1409
|
+
async dispatch(event, data) {
|
|
1410
|
+
const matching = Array.from(this.webhooks.values()).filter((wh) => wh.enabled !== false && (wh.events.includes(event) || wh.events.includes("*")));
|
|
1411
|
+
if (matching.length === 0) return [];
|
|
1412
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1413
|
+
const payloadString = JSON.stringify({
|
|
1414
|
+
event,
|
|
1415
|
+
timestamp: now,
|
|
1416
|
+
data
|
|
1417
|
+
});
|
|
1418
|
+
const deliveryPromises = matching.map(async (webhook) => {
|
|
1419
|
+
try {
|
|
1420
|
+
const headers = {
|
|
1421
|
+
"Content-Type": "application/json",
|
|
1422
|
+
"User-Agent": "azlib-cms-webhooks/1.0",
|
|
1423
|
+
"X-CMS-Event": event,
|
|
1424
|
+
"X-CMS-Delivery-Time": now,
|
|
1425
|
+
...webhook.headers || {}
|
|
1426
|
+
};
|
|
1427
|
+
if (webhook.secret) headers["X-CMS-Signature"] = await computeHmacSignature(payloadString, webhook.secret);
|
|
1428
|
+
const res = await this.fetchFn(webhook.url, {
|
|
1429
|
+
method: "POST",
|
|
1430
|
+
headers,
|
|
1431
|
+
body: payloadString
|
|
1432
|
+
});
|
|
1433
|
+
if (this.hooks) await this.hooks.doAction("cms.webhook_delivered", {
|
|
1434
|
+
webhook,
|
|
1435
|
+
event,
|
|
1436
|
+
status: res.status,
|
|
1437
|
+
ok: res.ok
|
|
1438
|
+
});
|
|
1439
|
+
return {
|
|
1440
|
+
url: webhook.url,
|
|
1441
|
+
success: res.ok,
|
|
1442
|
+
status: res.status
|
|
1443
|
+
};
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
if (this.hooks) await this.hooks.doAction("cms.webhook_failed", {
|
|
1446
|
+
webhook,
|
|
1447
|
+
event,
|
|
1448
|
+
error: error.message
|
|
1449
|
+
});
|
|
1450
|
+
return {
|
|
1451
|
+
url: webhook.url,
|
|
1452
|
+
success: false
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
return Promise.all(deliveryPromises);
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
//#endregion
|
|
1460
|
+
//#region src/core/preview.ts
|
|
1461
|
+
/**
|
|
1462
|
+
* @azlib/cms - Draft Preview Mode Engine
|
|
1463
|
+
*/
|
|
1464
|
+
var PreviewManager = class {
|
|
1465
|
+
secret;
|
|
1466
|
+
constructor(secret = "azlib_preview_secret_token_default") {
|
|
1467
|
+
this.secret = secret;
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Mint a signed draft preview token.
|
|
1471
|
+
*/
|
|
1472
|
+
async createPreviewToken(contentId, collection, expiresInSeconds = 3600) {
|
|
1473
|
+
return signJwt({
|
|
1474
|
+
contentId,
|
|
1475
|
+
collection,
|
|
1476
|
+
scope: "preview"
|
|
1477
|
+
}, this.secret, expiresInSeconds);
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Verify and unpack a draft preview token.
|
|
1481
|
+
*/
|
|
1482
|
+
async verifyPreviewToken(token) {
|
|
1483
|
+
const payload = await verifyJwt(token, this.secret);
|
|
1484
|
+
if (!payload || payload.scope !== "preview") return null;
|
|
1485
|
+
return {
|
|
1486
|
+
contentId: payload.contentId,
|
|
1487
|
+
collection: payload.collection
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
//#endregion
|
|
1492
|
+
//#region src/content/i18n.ts
|
|
1493
|
+
/**
|
|
1494
|
+
* Resolve localized fields on a content record for a target locale.
|
|
1495
|
+
*/
|
|
1496
|
+
function resolveLocalizedData(data, fieldsList, targetLocale, fallbackLocale = "en") {
|
|
1497
|
+
const resolved = { ...data };
|
|
1498
|
+
for (const field of fieldsList) {
|
|
1499
|
+
if (!field.localized) continue;
|
|
1500
|
+
const val = data[field.name];
|
|
1501
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
1502
|
+
const locMap = val;
|
|
1503
|
+
if (targetLocale in locMap && locMap[targetLocale] !== void 0) resolved[field.name] = locMap[targetLocale];
|
|
1504
|
+
else if (fallbackLocale && fallbackLocale in locMap && locMap[fallbackLocale] !== void 0) resolved[field.name] = locMap[fallbackLocale];
|
|
1505
|
+
else {
|
|
1506
|
+
const firstAvailable = Object.values(locMap)[0];
|
|
1507
|
+
resolved[field.name] = firstAvailable;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
return resolved;
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Merge an incoming update for a specific locale into existing localized dictionaries.
|
|
1515
|
+
*/
|
|
1516
|
+
function mergeLocalizedInput(incomingData, existingData = {}, fieldsList, locale) {
|
|
1517
|
+
const merged = {
|
|
1518
|
+
...existingData,
|
|
1519
|
+
...incomingData
|
|
1520
|
+
};
|
|
1521
|
+
for (const field of fieldsList) {
|
|
1522
|
+
if (!field.localized) continue;
|
|
1523
|
+
const incomingVal = incomingData[field.name];
|
|
1524
|
+
if (incomingVal === void 0) continue;
|
|
1525
|
+
if (incomingVal && typeof incomingVal === "object" && !Array.isArray(incomingVal) && Object.keys(incomingVal).some((k) => k.length === 2 || k.includes("-"))) {
|
|
1526
|
+
const existingObj = typeof existingData[field.name] === "object" && existingData[field.name] !== null ? existingData[field.name] : {};
|
|
1527
|
+
merged[field.name] = {
|
|
1528
|
+
...existingObj,
|
|
1529
|
+
...incomingVal
|
|
1530
|
+
};
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
const existingMap = typeof existingData[field.name] === "object" && existingData[field.name] !== null ? { ...existingData[field.name] } : {};
|
|
1534
|
+
existingMap[locale] = incomingVal;
|
|
1535
|
+
merged[field.name] = existingMap;
|
|
1536
|
+
}
|
|
1537
|
+
return merged;
|
|
1538
|
+
}
|
|
1539
|
+
//#endregion
|
|
962
1540
|
//#region src/storage/memory-adapter.ts
|
|
963
|
-
function generateId(prefix = "") {
|
|
1541
|
+
function generateId$1(prefix = "") {
|
|
964
1542
|
return `${prefix}${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 9)}`;
|
|
965
1543
|
}
|
|
1544
|
+
function matchesCondition(actualValue, expected) {
|
|
1545
|
+
if (expected === null || expected === void 0 || typeof expected !== "object" || Array.isArray(expected)) return actualValue === expected;
|
|
1546
|
+
const exp = expected;
|
|
1547
|
+
if (!Object.keys(exp).some((k) => k.startsWith("$"))) return JSON.stringify(actualValue) === JSON.stringify(expected);
|
|
1548
|
+
for (const [op, target] of Object.entries(exp)) switch (op) {
|
|
1549
|
+
case "$eq":
|
|
1550
|
+
if (actualValue !== target) return false;
|
|
1551
|
+
break;
|
|
1552
|
+
case "$ne":
|
|
1553
|
+
if (actualValue === target) return false;
|
|
1554
|
+
break;
|
|
1555
|
+
case "$gt":
|
|
1556
|
+
if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
|
|
1557
|
+
if (actualValue <= target) return false;
|
|
1558
|
+
break;
|
|
1559
|
+
case "$gte":
|
|
1560
|
+
if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
|
|
1561
|
+
if (actualValue < target) return false;
|
|
1562
|
+
break;
|
|
1563
|
+
case "$lt":
|
|
1564
|
+
if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
|
|
1565
|
+
if (actualValue >= target) return false;
|
|
1566
|
+
break;
|
|
1567
|
+
case "$lte":
|
|
1568
|
+
if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
|
|
1569
|
+
if (actualValue > target) return false;
|
|
1570
|
+
break;
|
|
1571
|
+
case "$in":
|
|
1572
|
+
if (!Array.isArray(target) || !target.includes(actualValue)) return false;
|
|
1573
|
+
break;
|
|
1574
|
+
case "$nin":
|
|
1575
|
+
if (Array.isArray(target) && target.includes(actualValue)) return false;
|
|
1576
|
+
break;
|
|
1577
|
+
case "$contains":
|
|
1578
|
+
if (typeof actualValue !== "string" || !actualValue.toLowerCase().includes(String(target).toLowerCase())) return false;
|
|
1579
|
+
break;
|
|
1580
|
+
case "$startsWith":
|
|
1581
|
+
if (typeof actualValue !== "string" || !actualValue.startsWith(String(target))) return false;
|
|
1582
|
+
break;
|
|
1583
|
+
case "$endsWith":
|
|
1584
|
+
if (typeof actualValue !== "string" || !actualValue.endsWith(String(target))) return false;
|
|
1585
|
+
break;
|
|
1586
|
+
case "$between":
|
|
1587
|
+
if (!Array.isArray(target) || target.length !== 2) return false;
|
|
1588
|
+
if (actualValue < target[0] || actualValue > target[1]) return false;
|
|
1589
|
+
break;
|
|
1590
|
+
case "$exists":
|
|
1591
|
+
if (actualValue !== void 0 !== Boolean(target)) return false;
|
|
1592
|
+
break;
|
|
1593
|
+
}
|
|
1594
|
+
return true;
|
|
1595
|
+
}
|
|
966
1596
|
var MemoryStorageAdapter = class {
|
|
967
1597
|
content = /* @__PURE__ */ new Map();
|
|
968
1598
|
revisions = /* @__PURE__ */ new Map();
|
|
@@ -973,7 +1603,7 @@ var MemoryStorageAdapter = class {
|
|
|
973
1603
|
async init() {}
|
|
974
1604
|
async close() {}
|
|
975
1605
|
async createContent(item) {
|
|
976
|
-
const id = generateId("cnt_");
|
|
1606
|
+
const id = generateId$1("cnt_");
|
|
977
1607
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
978
1608
|
const fullItem = {
|
|
979
1609
|
...item,
|
|
@@ -1016,7 +1646,7 @@ var MemoryStorageAdapter = class {
|
|
|
1016
1646
|
}
|
|
1017
1647
|
if (options.where) {
|
|
1018
1648
|
let match = true;
|
|
1019
|
-
for (const [key,
|
|
1649
|
+
for (const [key, expected] of Object.entries(options.where)) if (!matchesCondition(item.data[key] !== void 0 ? item.data[key] : item[key], expected)) {
|
|
1020
1650
|
match = false;
|
|
1021
1651
|
break;
|
|
1022
1652
|
}
|
|
@@ -1037,14 +1667,49 @@ var MemoryStorageAdapter = class {
|
|
|
1037
1667
|
const offset = options.offset ?? 0;
|
|
1038
1668
|
const limit = options.limit ?? 20;
|
|
1039
1669
|
const paged = list.slice(offset, offset + limit);
|
|
1670
|
+
let items = JSON.parse(JSON.stringify(paged));
|
|
1671
|
+
if (options.populate && options.populate.length > 0) items = items.map((it) => this.populateItem(it, options.populate));
|
|
1672
|
+
if (options.select && options.select.length > 0) items = items.map((it) => this.projectItem(it, options.select));
|
|
1040
1673
|
return {
|
|
1041
|
-
items
|
|
1674
|
+
items,
|
|
1042
1675
|
total,
|
|
1043
1676
|
limit,
|
|
1044
1677
|
offset,
|
|
1045
1678
|
hasMore: offset + limit < total
|
|
1046
1679
|
};
|
|
1047
1680
|
}
|
|
1681
|
+
populateItem(item, populateFields) {
|
|
1682
|
+
const populated = { ...item.populated || {} };
|
|
1683
|
+
for (const field of populateFields) {
|
|
1684
|
+
const rawVal = item.data[field];
|
|
1685
|
+
if (!rawVal) continue;
|
|
1686
|
+
if (Array.isArray(rawVal)) populated[field] = rawVal.map((id) => {
|
|
1687
|
+
if (typeof id === "string") return this.content.get(id) || this.media.get(id) || id;
|
|
1688
|
+
return id;
|
|
1689
|
+
});
|
|
1690
|
+
else if (typeof rawVal === "string") {
|
|
1691
|
+
const target = this.content.get(rawVal) || this.media.get(rawVal) || null;
|
|
1692
|
+
if (target) populated[field] = target;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
return {
|
|
1696
|
+
...item,
|
|
1697
|
+
populated
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
projectItem(item, selectFields) {
|
|
1701
|
+
const projected = {
|
|
1702
|
+
id: item.id,
|
|
1703
|
+
collection: item.collection
|
|
1704
|
+
};
|
|
1705
|
+
for (const key of selectFields) if (key in item) projected[key] = item[key];
|
|
1706
|
+
else if (key in item.data) {
|
|
1707
|
+
if (!projected.data) projected.data = {};
|
|
1708
|
+
projected.data[key] = item.data[key];
|
|
1709
|
+
}
|
|
1710
|
+
if (item.populated) projected.populated = item.populated;
|
|
1711
|
+
return projected;
|
|
1712
|
+
}
|
|
1048
1713
|
async updateContent(collection, id, updates) {
|
|
1049
1714
|
const existing = this.content.get(id);
|
|
1050
1715
|
if (!existing || existing.collection !== collection) return null;
|
|
@@ -1076,7 +1741,7 @@ var MemoryStorageAdapter = class {
|
|
|
1076
1741
|
})).total;
|
|
1077
1742
|
}
|
|
1078
1743
|
async createRevision(contentId, collection, version, snapshot, authorId, note) {
|
|
1079
|
-
const id = generateId("rev_");
|
|
1744
|
+
const id = generateId$1("rev_");
|
|
1080
1745
|
const record = {
|
|
1081
1746
|
id,
|
|
1082
1747
|
contentId,
|
|
@@ -1109,7 +1774,7 @@ var MemoryStorageAdapter = class {
|
|
|
1109
1774
|
return count;
|
|
1110
1775
|
}
|
|
1111
1776
|
async createTerm(term) {
|
|
1112
|
-
const id = generateId("trm_");
|
|
1777
|
+
const id = generateId$1("trm_");
|
|
1113
1778
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1114
1779
|
const item = {
|
|
1115
1780
|
...term,
|
|
@@ -1187,7 +1852,7 @@ var MemoryStorageAdapter = class {
|
|
|
1187
1852
|
for (const [id, term] of this.terms.entries()) term.count = counts.get(id) || 0;
|
|
1188
1853
|
}
|
|
1189
1854
|
async createMedia(item) {
|
|
1190
|
-
const id = generateId("med_");
|
|
1855
|
+
const id = generateId$1("med_");
|
|
1191
1856
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1192
1857
|
const mediaItem = {
|
|
1193
1858
|
...item,
|
|
@@ -1268,6 +1933,9 @@ var CMSEngine = class {
|
|
|
1268
1933
|
taxonomies;
|
|
1269
1934
|
media;
|
|
1270
1935
|
rbac;
|
|
1936
|
+
auth;
|
|
1937
|
+
webhooks;
|
|
1938
|
+
preview;
|
|
1271
1939
|
revisions;
|
|
1272
1940
|
lifecycle;
|
|
1273
1941
|
collections = /* @__PURE__ */ new Map();
|
|
@@ -1280,8 +1948,11 @@ var CMSEngine = class {
|
|
|
1280
1948
|
this.storage = storage ?? new MemoryStorageAdapter();
|
|
1281
1949
|
this.options = new OptionsManager(this.storage, this.hooks);
|
|
1282
1950
|
this.taxonomies = new TaxonomyManager(this.storage, this.hooks);
|
|
1283
|
-
this.media = new MediaManager(this.storage, this.hooks);
|
|
1284
1951
|
this.rbac = new RBACManager();
|
|
1952
|
+
this.auth = new AuthService(this.config.auth, this.rbac);
|
|
1953
|
+
this.webhooks = new WebhookManager(this.config.webhooks || [], this.hooks);
|
|
1954
|
+
this.preview = new PreviewManager(this.config.auth?.jwtSecret || "azlib_preview_secret_token_default");
|
|
1955
|
+
this.media = new MediaManager(this.storage, this.hooks, this.config.media?.publicBaseUrl || "/uploads", this.config.media?.storageDriver);
|
|
1285
1956
|
this.revisions = new RevisionManager(this.storage, this.hooks);
|
|
1286
1957
|
this.lifecycle = new ContentLifecycle(this.storage, this.hooks);
|
|
1287
1958
|
for (const coll of this.config.collections) this.collections.set(coll.slug, coll);
|
|
@@ -1422,11 +2093,12 @@ var CMSEngine = class {
|
|
|
1422
2093
|
const filteredInput = await self.hooks.applyFilters("cms.before_create_input", input, { collection: slug });
|
|
1423
2094
|
const inputData = { ...filteredInput.data || {} };
|
|
1424
2095
|
if (filteredInput.title !== void 0 && inputData.title === void 0) inputData.title = filteredInput.title;
|
|
2096
|
+
if (filteredInput.name !== void 0 && inputData.name === void 0) inputData.name = filteredInput.name;
|
|
1425
2097
|
if (filteredInput.slug !== void 0 && inputData.slug === void 0) inputData.slug = filteredInput.slug;
|
|
1426
2098
|
if (filteredInput.status !== void 0 && inputData.status === void 0) inputData.status = filteredInput.status;
|
|
1427
2099
|
const { data: normalizedData, errors } = validateAndNormalizeData(collConfig.fields, inputData);
|
|
1428
2100
|
if (Object.keys(errors).length > 0) throw new Error(`[CMSEngine] Validation failed for collection '${slug}': ${JSON.stringify(errors)}`);
|
|
1429
|
-
const title = filteredInput.title ?? normalizedData.title ?? "";
|
|
2101
|
+
const title = filteredInput.title ?? filteredInput.name ?? normalizedData.title ?? normalizedData.name ?? "";
|
|
1430
2102
|
const finalSlug = await resolveUniqueSlug(filteredInput.slug ? slugify(filteredInput.slug) : slugify(title) || "item", async (s) => {
|
|
1431
2103
|
return await self.storage.getContentBySlug(slug, s) !== null;
|
|
1432
2104
|
});
|
|
@@ -1451,17 +2123,48 @@ var CMSEngine = class {
|
|
|
1451
2123
|
if (collConfig.revisions) await self.revisions.createRevision(item, authorId, "Initial creation");
|
|
1452
2124
|
await self.hooks.doAction("cms.content_created", item);
|
|
1453
2125
|
await self.hooks.doAction(`cms.${slug}_created`, item);
|
|
2126
|
+
self.webhooks.dispatch("content.created", item);
|
|
2127
|
+
if (item.status === "published") self.webhooks.dispatch("content.published", item);
|
|
1454
2128
|
return self.hooks.applyFilters("cms.after_create_item", item, { collection: slug });
|
|
1455
2129
|
},
|
|
1456
|
-
async findById(id) {
|
|
1457
|
-
|
|
2130
|
+
async findById(id, options) {
|
|
2131
|
+
if (options?.populate || options?.select) {
|
|
2132
|
+
const it = (await self.storage.findContent(slug, {
|
|
2133
|
+
where: { id },
|
|
2134
|
+
limit: 1,
|
|
2135
|
+
populate: options.populate,
|
|
2136
|
+
select: options.select
|
|
2137
|
+
})).items[0] || null;
|
|
2138
|
+
if (it && options.locale) it.data = resolveLocalizedData(it.data, collConfig.fields, options.locale, self.config.i18n?.defaultLocale);
|
|
2139
|
+
return it;
|
|
2140
|
+
}
|
|
2141
|
+
const item = await self.storage.getContent(slug, id);
|
|
2142
|
+
if (item && options?.locale) item.data = resolveLocalizedData(item.data, collConfig.fields, options.locale, self.config.i18n?.defaultLocale);
|
|
2143
|
+
return item;
|
|
1458
2144
|
},
|
|
1459
|
-
async findBySlug(contentSlug) {
|
|
1460
|
-
|
|
2145
|
+
async findBySlug(contentSlug, options) {
|
|
2146
|
+
if (options?.populate || options?.select) {
|
|
2147
|
+
const it = (await self.storage.findContent(slug, {
|
|
2148
|
+
where: { slug: contentSlug },
|
|
2149
|
+
limit: 1,
|
|
2150
|
+
populate: options.populate,
|
|
2151
|
+
select: options.select
|
|
2152
|
+
})).items[0] || null;
|
|
2153
|
+
if (it && options.locale) it.data = resolveLocalizedData(it.data, collConfig.fields, options.locale, self.config.i18n?.defaultLocale);
|
|
2154
|
+
return it;
|
|
2155
|
+
}
|
|
2156
|
+
const item = await self.storage.getContentBySlug(slug, contentSlug);
|
|
2157
|
+
if (item && options?.locale) item.data = resolveLocalizedData(item.data, collConfig.fields, options.locale, self.config.i18n?.defaultLocale);
|
|
2158
|
+
return item;
|
|
1461
2159
|
},
|
|
1462
2160
|
async find(options = {}) {
|
|
1463
2161
|
const filteredOptions = await self.hooks.applyFilters("cms.find_options", options, { collection: slug });
|
|
1464
|
-
|
|
2162
|
+
const res = await self.storage.findContent(slug, filteredOptions);
|
|
2163
|
+
if (options.locale) res.items = res.items.map((it) => ({
|
|
2164
|
+
...it,
|
|
2165
|
+
data: resolveLocalizedData(it.data, collConfig.fields, options.locale, self.config.i18n?.defaultLocale)
|
|
2166
|
+
}));
|
|
2167
|
+
return res;
|
|
1465
2168
|
},
|
|
1466
2169
|
async update(id, input, authorId = null, revisionNote) {
|
|
1467
2170
|
const existing = await self.storage.getContent(slug, id);
|
|
@@ -1511,6 +2214,8 @@ var CMSEngine = class {
|
|
|
1511
2214
|
if (collConfig.revisions) await self.revisions.createRevision(updated, authorId, revisionNote ?? `Updated version ${updated.version}`);
|
|
1512
2215
|
await self.hooks.doAction("cms.content_updated", updated);
|
|
1513
2216
|
await self.hooks.doAction(`cms.${slug}_updated`, updated);
|
|
2217
|
+
self.webhooks.dispatch("content.updated", updated);
|
|
2218
|
+
if (updated.status === "published" && existing.status !== "published") self.webhooks.dispatch("content.published", updated);
|
|
1514
2219
|
return self.hooks.applyFilters("cms.after_update_item", updated, { collection: slug });
|
|
1515
2220
|
}
|
|
1516
2221
|
return updated;
|
|
@@ -1522,6 +2227,7 @@ var CMSEngine = class {
|
|
|
1522
2227
|
if (deleted) {
|
|
1523
2228
|
await self.hooks.doAction("cms.content_deleted", item);
|
|
1524
2229
|
await self.hooks.doAction(`cms.${slug}_deleted`, item);
|
|
2230
|
+
self.webhooks.dispatch("content.deleted", item);
|
|
1525
2231
|
}
|
|
1526
2232
|
return deleted;
|
|
1527
2233
|
},
|
|
@@ -1576,6 +2282,700 @@ function definePlugin(factory) {
|
|
|
1576
2282
|
return factory;
|
|
1577
2283
|
}
|
|
1578
2284
|
//#endregion
|
|
2285
|
+
//#region src/media/drivers/disk-driver.ts
|
|
2286
|
+
/**
|
|
2287
|
+
* @azlib/cms - Local Disk Media Storage Driver
|
|
2288
|
+
*/
|
|
2289
|
+
var DiskMediaStorageDriver = class {
|
|
2290
|
+
uploadDir;
|
|
2291
|
+
publicBaseUrl;
|
|
2292
|
+
constructor(options) {
|
|
2293
|
+
this.uploadDir = options.uploadDir;
|
|
2294
|
+
this.publicBaseUrl = options.publicBaseUrl?.replace(/\/+$/, "") || "/uploads";
|
|
2295
|
+
}
|
|
2296
|
+
async write(input) {
|
|
2297
|
+
await node_fs.promises.mkdir(this.uploadDir, { recursive: true });
|
|
2298
|
+
const safeName = `${Date.now()}_${input.filename.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
2299
|
+
const filePath = node_path.join(this.uploadDir, safeName);
|
|
2300
|
+
await node_fs.promises.writeFile(filePath, input.buffer);
|
|
2301
|
+
return {
|
|
2302
|
+
url: `${this.publicBaseUrl}/${safeName}`,
|
|
2303
|
+
path: filePath,
|
|
2304
|
+
sizeBytes: input.buffer.byteLength
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
async read(pathOrUrl) {
|
|
2308
|
+
try {
|
|
2309
|
+
const filename = node_path.basename(pathOrUrl);
|
|
2310
|
+
const filePath = node_path.join(this.uploadDir, filename);
|
|
2311
|
+
const data = await node_fs.promises.readFile(filePath);
|
|
2312
|
+
return new Uint8Array(data);
|
|
2313
|
+
} catch {
|
|
2314
|
+
return null;
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
async delete(pathOrUrl) {
|
|
2318
|
+
try {
|
|
2319
|
+
const filename = node_path.basename(pathOrUrl);
|
|
2320
|
+
const filePath = node_path.join(this.uploadDir, filename);
|
|
2321
|
+
await node_fs.promises.unlink(filePath);
|
|
2322
|
+
return true;
|
|
2323
|
+
} catch {
|
|
2324
|
+
return false;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
getUrl(filePathOrName) {
|
|
2328
|
+
const filename = node_path.basename(filePathOrName);
|
|
2329
|
+
return `${this.publicBaseUrl}/${filename}`;
|
|
2330
|
+
}
|
|
2331
|
+
};
|
|
2332
|
+
//#endregion
|
|
2333
|
+
//#region src/storage/persistence-adapter.ts
|
|
2334
|
+
/**
|
|
2335
|
+
* @azlib/cms - Persistence Storage Adapter (@azlib/persistence)
|
|
2336
|
+
*/
|
|
2337
|
+
function generateId(prefix = "") {
|
|
2338
|
+
return `${prefix}${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 9)}`;
|
|
2339
|
+
}
|
|
2340
|
+
const cmsContentTable = (0, _azlib_persistence.createTable)("cms_content", {
|
|
2341
|
+
id: _azlib_persistence.column.varchar("id", { length: 64 }).notNull().primaryKey(),
|
|
2342
|
+
collection: _azlib_persistence.column.varchar("collection", { length: 64 }).notNull(),
|
|
2343
|
+
slug: _azlib_persistence.column.varchar("slug", { length: 255 }).notNull(),
|
|
2344
|
+
status: _azlib_persistence.column.varchar("status", { length: 32 }).notNull(),
|
|
2345
|
+
title: _azlib_persistence.column.text("title"),
|
|
2346
|
+
parentId: _azlib_persistence.column.varchar("parent_id", { length: 64 }),
|
|
2347
|
+
authorId: _azlib_persistence.column.varchar("author_id", { length: 64 }),
|
|
2348
|
+
publishedAt: _azlib_persistence.column.varchar("published_at", { length: 32 }),
|
|
2349
|
+
scheduledAt: _azlib_persistence.column.varchar("scheduled_at", { length: 32 }),
|
|
2350
|
+
createdAt: _azlib_persistence.column.varchar("created_at", { length: 32 }).notNull(),
|
|
2351
|
+
updatedAt: _azlib_persistence.column.varchar("updated_at", { length: 32 }).notNull(),
|
|
2352
|
+
version: _azlib_persistence.column.integer("version").notNull().default(1),
|
|
2353
|
+
locale: _azlib_persistence.column.varchar("locale", { length: 16 }),
|
|
2354
|
+
data: _azlib_persistence.column.text("data").notNull(),
|
|
2355
|
+
terms: _azlib_persistence.column.text("terms")
|
|
2356
|
+
});
|
|
2357
|
+
const cmsRevisionsTable = (0, _azlib_persistence.createTable)("cms_revisions", {
|
|
2358
|
+
id: _azlib_persistence.column.varchar("id", { length: 64 }).notNull().primaryKey(),
|
|
2359
|
+
contentId: _azlib_persistence.column.varchar("content_id", { length: 64 }).notNull(),
|
|
2360
|
+
collection: _azlib_persistence.column.varchar("collection", { length: 64 }).notNull(),
|
|
2361
|
+
version: _azlib_persistence.column.integer("version").notNull(),
|
|
2362
|
+
snapshot: _azlib_persistence.column.text("snapshot").notNull(),
|
|
2363
|
+
authorId: _azlib_persistence.column.varchar("author_id", { length: 64 }),
|
|
2364
|
+
note: _azlib_persistence.column.text("note"),
|
|
2365
|
+
createdAt: _azlib_persistence.column.varchar("created_at", { length: 32 }).notNull()
|
|
2366
|
+
});
|
|
2367
|
+
const cmsTermsTable = (0, _azlib_persistence.createTable)("cms_terms", {
|
|
2368
|
+
id: _azlib_persistence.column.varchar("id", { length: 64 }).notNull().primaryKey(),
|
|
2369
|
+
taxonomy: _azlib_persistence.column.varchar("taxonomy", { length: 64 }).notNull(),
|
|
2370
|
+
name: _azlib_persistence.column.varchar("name", { length: 255 }).notNull(),
|
|
2371
|
+
slug: _azlib_persistence.column.varchar("slug", { length: 255 }).notNull(),
|
|
2372
|
+
description: _azlib_persistence.column.text("description"),
|
|
2373
|
+
parentId: _azlib_persistence.column.varchar("parent_id", { length: 64 }),
|
|
2374
|
+
count: _azlib_persistence.column.integer("count").notNull().default(0),
|
|
2375
|
+
meta: _azlib_persistence.column.text("meta"),
|
|
2376
|
+
createdAt: _azlib_persistence.column.varchar("created_at", { length: 32 }).notNull(),
|
|
2377
|
+
updatedAt: _azlib_persistence.column.varchar("updated_at", { length: 32 }).notNull()
|
|
2378
|
+
});
|
|
2379
|
+
const cmsContentTermsTable = (0, _azlib_persistence.createTable)("cms_content_terms", {
|
|
2380
|
+
contentId: _azlib_persistence.column.varchar("content_id", { length: 64 }).notNull(),
|
|
2381
|
+
termId: _azlib_persistence.column.varchar("term_id", { length: 64 }).notNull()
|
|
2382
|
+
});
|
|
2383
|
+
const cmsMediaTable = (0, _azlib_persistence.createTable)("cms_media", {
|
|
2384
|
+
id: _azlib_persistence.column.varchar("id", { length: 64 }).notNull().primaryKey(),
|
|
2385
|
+
filename: _azlib_persistence.column.varchar("filename", { length: 255 }).notNull(),
|
|
2386
|
+
originalName: _azlib_persistence.column.varchar("original_name", { length: 255 }).notNull(),
|
|
2387
|
+
mimeType: _azlib_persistence.column.varchar("mime_type", { length: 128 }).notNull(),
|
|
2388
|
+
sizeBytes: _azlib_persistence.column.integer("size_bytes").notNull(),
|
|
2389
|
+
url: _azlib_persistence.column.text("url").notNull(),
|
|
2390
|
+
path: _azlib_persistence.column.text("path"),
|
|
2391
|
+
width: _azlib_persistence.column.integer("width"),
|
|
2392
|
+
height: _azlib_persistence.column.integer("height"),
|
|
2393
|
+
altText: _azlib_persistence.column.text("alt_text"),
|
|
2394
|
+
caption: _azlib_persistence.column.text("caption"),
|
|
2395
|
+
authorId: _azlib_persistence.column.varchar("author_id", { length: 64 }),
|
|
2396
|
+
variants: _azlib_persistence.column.text("variants"),
|
|
2397
|
+
createdAt: _azlib_persistence.column.varchar("created_at", { length: 32 }).notNull(),
|
|
2398
|
+
updatedAt: _azlib_persistence.column.varchar("updated_at", { length: 32 }).notNull()
|
|
2399
|
+
});
|
|
2400
|
+
const cmsOptionsTable = (0, _azlib_persistence.createTable)("cms_options", {
|
|
2401
|
+
key: _azlib_persistence.column.varchar("key", { length: 128 }).notNull().primaryKey(),
|
|
2402
|
+
value: _azlib_persistence.column.text("value").notNull(),
|
|
2403
|
+
autoload: _azlib_persistence.column.boolean("autoload").notNull().default(false),
|
|
2404
|
+
namespace: _azlib_persistence.column.varchar("namespace", { length: 64 }),
|
|
2405
|
+
updatedAt: _azlib_persistence.column.varchar("updated_at", { length: 32 }).notNull()
|
|
2406
|
+
});
|
|
2407
|
+
var PersistenceStorageAdapter = class {
|
|
2408
|
+
client;
|
|
2409
|
+
config;
|
|
2410
|
+
autoMigrate;
|
|
2411
|
+
constructor(options) {
|
|
2412
|
+
this.client = options.client;
|
|
2413
|
+
this.config = options.config;
|
|
2414
|
+
this.autoMigrate = options.autoMigrate ?? true;
|
|
2415
|
+
}
|
|
2416
|
+
async init() {
|
|
2417
|
+
if (!this.autoMigrate) return;
|
|
2418
|
+
const tables = [
|
|
2419
|
+
cmsContentTable,
|
|
2420
|
+
cmsRevisionsTable,
|
|
2421
|
+
cmsTermsTable,
|
|
2422
|
+
cmsContentTermsTable,
|
|
2423
|
+
cmsMediaTable,
|
|
2424
|
+
cmsOptionsTable
|
|
2425
|
+
];
|
|
2426
|
+
for (const table of tables) {
|
|
2427
|
+
const ddl = (0, _azlib_persistence.generateCreateTableDdl)(table, this.config.dialect);
|
|
2428
|
+
try {
|
|
2429
|
+
await this.client.raw(ddl);
|
|
2430
|
+
} catch (err) {}
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
async close() {}
|
|
2434
|
+
async createContent(item) {
|
|
2435
|
+
const id = generateId("cnt_");
|
|
2436
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2437
|
+
const version = 1;
|
|
2438
|
+
const record = {
|
|
2439
|
+
id,
|
|
2440
|
+
collection: item.collection,
|
|
2441
|
+
slug: item.slug,
|
|
2442
|
+
status: item.status,
|
|
2443
|
+
title: item.title ?? null,
|
|
2444
|
+
parentId: item.parentId ?? null,
|
|
2445
|
+
authorId: item.authorId ?? null,
|
|
2446
|
+
publishedAt: item.publishedAt ?? null,
|
|
2447
|
+
scheduledAt: item.scheduledAt ?? null,
|
|
2448
|
+
createdAt: now,
|
|
2449
|
+
updatedAt: now,
|
|
2450
|
+
version,
|
|
2451
|
+
locale: item.locale ?? null,
|
|
2452
|
+
data: JSON.stringify(item.data || {}),
|
|
2453
|
+
terms: item.terms ? JSON.stringify(item.terms) : null
|
|
2454
|
+
};
|
|
2455
|
+
await this.client.insert(cmsContentTable).values(record).execute();
|
|
2456
|
+
return {
|
|
2457
|
+
id,
|
|
2458
|
+
collection: item.collection,
|
|
2459
|
+
slug: item.slug,
|
|
2460
|
+
status: item.status,
|
|
2461
|
+
title: item.title,
|
|
2462
|
+
parentId: item.parentId ?? null,
|
|
2463
|
+
authorId: item.authorId ?? null,
|
|
2464
|
+
publishedAt: item.publishedAt ?? null,
|
|
2465
|
+
scheduledAt: item.scheduledAt ?? null,
|
|
2466
|
+
createdAt: now,
|
|
2467
|
+
updatedAt: now,
|
|
2468
|
+
version,
|
|
2469
|
+
locale: item.locale,
|
|
2470
|
+
data: item.data,
|
|
2471
|
+
terms: item.terms
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
async getContent(collection, id) {
|
|
2475
|
+
const rows = await this.client.select().from(cmsContentTable).where((0, _azlib_persistence.and)((0, _azlib_persistence.eq)(cmsContentTable.id, id), (0, _azlib_persistence.eq)(cmsContentTable.collection, collection))).execute();
|
|
2476
|
+
if (!rows || rows.length === 0) return null;
|
|
2477
|
+
return this.deserializeContent(rows[0]);
|
|
2478
|
+
}
|
|
2479
|
+
async getContentBySlug(collection, slug) {
|
|
2480
|
+
const rows = await this.client.select().from(cmsContentTable).where((0, _azlib_persistence.and)((0, _azlib_persistence.eq)(cmsContentTable.slug, slug), (0, _azlib_persistence.eq)(cmsContentTable.collection, collection))).execute();
|
|
2481
|
+
if (!rows || rows.length === 0) return null;
|
|
2482
|
+
return this.deserializeContent(rows[0]);
|
|
2483
|
+
}
|
|
2484
|
+
async findContent(collection, options = {}) {
|
|
2485
|
+
const conditions = [(0, _azlib_persistence.eq)(cmsContentTable.collection, collection)];
|
|
2486
|
+
if (options.status) if (Array.isArray(options.status)) conditions.push((0, _azlib_persistence.inArray)(cmsContentTable.status, options.status));
|
|
2487
|
+
else conditions.push((0, _azlib_persistence.eq)(cmsContentTable.status, options.status));
|
|
2488
|
+
if (options.authorId) conditions.push((0, _azlib_persistence.eq)(cmsContentTable.authorId, options.authorId));
|
|
2489
|
+
if (options.parentId !== void 0) conditions.push((0, _azlib_persistence.eq)(cmsContentTable.parentId, options.parentId));
|
|
2490
|
+
if (options.locale) conditions.push((0, _azlib_persistence.eq)(cmsContentTable.locale, options.locale));
|
|
2491
|
+
if (options.search) {
|
|
2492
|
+
const q = `%${options.search}%`;
|
|
2493
|
+
conditions.push((0, _azlib_persistence.or)((0, _azlib_persistence.like)(cmsContentTable.title, q), (0, _azlib_persistence.like)(cmsContentTable.slug, q), (0, _azlib_persistence.like)(cmsContentTable.data, q)));
|
|
2494
|
+
}
|
|
2495
|
+
if (options.termIds && options.termIds.length > 0) {
|
|
2496
|
+
const termRows = await this.client.select().from(cmsContentTermsTable).where((0, _azlib_persistence.inArray)(cmsContentTermsTable.termId, options.termIds)).execute();
|
|
2497
|
+
const matchedContentIds = Array.from(new Set(termRows.map((r) => r.contentId || r.content_id)));
|
|
2498
|
+
if (matchedContentIds.length === 0) return {
|
|
2499
|
+
items: [],
|
|
2500
|
+
total: 0,
|
|
2501
|
+
limit: options.limit ?? 20,
|
|
2502
|
+
offset: options.offset ?? 0,
|
|
2503
|
+
hasMore: false
|
|
2504
|
+
};
|
|
2505
|
+
conditions.push((0, _azlib_persistence.inArray)(cmsContentTable.id, matchedContentIds));
|
|
2506
|
+
}
|
|
2507
|
+
const whereClause = conditions.length === 1 ? conditions[0] : (0, _azlib_persistence.and)(...conditions);
|
|
2508
|
+
let list = (await this.client.select().from(cmsContentTable).where(whereClause).execute()).map((r) => this.deserializeContent(r));
|
|
2509
|
+
if (options.where) list = list.filter((item) => {
|
|
2510
|
+
for (const [key, expected] of Object.entries(options.where)) {
|
|
2511
|
+
const actual = item.data[key] !== void 0 ? item.data[key] : item[key];
|
|
2512
|
+
if (!this.matchesCondition(actual, expected)) return false;
|
|
2513
|
+
}
|
|
2514
|
+
return true;
|
|
2515
|
+
});
|
|
2516
|
+
const total = list.length;
|
|
2517
|
+
const orderBy = options.orderBy || "createdAt";
|
|
2518
|
+
const dir = options.orderDirection === "asc" ? 1 : -1;
|
|
2519
|
+
list.sort((a, b) => {
|
|
2520
|
+
const valA = a[orderBy] ?? a.data[orderBy] ?? "";
|
|
2521
|
+
const valB = b[orderBy] ?? b.data[orderBy] ?? "";
|
|
2522
|
+
if (valA < valB) return -1 * dir;
|
|
2523
|
+
if (valA > valB) return 1 * dir;
|
|
2524
|
+
return 0;
|
|
2525
|
+
});
|
|
2526
|
+
const offset = options.offset ?? 0;
|
|
2527
|
+
const limit = options.limit ?? 20;
|
|
2528
|
+
let items = list.slice(offset, offset + limit);
|
|
2529
|
+
if (options.populate && options.populate.length > 0) items = await Promise.all(items.map((it) => this.populateItem(it, options.populate)));
|
|
2530
|
+
return {
|
|
2531
|
+
items,
|
|
2532
|
+
total,
|
|
2533
|
+
limit,
|
|
2534
|
+
offset,
|
|
2535
|
+
hasMore: offset + limit < total
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
async updateContent(collection, id, updates) {
|
|
2539
|
+
const existing = await this.getContent(collection, id);
|
|
2540
|
+
if (!existing) return null;
|
|
2541
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2542
|
+
const nextVersion = existing.version + 1;
|
|
2543
|
+
const mergedData = {
|
|
2544
|
+
...existing.data,
|
|
2545
|
+
...updates.data || {}
|
|
2546
|
+
};
|
|
2547
|
+
const updateRecord = {
|
|
2548
|
+
updatedAt: now,
|
|
2549
|
+
version: nextVersion,
|
|
2550
|
+
data: JSON.stringify(mergedData)
|
|
2551
|
+
};
|
|
2552
|
+
if (updates.title !== void 0) updateRecord.title = updates.title;
|
|
2553
|
+
if (updates.slug !== void 0) updateRecord.slug = updates.slug;
|
|
2554
|
+
if (updates.status !== void 0) updateRecord.status = updates.status;
|
|
2555
|
+
if (updates.parentId !== void 0) updateRecord.parentId = updates.parentId;
|
|
2556
|
+
if (updates.authorId !== void 0) updateRecord.authorId = updates.authorId;
|
|
2557
|
+
if (updates.publishedAt !== void 0) updateRecord.publishedAt = updates.publishedAt;
|
|
2558
|
+
if (updates.scheduledAt !== void 0) updateRecord.scheduledAt = updates.scheduledAt;
|
|
2559
|
+
if (updates.locale !== void 0) updateRecord.locale = updates.locale;
|
|
2560
|
+
if (updates.terms !== void 0) updateRecord.terms = JSON.stringify(updates.terms);
|
|
2561
|
+
await this.client.update(cmsContentTable).set(updateRecord).where((0, _azlib_persistence.and)((0, _azlib_persistence.eq)(cmsContentTable.id, id), (0, _azlib_persistence.eq)(cmsContentTable.collection, collection))).execute();
|
|
2562
|
+
return this.getContent(collection, id);
|
|
2563
|
+
}
|
|
2564
|
+
async deleteContent(collection, id) {
|
|
2565
|
+
if (!await this.getContent(collection, id)) return false;
|
|
2566
|
+
await this.client.delete(cmsContentTable).where((0, _azlib_persistence.and)((0, _azlib_persistence.eq)(cmsContentTable.id, id), (0, _azlib_persistence.eq)(cmsContentTable.collection, collection))).execute();
|
|
2567
|
+
await this.client.delete(cmsContentTermsTable).where((0, _azlib_persistence.eq)(cmsContentTermsTable.contentId, id)).execute();
|
|
2568
|
+
await this.deleteRevisionsByContentId(id);
|
|
2569
|
+
return true;
|
|
2570
|
+
}
|
|
2571
|
+
async countContent(collection, options = {}) {
|
|
2572
|
+
return (await this.findContent(collection, {
|
|
2573
|
+
...options,
|
|
2574
|
+
limit: 1e6
|
|
2575
|
+
})).total;
|
|
2576
|
+
}
|
|
2577
|
+
async createRevision(contentId, collection, version, snapshot, authorId, note) {
|
|
2578
|
+
const id = generateId("rev_");
|
|
2579
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2580
|
+
const record = {
|
|
2581
|
+
id,
|
|
2582
|
+
contentId,
|
|
2583
|
+
collection,
|
|
2584
|
+
version,
|
|
2585
|
+
snapshot: JSON.stringify(snapshot),
|
|
2586
|
+
authorId: authorId ?? null,
|
|
2587
|
+
note: note ?? null,
|
|
2588
|
+
createdAt: now
|
|
2589
|
+
};
|
|
2590
|
+
await this.client.insert(cmsRevisionsTable).values(record).execute();
|
|
2591
|
+
return {
|
|
2592
|
+
id,
|
|
2593
|
+
contentId,
|
|
2594
|
+
collection,
|
|
2595
|
+
version,
|
|
2596
|
+
snapshot,
|
|
2597
|
+
authorId,
|
|
2598
|
+
note,
|
|
2599
|
+
createdAt: now
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
async getRevisions(contentId) {
|
|
2603
|
+
return (await this.client.select().from(cmsRevisionsTable).where((0, _azlib_persistence.eq)(cmsRevisionsTable.contentId, contentId)).execute()).map((r) => ({
|
|
2604
|
+
id: r.id,
|
|
2605
|
+
contentId: r.contentId || r.content_id,
|
|
2606
|
+
collection: r.collection,
|
|
2607
|
+
version: Number(r.version),
|
|
2608
|
+
snapshot: typeof r.snapshot === "string" ? JSON.parse(r.snapshot) : r.snapshot,
|
|
2609
|
+
authorId: r.authorId || r.author_id || null,
|
|
2610
|
+
note: r.note || void 0,
|
|
2611
|
+
createdAt: r.createdAt || r.created_at
|
|
2612
|
+
}));
|
|
2613
|
+
}
|
|
2614
|
+
async getRevision(revisionId) {
|
|
2615
|
+
const rows = await this.client.select().from(cmsRevisionsTable).where((0, _azlib_persistence.eq)(cmsRevisionsTable.id, revisionId)).execute();
|
|
2616
|
+
if (!rows || rows.length === 0) return null;
|
|
2617
|
+
const r = rows[0];
|
|
2618
|
+
return {
|
|
2619
|
+
id: r.id,
|
|
2620
|
+
contentId: r.contentId || r.content_id,
|
|
2621
|
+
collection: r.collection,
|
|
2622
|
+
version: Number(r.version),
|
|
2623
|
+
snapshot: typeof r.snapshot === "string" ? JSON.parse(r.snapshot) : r.snapshot,
|
|
2624
|
+
authorId: r.authorId || r.author_id || null,
|
|
2625
|
+
note: r.note || void 0,
|
|
2626
|
+
createdAt: r.createdAt || r.created_at
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
async deleteRevisionsByContentId(contentId) {
|
|
2630
|
+
const rows = await this.getRevisions(contentId);
|
|
2631
|
+
await this.client.delete(cmsRevisionsTable).where((0, _azlib_persistence.eq)(cmsRevisionsTable.contentId, contentId)).execute();
|
|
2632
|
+
return rows.length;
|
|
2633
|
+
}
|
|
2634
|
+
async createTerm(term) {
|
|
2635
|
+
const id = generateId("trm_");
|
|
2636
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2637
|
+
const record = {
|
|
2638
|
+
id,
|
|
2639
|
+
taxonomy: term.taxonomy,
|
|
2640
|
+
name: term.name,
|
|
2641
|
+
slug: term.slug,
|
|
2642
|
+
description: term.description ?? null,
|
|
2643
|
+
parentId: term.parentId ?? null,
|
|
2644
|
+
count: 0,
|
|
2645
|
+
meta: term.meta ? JSON.stringify(term.meta) : null,
|
|
2646
|
+
createdAt: now,
|
|
2647
|
+
updatedAt: now
|
|
2648
|
+
};
|
|
2649
|
+
await this.client.insert(cmsTermsTable).values(record).execute();
|
|
2650
|
+
return {
|
|
2651
|
+
id,
|
|
2652
|
+
taxonomy: term.taxonomy,
|
|
2653
|
+
name: term.name,
|
|
2654
|
+
slug: term.slug,
|
|
2655
|
+
description: term.description,
|
|
2656
|
+
parentId: term.parentId ?? null,
|
|
2657
|
+
count: 0,
|
|
2658
|
+
meta: term.meta,
|
|
2659
|
+
createdAt: now,
|
|
2660
|
+
updatedAt: now
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
async getTermById(id) {
|
|
2664
|
+
const rows = await this.client.select().from(cmsTermsTable).where((0, _azlib_persistence.eq)(cmsTermsTable.id, id)).execute();
|
|
2665
|
+
if (!rows || rows.length === 0) return null;
|
|
2666
|
+
return this.deserializeTerm(rows[0]);
|
|
2667
|
+
}
|
|
2668
|
+
async getTermBySlug(taxonomy, slug) {
|
|
2669
|
+
const rows = await this.client.select().from(cmsTermsTable).where((0, _azlib_persistence.and)((0, _azlib_persistence.eq)(cmsTermsTable.taxonomy, taxonomy), (0, _azlib_persistence.eq)(cmsTermsTable.slug, slug))).execute();
|
|
2670
|
+
if (!rows || rows.length === 0) return null;
|
|
2671
|
+
return this.deserializeTerm(rows[0]);
|
|
2672
|
+
}
|
|
2673
|
+
async getTerms(taxonomy, options) {
|
|
2674
|
+
const conditions = [(0, _azlib_persistence.eq)(cmsTermsTable.taxonomy, taxonomy)];
|
|
2675
|
+
if (options?.parentId !== void 0) conditions.push((0, _azlib_persistence.eq)(cmsTermsTable.parentId, options.parentId));
|
|
2676
|
+
return (await this.client.select().from(cmsTermsTable).where(conditions.length === 1 ? conditions[0] : (0, _azlib_persistence.and)(...conditions)).execute()).map((r) => this.deserializeTerm(r));
|
|
2677
|
+
}
|
|
2678
|
+
async updateTerm(id, updates) {
|
|
2679
|
+
if (!await this.getTermById(id)) return null;
|
|
2680
|
+
const updateRecord = { updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2681
|
+
if (updates.name !== void 0) updateRecord.name = updates.name;
|
|
2682
|
+
if (updates.slug !== void 0) updateRecord.slug = updates.slug;
|
|
2683
|
+
if (updates.description !== void 0) updateRecord.description = updates.description;
|
|
2684
|
+
if (updates.parentId !== void 0) updateRecord.parentId = updates.parentId;
|
|
2685
|
+
if (updates.count !== void 0) updateRecord.count = updates.count;
|
|
2686
|
+
if (updates.meta !== void 0) updateRecord.meta = JSON.stringify(updates.meta);
|
|
2687
|
+
await this.client.update(cmsTermsTable).set(updateRecord).where((0, _azlib_persistence.eq)(cmsTermsTable.id, id)).execute();
|
|
2688
|
+
return this.getTermById(id);
|
|
2689
|
+
}
|
|
2690
|
+
async deleteTerm(id) {
|
|
2691
|
+
if (!await this.getTermById(id)) return false;
|
|
2692
|
+
await this.client.delete(cmsTermsTable).where((0, _azlib_persistence.eq)(cmsTermsTable.id, id)).execute();
|
|
2693
|
+
await this.client.delete(cmsContentTermsTable).where((0, _azlib_persistence.eq)(cmsContentTermsTable.termId, id)).execute();
|
|
2694
|
+
return true;
|
|
2695
|
+
}
|
|
2696
|
+
async assignTermsToContent(contentId, termIds) {
|
|
2697
|
+
for (const termId of termIds) try {
|
|
2698
|
+
await this.client.insert(cmsContentTermsTable).values({
|
|
2699
|
+
contentId,
|
|
2700
|
+
termId
|
|
2701
|
+
}).execute();
|
|
2702
|
+
const term = await this.getTermById(termId);
|
|
2703
|
+
if (term) await this.updateTerm(termId, { count: term.count + 1 });
|
|
2704
|
+
} catch {}
|
|
2705
|
+
}
|
|
2706
|
+
async getContentTerms(contentId, taxonomy) {
|
|
2707
|
+
const termIds = (await this.client.select().from(cmsContentTermsTable).where((0, _azlib_persistence.eq)(cmsContentTermsTable.contentId, contentId)).execute()).map((r) => r.termId || r.term_id);
|
|
2708
|
+
if (termIds.length === 0) return [];
|
|
2709
|
+
const terms = [];
|
|
2710
|
+
for (const tId of termIds) {
|
|
2711
|
+
const t = await this.getTermById(tId);
|
|
2712
|
+
if (t && (!taxonomy || t.taxonomy === taxonomy)) terms.push(t);
|
|
2713
|
+
}
|
|
2714
|
+
return terms;
|
|
2715
|
+
}
|
|
2716
|
+
async removeTermsFromContent(contentId, termIds) {
|
|
2717
|
+
for (const termId of termIds) {
|
|
2718
|
+
await this.client.delete(cmsContentTermsTable).where((0, _azlib_persistence.and)((0, _azlib_persistence.eq)(cmsContentTermsTable.contentId, contentId), (0, _azlib_persistence.eq)(cmsContentTermsTable.termId, termId))).execute();
|
|
2719
|
+
const term = await this.getTermById(termId);
|
|
2720
|
+
if (term && term.count > 0) await this.updateTerm(termId, { count: term.count - 1 });
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
async createMedia(item) {
|
|
2724
|
+
const id = generateId("med_");
|
|
2725
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2726
|
+
const record = {
|
|
2727
|
+
id,
|
|
2728
|
+
filename: item.filename,
|
|
2729
|
+
originalName: item.originalName,
|
|
2730
|
+
mimeType: item.mimeType,
|
|
2731
|
+
sizeBytes: item.sizeBytes,
|
|
2732
|
+
url: item.url,
|
|
2733
|
+
path: item.path ?? null,
|
|
2734
|
+
width: item.width ?? null,
|
|
2735
|
+
height: item.height ?? null,
|
|
2736
|
+
altText: item.altText ?? null,
|
|
2737
|
+
caption: item.caption ?? null,
|
|
2738
|
+
authorId: item.authorId ?? null,
|
|
2739
|
+
variants: item.variants ? JSON.stringify(item.variants) : null,
|
|
2740
|
+
createdAt: now,
|
|
2741
|
+
updatedAt: now
|
|
2742
|
+
};
|
|
2743
|
+
await this.client.insert(cmsMediaTable).values(record).execute();
|
|
2744
|
+
return {
|
|
2745
|
+
id,
|
|
2746
|
+
filename: item.filename,
|
|
2747
|
+
originalName: item.originalName,
|
|
2748
|
+
mimeType: item.mimeType,
|
|
2749
|
+
sizeBytes: item.sizeBytes,
|
|
2750
|
+
url: item.url,
|
|
2751
|
+
path: item.path,
|
|
2752
|
+
width: item.width,
|
|
2753
|
+
height: item.height,
|
|
2754
|
+
altText: item.altText,
|
|
2755
|
+
caption: item.caption,
|
|
2756
|
+
authorId: item.authorId,
|
|
2757
|
+
variants: item.variants,
|
|
2758
|
+
createdAt: now,
|
|
2759
|
+
updatedAt: now
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
async getMedia(id) {
|
|
2763
|
+
const rows = await this.client.select().from(cmsMediaTable).where((0, _azlib_persistence.eq)(cmsMediaTable.id, id)).execute();
|
|
2764
|
+
if (!rows || rows.length === 0) return null;
|
|
2765
|
+
return this.deserializeMedia(rows[0]);
|
|
2766
|
+
}
|
|
2767
|
+
async findMedia(options) {
|
|
2768
|
+
const conditions = [];
|
|
2769
|
+
if (options?.mimeType) conditions.push((0, _azlib_persistence.like)(cmsMediaTable.mimeType, `${options.mimeType}%`));
|
|
2770
|
+
if (options?.authorId) conditions.push((0, _azlib_persistence.eq)(cmsMediaTable.authorId, options.authorId));
|
|
2771
|
+
if (options?.search) conditions.push((0, _azlib_persistence.or)((0, _azlib_persistence.like)(cmsMediaTable.filename, `%${options.search}%`), (0, _azlib_persistence.like)(cmsMediaTable.originalName, `%${options.search}%`)));
|
|
2772
|
+
const items = (await this.client.select().from(cmsMediaTable).where(conditions.length > 0 ? conditions.length === 1 ? conditions[0] : (0, _azlib_persistence.and)(...conditions) : void 0).execute()).map((r) => this.deserializeMedia(r));
|
|
2773
|
+
const total = items.length;
|
|
2774
|
+
const offset = options?.offset ?? 0;
|
|
2775
|
+
const limit = options?.limit ?? 20;
|
|
2776
|
+
return {
|
|
2777
|
+
items: items.slice(offset, offset + limit),
|
|
2778
|
+
total,
|
|
2779
|
+
limit,
|
|
2780
|
+
offset,
|
|
2781
|
+
hasMore: offset + limit < total
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
async updateMedia(id, updates) {
|
|
2785
|
+
if (!await this.getMedia(id)) return null;
|
|
2786
|
+
const updateRecord = { updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2787
|
+
if (updates.filename !== void 0) updateRecord.filename = updates.filename;
|
|
2788
|
+
if (updates.altText !== void 0) updateRecord.altText = updates.altText;
|
|
2789
|
+
if (updates.caption !== void 0) updateRecord.caption = updates.caption;
|
|
2790
|
+
if (updates.width !== void 0) updateRecord.width = updates.width;
|
|
2791
|
+
if (updates.height !== void 0) updateRecord.height = updates.height;
|
|
2792
|
+
if (updates.variants !== void 0) updateRecord.variants = JSON.stringify(updates.variants);
|
|
2793
|
+
await this.client.update(cmsMediaTable).set(updateRecord).where((0, _azlib_persistence.eq)(cmsMediaTable.id, id)).execute();
|
|
2794
|
+
return this.getMedia(id);
|
|
2795
|
+
}
|
|
2796
|
+
async deleteMedia(id) {
|
|
2797
|
+
if (!await this.getMedia(id)) return false;
|
|
2798
|
+
await this.client.delete(cmsMediaTable).where((0, _azlib_persistence.eq)(cmsMediaTable.id, id)).execute();
|
|
2799
|
+
return true;
|
|
2800
|
+
}
|
|
2801
|
+
async getOption(key) {
|
|
2802
|
+
const rows = await this.client.select().from(cmsOptionsTable).where((0, _azlib_persistence.eq)(cmsOptionsTable.key, key)).execute();
|
|
2803
|
+
if (!rows || rows.length === 0) return null;
|
|
2804
|
+
const r = rows[0];
|
|
2805
|
+
return {
|
|
2806
|
+
key: r.key,
|
|
2807
|
+
value: typeof r.value === "string" ? JSON.parse(r.value) : r.value,
|
|
2808
|
+
autoload: Boolean(r.autoload),
|
|
2809
|
+
namespace: r.namespace || void 0,
|
|
2810
|
+
updatedAt: r.updatedAt || r.updated_at
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
async setOption(item) {
|
|
2814
|
+
const existing = await this.getOption(item.key);
|
|
2815
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2816
|
+
if (existing) await this.client.update(cmsOptionsTable).set({
|
|
2817
|
+
value: JSON.stringify(item.value),
|
|
2818
|
+
autoload: item.autoload,
|
|
2819
|
+
namespace: item.namespace ?? null,
|
|
2820
|
+
updatedAt: now
|
|
2821
|
+
}).where((0, _azlib_persistence.eq)(cmsOptionsTable.key, item.key)).execute();
|
|
2822
|
+
else await this.client.insert(cmsOptionsTable).values({
|
|
2823
|
+
key: item.key,
|
|
2824
|
+
value: JSON.stringify(item.value),
|
|
2825
|
+
autoload: item.autoload,
|
|
2826
|
+
namespace: item.namespace ?? null,
|
|
2827
|
+
updatedAt: now
|
|
2828
|
+
}).execute();
|
|
2829
|
+
}
|
|
2830
|
+
async deleteOption(key) {
|
|
2831
|
+
if (!await this.getOption(key)) return false;
|
|
2832
|
+
await this.client.delete(cmsOptionsTable).where((0, _azlib_persistence.eq)(cmsOptionsTable.key, key)).execute();
|
|
2833
|
+
return true;
|
|
2834
|
+
}
|
|
2835
|
+
async getOptions(namespace) {
|
|
2836
|
+
const builder = this.client.select().from(cmsOptionsTable);
|
|
2837
|
+
return (await (namespace ? builder.where((0, _azlib_persistence.eq)(cmsOptionsTable.namespace, namespace)).execute() : builder.execute())).map((r) => ({
|
|
2838
|
+
key: r.key,
|
|
2839
|
+
value: typeof r.value === "string" ? JSON.parse(r.value) : r.value,
|
|
2840
|
+
autoload: Boolean(r.autoload),
|
|
2841
|
+
namespace: r.namespace || void 0,
|
|
2842
|
+
updatedAt: r.updatedAt || r.updated_at
|
|
2843
|
+
}));
|
|
2844
|
+
}
|
|
2845
|
+
deserializeContent(row) {
|
|
2846
|
+
return {
|
|
2847
|
+
id: row.id,
|
|
2848
|
+
collection: row.collection,
|
|
2849
|
+
slug: row.slug,
|
|
2850
|
+
status: row.status,
|
|
2851
|
+
title: row.title || void 0,
|
|
2852
|
+
parentId: row.parentId || row.parent_id || null,
|
|
2853
|
+
authorId: row.authorId || row.author_id || null,
|
|
2854
|
+
publishedAt: row.publishedAt || row.published_at || null,
|
|
2855
|
+
scheduledAt: row.scheduledAt || row.scheduled_at || null,
|
|
2856
|
+
createdAt: row.createdAt || row.created_at,
|
|
2857
|
+
updatedAt: row.updatedAt || row.updated_at,
|
|
2858
|
+
version: Number(row.version),
|
|
2859
|
+
locale: row.locale || void 0,
|
|
2860
|
+
data: typeof row.data === "string" ? JSON.parse(row.data) : row.data || {},
|
|
2861
|
+
terms: row.terms ? typeof row.terms === "string" ? JSON.parse(row.terms) : row.terms : void 0
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
deserializeTerm(row) {
|
|
2865
|
+
return {
|
|
2866
|
+
id: row.id,
|
|
2867
|
+
taxonomy: row.taxonomy,
|
|
2868
|
+
name: row.name,
|
|
2869
|
+
slug: row.slug,
|
|
2870
|
+
description: row.description || void 0,
|
|
2871
|
+
parentId: row.parentId || row.parent_id || null,
|
|
2872
|
+
count: Number(row.count),
|
|
2873
|
+
meta: row.meta ? typeof row.meta === "string" ? JSON.parse(row.meta) : row.meta : void 0,
|
|
2874
|
+
createdAt: row.createdAt || row.created_at,
|
|
2875
|
+
updatedAt: row.updatedAt || row.updated_at
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
deserializeMedia(row) {
|
|
2879
|
+
return {
|
|
2880
|
+
id: row.id,
|
|
2881
|
+
filename: row.filename,
|
|
2882
|
+
originalName: row.originalName || row.original_name,
|
|
2883
|
+
mimeType: row.mimeType || row.mime_type,
|
|
2884
|
+
sizeBytes: Number(row.sizeBytes || row.size_bytes),
|
|
2885
|
+
url: row.url,
|
|
2886
|
+
path: row.path || void 0,
|
|
2887
|
+
width: row.width ? Number(row.width) : void 0,
|
|
2888
|
+
height: row.height ? Number(row.height) : void 0,
|
|
2889
|
+
altText: row.altText || row.alt_text || void 0,
|
|
2890
|
+
caption: row.caption || void 0,
|
|
2891
|
+
authorId: row.authorId || row.author_id || null,
|
|
2892
|
+
variants: row.variants ? typeof row.variants === "string" ? JSON.parse(row.variants) : row.variants : void 0,
|
|
2893
|
+
createdAt: row.createdAt || row.created_at,
|
|
2894
|
+
updatedAt: row.updatedAt || row.updated_at
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
async populateItem(item, populateFields) {
|
|
2898
|
+
const populated = { ...item.populated || {} };
|
|
2899
|
+
for (const field of populateFields) {
|
|
2900
|
+
const rawVal = item.data[field];
|
|
2901
|
+
if (!rawVal) continue;
|
|
2902
|
+
if (Array.isArray(rawVal)) populated[field] = await Promise.all(rawVal.map(async (id) => {
|
|
2903
|
+
if (typeof id === "string") {
|
|
2904
|
+
const rows = await this.client.select().from(cmsContentTable).where((0, _azlib_persistence.eq)(cmsContentTable.id, id)).execute();
|
|
2905
|
+
if (rows.length > 0) return this.deserializeContent(rows[0]);
|
|
2906
|
+
const m = await this.getMedia(id);
|
|
2907
|
+
if (m) return m;
|
|
2908
|
+
}
|
|
2909
|
+
return id;
|
|
2910
|
+
}));
|
|
2911
|
+
else if (typeof rawVal === "string") {
|
|
2912
|
+
const rows = await this.client.select().from(cmsContentTable).where((0, _azlib_persistence.eq)(cmsContentTable.id, rawVal)).execute();
|
|
2913
|
+
if (rows.length > 0) populated[field] = this.deserializeContent(rows[0]);
|
|
2914
|
+
else {
|
|
2915
|
+
const m = await this.getMedia(rawVal);
|
|
2916
|
+
if (m) populated[field] = m;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
return {
|
|
2921
|
+
...item,
|
|
2922
|
+
populated
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2925
|
+
matchesCondition(actualValue, expected) {
|
|
2926
|
+
if (expected === null || expected === void 0 || typeof expected !== "object" || Array.isArray(expected)) return actualValue === expected;
|
|
2927
|
+
const exp = expected;
|
|
2928
|
+
if (!Object.keys(exp).some((k) => k.startsWith("$"))) return JSON.stringify(actualValue) === JSON.stringify(expected);
|
|
2929
|
+
for (const [op, target] of Object.entries(exp)) switch (op) {
|
|
2930
|
+
case "$eq":
|
|
2931
|
+
if (actualValue !== target) return false;
|
|
2932
|
+
break;
|
|
2933
|
+
case "$ne":
|
|
2934
|
+
if (actualValue === target) return false;
|
|
2935
|
+
break;
|
|
2936
|
+
case "$gt":
|
|
2937
|
+
if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
|
|
2938
|
+
if (actualValue <= target) return false;
|
|
2939
|
+
break;
|
|
2940
|
+
case "$gte":
|
|
2941
|
+
if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
|
|
2942
|
+
if (actualValue < target) return false;
|
|
2943
|
+
break;
|
|
2944
|
+
case "$lt":
|
|
2945
|
+
if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
|
|
2946
|
+
if (actualValue >= target) return false;
|
|
2947
|
+
break;
|
|
2948
|
+
case "$lte":
|
|
2949
|
+
if (typeof actualValue !== "number" && typeof actualValue !== "string") return false;
|
|
2950
|
+
if (actualValue > target) return false;
|
|
2951
|
+
break;
|
|
2952
|
+
case "$in":
|
|
2953
|
+
if (!Array.isArray(target) || !target.includes(actualValue)) return false;
|
|
2954
|
+
break;
|
|
2955
|
+
case "$nin":
|
|
2956
|
+
if (Array.isArray(target) && target.includes(actualValue)) return false;
|
|
2957
|
+
break;
|
|
2958
|
+
case "$contains":
|
|
2959
|
+
if (typeof actualValue !== "string" || !actualValue.toLowerCase().includes(String(target).toLowerCase())) return false;
|
|
2960
|
+
break;
|
|
2961
|
+
case "$startsWith":
|
|
2962
|
+
if (typeof actualValue !== "string" || !actualValue.startsWith(String(target))) return false;
|
|
2963
|
+
break;
|
|
2964
|
+
case "$endsWith":
|
|
2965
|
+
if (typeof actualValue !== "string" || !actualValue.endsWith(String(target))) return false;
|
|
2966
|
+
break;
|
|
2967
|
+
case "$between":
|
|
2968
|
+
if (!Array.isArray(target) || target.length !== 2) return false;
|
|
2969
|
+
if (actualValue < target[0] || actualValue > target[1]) return false;
|
|
2970
|
+
break;
|
|
2971
|
+
case "$exists":
|
|
2972
|
+
if (actualValue !== void 0 !== Boolean(target)) return false;
|
|
2973
|
+
break;
|
|
2974
|
+
}
|
|
2975
|
+
return true;
|
|
2976
|
+
}
|
|
2977
|
+
};
|
|
2978
|
+
//#endregion
|
|
1579
2979
|
//#region src/api/router.ts
|
|
1580
2980
|
function compilePath(path) {
|
|
1581
2981
|
const keys = [];
|
|
@@ -1602,7 +3002,7 @@ function jsonResponse(data, status = 200, headers = {}) {
|
|
|
1602
3002
|
"Content-Type": "application/json",
|
|
1603
3003
|
"Access-Control-Allow-Origin": "*",
|
|
1604
3004
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
1605
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
3005
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key",
|
|
1606
3006
|
...headers
|
|
1607
3007
|
}
|
|
1608
3008
|
});
|
|
@@ -1639,10 +3039,11 @@ var CMSRouter = class {
|
|
|
1639
3039
|
headers: {
|
|
1640
3040
|
"Access-Control-Allow-Origin": "*",
|
|
1641
3041
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
1642
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
3042
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key"
|
|
1643
3043
|
}
|
|
1644
3044
|
});
|
|
1645
3045
|
try {
|
|
3046
|
+
const user = await this.engine.auth.authenticateRequest(request);
|
|
1646
3047
|
if (pathname === "/api/cron/scheduled" && method === "POST") {
|
|
1647
3048
|
const published = await this.engine.processScheduledContent();
|
|
1648
3049
|
return jsonResponse({
|
|
@@ -1651,10 +3052,11 @@ var CMSRouter = class {
|
|
|
1651
3052
|
items: published
|
|
1652
3053
|
});
|
|
1653
3054
|
}
|
|
1654
|
-
if (pathname.startsWith("/api/
|
|
1655
|
-
if (pathname.startsWith("/api/
|
|
1656
|
-
if (pathname.startsWith("/api/
|
|
1657
|
-
if (pathname.startsWith("/api/
|
|
3055
|
+
if (pathname.startsWith("/api/auth")) return this.handleAuth(pathname, method, request, user);
|
|
3056
|
+
if (pathname.startsWith("/api/options")) return this.handleOptions(pathname, method, url, request, user);
|
|
3057
|
+
if (pathname.startsWith("/api/taxonomies")) return this.handleTaxonomies(pathname, method, url, request, user);
|
|
3058
|
+
if (pathname.startsWith("/api/media")) return this.handleMedia(pathname, method, url, request, user);
|
|
3059
|
+
if (pathname.startsWith("/api/content")) return this.handleContent(pathname, method, url, request, user);
|
|
1658
3060
|
const customResponse = await this.handleCustomRoute(pathname, method, url, request);
|
|
1659
3061
|
if (customResponse) return customResponse;
|
|
1660
3062
|
return jsonResponse({
|
|
@@ -1688,55 +3090,126 @@ var CMSRouter = class {
|
|
|
1688
3090
|
}
|
|
1689
3091
|
return null;
|
|
1690
3092
|
}
|
|
1691
|
-
async
|
|
3093
|
+
async handleAuth(pathname, method, request, user) {
|
|
3094
|
+
if (pathname === "/api/auth/register" && method === "POST") {
|
|
3095
|
+
const body = await request.json();
|
|
3096
|
+
try {
|
|
3097
|
+
return jsonResponse(await this.engine.auth.register(body), 201);
|
|
3098
|
+
} catch (err) {
|
|
3099
|
+
return jsonResponse({ error: err.message }, 400);
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
if (pathname === "/api/auth/login" && method === "POST") {
|
|
3103
|
+
const body = await request.json();
|
|
3104
|
+
try {
|
|
3105
|
+
return jsonResponse(await this.engine.auth.login(body.identifier || body.username || body.email, body.password), 200);
|
|
3106
|
+
} catch (err) {
|
|
3107
|
+
return jsonResponse({ error: err.message }, 401);
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
if (pathname === "/api/auth/me" && method === "GET") {
|
|
3111
|
+
if (!user) return jsonResponse({ error: "Unauthorized: Authentication required" }, 401);
|
|
3112
|
+
return jsonResponse(user, 200);
|
|
3113
|
+
}
|
|
3114
|
+
return jsonResponse({ error: "Endpoint not found" }, 404);
|
|
3115
|
+
}
|
|
3116
|
+
async handleContent(pathname, method, url, request, user) {
|
|
1692
3117
|
const parts = pathname.replace(/^\/api\/content\/?/, "").split("/").filter(Boolean);
|
|
1693
|
-
|
|
1694
|
-
if (!collectionSlug) return jsonResponse({ collections: this.engine.getCollections().map((c) => ({
|
|
3118
|
+
if (parts.length === 0 && method === "GET") return jsonResponse({ collections: this.engine.getCollections().map((c) => ({
|
|
1695
3119
|
slug: c.slug,
|
|
1696
3120
|
label: c.label,
|
|
1697
3121
|
hierarchical: c.hierarchical
|
|
1698
3122
|
})) }, 200);
|
|
3123
|
+
const collectionSlug = parts[0];
|
|
3124
|
+
const collConfig = this.engine.getCollectionConfig(collectionSlug);
|
|
3125
|
+
if (!collConfig) return jsonResponse({ error: `Collection '${collectionSlug}' not found` }, 404);
|
|
1699
3126
|
const coll = this.engine.collection(collectionSlug);
|
|
1700
3127
|
if (parts.length === 1 && method === "GET") {
|
|
1701
3128
|
const status = url.searchParams.get("status") || void 0;
|
|
1702
3129
|
const search = url.searchParams.get("search") || void 0;
|
|
3130
|
+
const locale = url.searchParams.get("locale") || void 0;
|
|
1703
3131
|
const limit = url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : void 0;
|
|
1704
3132
|
const offset = url.searchParams.has("offset") ? Number(url.searchParams.get("offset")) : void 0;
|
|
1705
3133
|
const termIds = url.searchParams.has("termIds") ? url.searchParams.get("termIds").split(",") : void 0;
|
|
1706
|
-
|
|
3134
|
+
const populate = url.searchParams.has("populate") ? url.searchParams.get("populate").split(",") : void 0;
|
|
3135
|
+
const select = url.searchParams.has("select") ? url.searchParams.get("select").split(",") : void 0;
|
|
3136
|
+
let where = void 0;
|
|
3137
|
+
if (url.searchParams.has("where")) try {
|
|
3138
|
+
where = JSON.parse(url.searchParams.get("where"));
|
|
3139
|
+
} catch {}
|
|
3140
|
+
const result = await coll.find({
|
|
1707
3141
|
status,
|
|
1708
3142
|
search,
|
|
1709
3143
|
limit,
|
|
1710
3144
|
offset,
|
|
1711
|
-
termIds
|
|
1712
|
-
|
|
3145
|
+
termIds,
|
|
3146
|
+
populate,
|
|
3147
|
+
select,
|
|
3148
|
+
locale,
|
|
3149
|
+
where
|
|
3150
|
+
});
|
|
3151
|
+
result.items = result.items.map((it) => this.filterRestrictedFields(it, collConfig, user));
|
|
3152
|
+
return jsonResponse(result);
|
|
1713
3153
|
}
|
|
1714
3154
|
if (parts.length === 1 && method === "POST") {
|
|
3155
|
+
if (this.engine.auth.enabled && !this.engine.rbac.can(user, "edit_posts")) return jsonResponse({ error: "Forbidden: You do not have permission to create content." }, user ? 403 : 401);
|
|
1715
3156
|
const body = await request.json();
|
|
1716
|
-
|
|
3157
|
+
const created = await coll.create(body, user?.id);
|
|
3158
|
+
return jsonResponse(this.filterRestrictedFields(created, collConfig, user), 201);
|
|
1717
3159
|
}
|
|
1718
3160
|
const idOrSlug = parts[1];
|
|
3161
|
+
if (parts.length === 3 && parts[2] === "preview" && method === "GET") {
|
|
3162
|
+
const token = url.searchParams.get("token");
|
|
3163
|
+
if (!token) return jsonResponse({ error: "Missing preview token" }, 401);
|
|
3164
|
+
const verified = await this.engine.preview.verifyPreviewToken(token);
|
|
3165
|
+
if (!verified || verified.contentId !== idOrSlug || verified.collection !== collectionSlug) return jsonResponse({ error: "Invalid or expired preview token" }, 403);
|
|
3166
|
+
const item = await coll.findById(idOrSlug);
|
|
3167
|
+
if (!item) return jsonResponse({ error: "Content item not found" }, 404);
|
|
3168
|
+
return jsonResponse(this.filterRestrictedFields(item, collConfig, user));
|
|
3169
|
+
}
|
|
1719
3170
|
if (parts.length === 3 && parts[2] === "revisions" && method === "GET") return jsonResponse(await coll.getRevisions(idOrSlug));
|
|
1720
3171
|
if (parts.length === 4 && parts[2] === "restore" && method === "POST") {
|
|
3172
|
+
if (this.engine.auth.enabled && !this.engine.rbac.can(user, "edit_posts")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
|
|
1721
3173
|
const revisionId = parts[3];
|
|
1722
|
-
const restored = await coll.restoreRevision(idOrSlug, revisionId);
|
|
3174
|
+
const restored = await coll.restoreRevision(idOrSlug, revisionId, user?.id);
|
|
1723
3175
|
if (!restored) return jsonResponse({ error: "Revision or content item not found" }, 404);
|
|
1724
|
-
return jsonResponse(restored);
|
|
3176
|
+
return jsonResponse(this.filterRestrictedFields(restored, collConfig, user));
|
|
1725
3177
|
}
|
|
1726
3178
|
if (parts.length === 2 && method === "GET") {
|
|
1727
3179
|
const bySlug = url.searchParams.get("by") === "slug";
|
|
1728
|
-
|
|
1729
|
-
|
|
3180
|
+
const populate = url.searchParams.has("populate") ? url.searchParams.get("populate").split(",") : void 0;
|
|
3181
|
+
const select = url.searchParams.has("select") ? url.searchParams.get("select").split(",") : void 0;
|
|
3182
|
+
const locale = url.searchParams.get("locale") || void 0;
|
|
3183
|
+
let item = bySlug ? await coll.findBySlug(idOrSlug, {
|
|
3184
|
+
populate,
|
|
3185
|
+
select,
|
|
3186
|
+
locale
|
|
3187
|
+
}) : await coll.findById(idOrSlug, {
|
|
3188
|
+
populate,
|
|
3189
|
+
select,
|
|
3190
|
+
locale
|
|
3191
|
+
});
|
|
3192
|
+
if (!item && !bySlug) item = await coll.findBySlug(idOrSlug, {
|
|
3193
|
+
populate,
|
|
3194
|
+
select,
|
|
3195
|
+
locale
|
|
3196
|
+
});
|
|
1730
3197
|
if (!item) return jsonResponse({ error: "Content item not found" }, 404);
|
|
1731
|
-
return jsonResponse(item);
|
|
3198
|
+
return jsonResponse(this.filterRestrictedFields(item, collConfig, user));
|
|
1732
3199
|
}
|
|
1733
3200
|
if (parts.length === 2 && method === "PUT") {
|
|
3201
|
+
const existing = await coll.findById(idOrSlug);
|
|
3202
|
+
if (!existing) return jsonResponse({ error: "Content item not found" }, 404);
|
|
3203
|
+
if (this.engine.auth.enabled && !this.engine.rbac.can(user, "edit_posts", { contentItem: existing })) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
|
|
1734
3204
|
const body = await request.json();
|
|
1735
|
-
const updated = await coll.update(idOrSlug, body);
|
|
3205
|
+
const updated = await coll.update(idOrSlug, body, user?.id);
|
|
1736
3206
|
if (!updated) return jsonResponse({ error: "Content item not found" }, 404);
|
|
1737
|
-
return jsonResponse(updated);
|
|
3207
|
+
return jsonResponse(this.filterRestrictedFields(updated, collConfig, user));
|
|
1738
3208
|
}
|
|
1739
3209
|
if (parts.length === 2 && method === "DELETE") {
|
|
3210
|
+
const existing = await coll.findById(idOrSlug);
|
|
3211
|
+
if (!existing) return jsonResponse({ error: "Content item not found" }, 404);
|
|
3212
|
+
if (this.engine.auth.enabled && !this.engine.rbac.can(user, "delete_posts", { contentItem: existing })) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
|
|
1740
3213
|
if (!await coll.delete(idOrSlug)) return jsonResponse({ error: "Content item not found" }, 404);
|
|
1741
3214
|
return jsonResponse({
|
|
1742
3215
|
success: true,
|
|
@@ -1745,7 +3218,7 @@ var CMSRouter = class {
|
|
|
1745
3218
|
}
|
|
1746
3219
|
return jsonResponse({ error: "Method not allowed" }, 405);
|
|
1747
3220
|
}
|
|
1748
|
-
async handleTaxonomies(pathname, method, url, request) {
|
|
3221
|
+
async handleTaxonomies(pathname, method, url, request, user) {
|
|
1749
3222
|
const parts = pathname.replace(/^\/api\/taxonomies\/?/, "").split("/").filter(Boolean);
|
|
1750
3223
|
if (parts.length === 0 && method === "GET") return jsonResponse(this.engine.taxonomies.getTaxonomies());
|
|
1751
3224
|
const taxonomySlug = parts[0];
|
|
@@ -1754,6 +3227,7 @@ var CMSRouter = class {
|
|
|
1754
3227
|
return jsonResponse(await this.engine.taxonomies.getTerms(taxonomySlug));
|
|
1755
3228
|
}
|
|
1756
3229
|
if (parts.length === 2 && parts[1] === "terms" && method === "POST") {
|
|
3230
|
+
if (this.engine.auth.enabled && !this.engine.rbac.can(user, "manage_taxonomies")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
|
|
1757
3231
|
const body = await request.json();
|
|
1758
3232
|
return jsonResponse(await this.engine.taxonomies.createTerm(taxonomySlug, body), 201);
|
|
1759
3233
|
}
|
|
@@ -1763,12 +3237,14 @@ var CMSRouter = class {
|
|
|
1763
3237
|
return jsonResponse(term);
|
|
1764
3238
|
}
|
|
1765
3239
|
if (parts.length === 3 && parts[1] === "terms" && method === "PUT") {
|
|
3240
|
+
if (this.engine.auth.enabled && !this.engine.rbac.can(user, "manage_taxonomies")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
|
|
1766
3241
|
const body = await request.json();
|
|
1767
3242
|
const updated = await this.engine.taxonomies.updateTerm(parts[2], body);
|
|
1768
3243
|
if (!updated) return jsonResponse({ error: "Term not found" }, 404);
|
|
1769
3244
|
return jsonResponse(updated);
|
|
1770
3245
|
}
|
|
1771
3246
|
if (parts.length === 3 && parts[1] === "terms" && method === "DELETE") {
|
|
3247
|
+
if (this.engine.auth.enabled && !this.engine.rbac.can(user, "manage_taxonomies")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
|
|
1772
3248
|
if (!await this.engine.taxonomies.deleteTerm(parts[2])) return jsonResponse({ error: "Term not found" }, 404);
|
|
1773
3249
|
return jsonResponse({
|
|
1774
3250
|
success: true,
|
|
@@ -1777,7 +3253,7 @@ var CMSRouter = class {
|
|
|
1777
3253
|
}
|
|
1778
3254
|
return jsonResponse({ error: "Endpoint not found" }, 404);
|
|
1779
3255
|
}
|
|
1780
|
-
async handleMedia(pathname, method, url, request) {
|
|
3256
|
+
async handleMedia(pathname, method, url, request, user) {
|
|
1781
3257
|
const parts = pathname.replace(/^\/api\/media\/?/, "").split("/").filter(Boolean);
|
|
1782
3258
|
if (parts.length === 0 && method === "GET") {
|
|
1783
3259
|
const search = url.searchParams.get("search") || void 0;
|
|
@@ -1792,8 +3268,25 @@ var CMSRouter = class {
|
|
|
1792
3268
|
}));
|
|
1793
3269
|
}
|
|
1794
3270
|
if (parts.length === 0 && method === "POST") {
|
|
3271
|
+
if (this.engine.auth.enabled && !this.engine.rbac.can(user, "upload_files")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
|
|
3272
|
+
if ((request.headers.get("content-type") || request.headers.get("Content-Type") || "").includes("multipart/form-data")) {
|
|
3273
|
+
const formData = await request.formData();
|
|
3274
|
+
const file = formData.get("file");
|
|
3275
|
+
if (!file || typeof file.arrayBuffer !== "function") return jsonResponse({ error: "No file uploaded in 'file' field" }, 400);
|
|
3276
|
+
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
3277
|
+
const altText = formData.get("altText") || void 0;
|
|
3278
|
+
const caption = formData.get("caption") || void 0;
|
|
3279
|
+
return jsonResponse(await this.engine.media.upload({
|
|
3280
|
+
filename: file.name,
|
|
3281
|
+
mimeType: file.type || "application/octet-stream",
|
|
3282
|
+
buffer,
|
|
3283
|
+
sizeBytes: file.size,
|
|
3284
|
+
altText,
|
|
3285
|
+
caption
|
|
3286
|
+
}, user?.id), 201);
|
|
3287
|
+
}
|
|
1795
3288
|
const body = await request.json();
|
|
1796
|
-
return jsonResponse(await this.engine.media.upload(body), 201);
|
|
3289
|
+
return jsonResponse(await this.engine.media.upload(body, user?.id), 201);
|
|
1797
3290
|
}
|
|
1798
3291
|
const id = parts[0];
|
|
1799
3292
|
if (parts.length === 1 && method === "GET") {
|
|
@@ -1802,12 +3295,14 @@ var CMSRouter = class {
|
|
|
1802
3295
|
return jsonResponse(media);
|
|
1803
3296
|
}
|
|
1804
3297
|
if (parts.length === 1 && method === "PUT") {
|
|
3298
|
+
if (this.engine.auth.enabled && !this.engine.rbac.can(user, "upload_files")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
|
|
1805
3299
|
const body = await request.json();
|
|
1806
3300
|
const updated = await this.engine.media.updateMetadata(id, body);
|
|
1807
3301
|
if (!updated) return jsonResponse({ error: "Media not found" }, 404);
|
|
1808
3302
|
return jsonResponse(updated);
|
|
1809
3303
|
}
|
|
1810
3304
|
if (parts.length === 1 && method === "DELETE") {
|
|
3305
|
+
if (this.engine.auth.enabled && !this.engine.rbac.can(user, "delete_files")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
|
|
1811
3306
|
if (!await this.engine.media.delete(id)) return jsonResponse({ error: "Media not found" }, 404);
|
|
1812
3307
|
return jsonResponse({
|
|
1813
3308
|
success: true,
|
|
@@ -1816,7 +3311,7 @@ var CMSRouter = class {
|
|
|
1816
3311
|
}
|
|
1817
3312
|
return jsonResponse({ error: "Method not allowed" }, 405);
|
|
1818
3313
|
}
|
|
1819
|
-
async handleOptions(pathname, method, url, request) {
|
|
3314
|
+
async handleOptions(pathname, method, url, request, user) {
|
|
1820
3315
|
const key = pathname.replace(/^\/api\/options\/?/, "");
|
|
1821
3316
|
if (!key && method === "GET") {
|
|
1822
3317
|
const namespace = url.searchParams.get("namespace") || void 0;
|
|
@@ -1831,6 +3326,7 @@ var CMSRouter = class {
|
|
|
1831
3326
|
});
|
|
1832
3327
|
}
|
|
1833
3328
|
if (key && method === "PUT") {
|
|
3329
|
+
if (this.engine.auth.enabled && !this.engine.rbac.can(user, "manage_options")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
|
|
1834
3330
|
const body = await request.json();
|
|
1835
3331
|
return jsonResponse(await this.engine.options.set(key, body.value, {
|
|
1836
3332
|
autoload: body.autoload,
|
|
@@ -1838,6 +3334,7 @@ var CMSRouter = class {
|
|
|
1838
3334
|
}));
|
|
1839
3335
|
}
|
|
1840
3336
|
if (key && method === "DELETE") {
|
|
3337
|
+
if (this.engine.auth.enabled && !this.engine.rbac.can(user, "manage_options")) return jsonResponse({ error: "Forbidden" }, user ? 403 : 401);
|
|
1841
3338
|
if (!await this.engine.options.delete(key)) return jsonResponse({ error: "Option not found" }, 404);
|
|
1842
3339
|
return jsonResponse({
|
|
1843
3340
|
success: true,
|
|
@@ -1846,6 +3343,17 @@ var CMSRouter = class {
|
|
|
1846
3343
|
}
|
|
1847
3344
|
return jsonResponse({ error: "Method not allowed" }, 405);
|
|
1848
3345
|
}
|
|
3346
|
+
filterRestrictedFields(item, collConfig, user) {
|
|
3347
|
+
if (!collConfig.fields || !item.data) return item;
|
|
3348
|
+
const sanitizedData = { ...item.data };
|
|
3349
|
+
for (const field of collConfig.fields) if (field.readRoles && field.readRoles.length > 0) {
|
|
3350
|
+
if (!(user?.role === "admin" || user?.role && field.readRoles.includes(user.role))) delete sanitizedData[field.name];
|
|
3351
|
+
}
|
|
3352
|
+
return {
|
|
3353
|
+
...item,
|
|
3354
|
+
data: sanitizedData
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
1849
3357
|
};
|
|
1850
3358
|
function createCMSRouter(engine) {
|
|
1851
3359
|
return new CMSRouter(engine);
|
|
@@ -6654,12 +8162,14 @@ function getTransferService(engine, options) {
|
|
|
6654
8162
|
return service;
|
|
6655
8163
|
}
|
|
6656
8164
|
//#endregion
|
|
8165
|
+
exports.AuthService = AuthService;
|
|
6657
8166
|
exports.CMSClient = CMSClient;
|
|
6658
8167
|
exports.CMSEngine = CMSEngine;
|
|
6659
8168
|
exports.CMSRouter = CMSRouter;
|
|
6660
8169
|
exports.ContentLifecycle = ContentLifecycle;
|
|
6661
8170
|
exports.DEFAULT_COLLECTIONS = DEFAULT_COLLECTIONS;
|
|
6662
8171
|
exports.DEFAULT_ROLE_CAPABILITIES = DEFAULT_ROLE_CAPABILITIES;
|
|
8172
|
+
exports.DiskMediaStorageDriver = DiskMediaStorageDriver;
|
|
6663
8173
|
exports.EcommerceClient = EcommerceClient;
|
|
6664
8174
|
exports.EcommerceService = EcommerceService;
|
|
6665
8175
|
exports.HRMSClient = HRMSClient;
|
|
@@ -6670,14 +8180,18 @@ exports.HooksManager = HooksManager;
|
|
|
6670
8180
|
exports.MediaManager = MediaManager;
|
|
6671
8181
|
exports.MemoryStorageAdapter = MemoryStorageAdapter;
|
|
6672
8182
|
exports.OptionsManager = OptionsManager;
|
|
8183
|
+
exports.PersistenceStorageAdapter = PersistenceStorageAdapter;
|
|
8184
|
+
exports.PreviewManager = PreviewManager;
|
|
6673
8185
|
exports.RBACManager = RBACManager;
|
|
6674
8186
|
exports.RevisionManager = RevisionManager;
|
|
6675
8187
|
exports.TaxonomyManager = TaxonomyManager;
|
|
6676
8188
|
exports.TransferClient = TransferClient;
|
|
6677
8189
|
exports.TransferService = TransferService;
|
|
6678
8190
|
exports.VALID_STATUS_TRANSITIONS = VALID_STATUS_TRANSITIONS;
|
|
8191
|
+
exports.WebhookManager = WebhookManager;
|
|
6679
8192
|
exports.applyFieldTransform = applyFieldTransform;
|
|
6680
8193
|
exports.collection = collection;
|
|
8194
|
+
exports.computeHmacSignature = computeHmacSignature;
|
|
6681
8195
|
exports.createAttendanceCollection = createAttendanceCollection;
|
|
6682
8196
|
exports.createCMSEngine = createCMSEngine;
|
|
6683
8197
|
exports.createCMSRouter = createCMSRouter;
|
|
@@ -6704,22 +8218,28 @@ exports.getHRMSClient = getHRMSClient;
|
|
|
6704
8218
|
exports.getHRMSService = getHRMSService;
|
|
6705
8219
|
exports.getTransferClient = getTransferClient;
|
|
6706
8220
|
exports.getTransferService = getTransferService;
|
|
8221
|
+
exports.hashPassword = hashPassword;
|
|
6707
8222
|
exports.hrmsPlugin = hrmsPlugin;
|
|
6708
8223
|
exports.inspectSource = inspectSource;
|
|
6709
8224
|
exports.mapCmsItemForExport = mapCmsItemForExport;
|
|
6710
8225
|
exports.mapSourceRecord = mapSourceRecord;
|
|
8226
|
+
exports.mergeLocalizedInput = mergeLocalizedInput;
|
|
6711
8227
|
exports.normalizeConfig = normalizeConfig;
|
|
6712
8228
|
exports.parseCsvSource = parseCsvSource;
|
|
6713
8229
|
exports.parseExcelSource = parseExcelSource;
|
|
6714
8230
|
exports.parseJsonSource = parseJsonSource;
|
|
6715
8231
|
exports.parseSource = parseSource;
|
|
8232
|
+
exports.resolveLocalizedData = resolveLocalizedData;
|
|
6716
8233
|
exports.resolveUniqueSlug = resolveUniqueSlug;
|
|
6717
8234
|
exports.serializeCsv = serializeCsv;
|
|
6718
8235
|
exports.serializeExcel = serializeExcel;
|
|
6719
8236
|
exports.serializeJson = serializeJson;
|
|
6720
8237
|
exports.serializeSource = serializeSource;
|
|
8238
|
+
exports.signJwt = signJwt;
|
|
6721
8239
|
exports.slugify = slugify;
|
|
6722
8240
|
exports.summarizeCollection = summarizeCollection;
|
|
6723
8241
|
exports.transferPlugin = transferPlugin;
|
|
6724
8242
|
exports.validateAndNormalizeData = validateAndNormalizeData;
|
|
6725
8243
|
exports.validateTransferBatch = validateTransferBatch;
|
|
8244
|
+
exports.verifyJwt = verifyJwt;
|
|
8245
|
+
exports.verifyPassword = verifyPassword;
|