@dyrected/core 2.5.24 → 2.5.26
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/__tests__/app.test.d.ts +2 -0
- package/dist/__tests__/app.test.d.ts.map +1 -0
- package/dist/__tests__/app.test.js +27 -0
- package/dist/__tests__/app.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +34 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/deleteMany.test.d.ts +2 -0
- package/dist/__tests__/deleteMany.test.d.ts.map +1 -0
- package/dist/__tests__/deleteMany.test.js +75 -0
- package/dist/__tests__/deleteMany.test.js.map +1 -0
- package/dist/__tests__/depth.test.d.ts +2 -0
- package/dist/__tests__/depth.test.d.ts.map +1 -0
- package/dist/__tests__/depth.test.js +81 -0
- package/dist/__tests__/depth.test.js.map +1 -0
- package/dist/__tests__/dynamic-options.test.d.ts +2 -0
- package/dist/__tests__/dynamic-options.test.d.ts.map +1 -0
- package/dist/__tests__/dynamic-options.test.js +132 -0
- package/dist/__tests__/dynamic-options.test.js.map +1 -0
- package/dist/__tests__/field-inference.test-types.d.ts +24 -0
- package/dist/__tests__/field-inference.test-types.d.ts.map +1 -0
- package/dist/__tests__/field-inference.test-types.js +87 -0
- package/dist/__tests__/field-inference.test-types.js.map +1 -0
- package/dist/__tests__/hooks.test.d.ts +2 -0
- package/dist/__tests__/hooks.test.d.ts.map +1 -0
- package/dist/__tests__/hooks.test.js +320 -0
- package/dist/__tests__/hooks.test.js.map +1 -0
- package/dist/__tests__/mocks.d.ts +68 -0
- package/dist/__tests__/mocks.d.ts.map +1 -0
- package/dist/__tests__/mocks.js +151 -0
- package/dist/__tests__/mocks.js.map +1 -0
- package/dist/__tests__/router.test.d.ts +2 -0
- package/dist/__tests__/router.test.d.ts.map +1 -0
- package/dist/__tests__/router.test.js +48 -0
- package/dist/__tests__/router.test.js.map +1 -0
- package/dist/__tests__/where.test.d.ts +2 -0
- package/dist/__tests__/where.test.d.ts.map +1 -0
- package/dist/__tests__/where.test.js +97 -0
- package/dist/__tests__/where.test.js.map +1 -0
- package/dist/app-BOrsS7Tz.d.cts +1771 -0
- package/dist/app-BOrsS7Tz.d.ts +1771 -0
- package/dist/app-BibuoHQG.d.cts +1764 -0
- package/dist/app-BibuoHQG.d.ts +1764 -0
- package/dist/app-aW2FMuYM.d.cts +1759 -0
- package/dist/app-aW2FMuYM.d.ts +1759 -0
- package/dist/app.d.ts +21 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +56 -0
- package/dist/app.js.map +1 -0
- package/dist/auth/jexl.d.ts +10 -0
- package/dist/auth/jexl.d.ts.map +1 -0
- package/dist/auth/jexl.js +22 -0
- package/dist/auth/jexl.js.map +1 -0
- package/dist/auth/password.d.ts +10 -0
- package/dist/auth/password.d.ts.map +1 -0
- package/dist/auth/password.js +28 -0
- package/dist/auth/password.js.map +1 -0
- package/dist/auth/token.d.ts +20 -0
- package/dist/auth/token.d.ts.map +1 -0
- package/dist/auth/token.js +40 -0
- package/dist/auth/token.js.map +1 -0
- package/dist/chunk-4EDMZAM5.js +2692 -0
- package/dist/chunk-FDQYPPG3.js +2698 -0
- package/dist/chunk-NKDX67AW.js +2698 -0
- package/dist/chunk-SUGK7UYL.js +311 -0
- package/dist/chunk-ZFAOBRHT.js +2709 -0
- package/dist/controllers/auth.controller.d.ts +125 -0
- package/dist/controllers/auth.controller.d.ts.map +1 -0
- package/dist/controllers/auth.controller.js +323 -0
- package/dist/controllers/auth.controller.js.map +1 -0
- package/dist/controllers/collection.controller.d.ts +88 -0
- package/dist/controllers/collection.controller.d.ts.map +1 -0
- package/dist/controllers/collection.controller.js +554 -0
- package/dist/controllers/collection.controller.js.map +1 -0
- package/dist/controllers/global.controller.d.ts +17 -0
- package/dist/controllers/global.controller.d.ts.map +1 -0
- package/dist/controllers/global.controller.js +116 -0
- package/dist/controllers/global.controller.js.map +1 -0
- package/dist/controllers/media.controller.d.ts +36 -0
- package/dist/controllers/media.controller.d.ts.map +1 -0
- package/dist/controllers/media.controller.js +155 -0
- package/dist/controllers/media.controller.js.map +1 -0
- package/dist/controllers/preview.controller.d.ts +37 -0
- package/dist/controllers/preview.controller.d.ts.map +1 -0
- package/dist/controllers/preview.controller.js +48 -0
- package/dist/controllers/preview.controller.js.map +1 -0
- package/dist/index-Bp7PDOYG.d.cts +1750 -0
- package/dist/index-Bp7PDOYG.d.ts +1750 -0
- package/dist/index-DfAmTZXk.d.cts +1749 -0
- package/dist/index-DfAmTZXk.d.ts +1749 -0
- package/dist/index.cjs +19 -2324
- package/dist/index.d.cts +5 -6
- package/dist/index.d.ts +5 -6
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -5
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +18 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +45 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/router.d.ts +8 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +463 -0
- package/dist/router.js.map +1 -0
- package/dist/server.cjs +237 -45
- package/dist/server.d.cts +22 -4
- package/dist/server.d.ts +22 -4
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +2429 -8
- package/dist/server.js.map +1 -0
- package/dist/services/audit.service.d.ts +23 -0
- package/dist/services/audit.service.d.ts.map +1 -0
- package/dist/services/audit.service.js +28 -0
- package/dist/services/audit.service.js.map +1 -0
- package/dist/services/defaults.service.d.ts +8 -0
- package/dist/services/defaults.service.d.ts.map +1 -0
- package/dist/services/defaults.service.js +55 -0
- package/dist/services/defaults.service.js.map +1 -0
- package/dist/services/email.service.d.ts +33 -0
- package/dist/services/email.service.d.ts.map +1 -0
- package/dist/services/email.service.js +219 -0
- package/dist/services/email.service.js.map +1 -0
- package/dist/services/media.service.d.ts +20 -0
- package/dist/services/media.service.d.ts.map +1 -0
- package/dist/services/media.service.js +49 -0
- package/dist/services/media.service.js.map +1 -0
- package/dist/services/population.service.d.ts +20 -0
- package/dist/services/population.service.d.ts.map +1 -0
- package/dist/services/population.service.js +168 -0
- package/dist/services/population.service.js.map +1 -0
- package/dist/types/index.d.ts +1749 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/config.d.ts +8 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +153 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/hooks.d.ts +41 -0
- package/dist/utils/hooks.d.ts.map +1 -0
- package/dist/utils/hooks.js +169 -0
- package/dist/utils/hooks.js.map +1 -0
- package/dist/utils/openapi.d.ts +6 -0
- package/dist/utils/openapi.d.ts.map +1 -0
- package/dist/utils/openapi.js +331 -0
- package/dist/utils/openapi.js.map +1 -0
- package/dist/utils/parse-where.d.ts +63 -0
- package/dist/utils/parse-where.d.ts.map +1 -0
- package/dist/utils/parse-where.js +196 -0
- package/dist/utils/parse-where.js.map +1 -0
- package/dist/utils/readonly-db.d.ts +9 -0
- package/dist/utils/readonly-db.d.ts.map +1 -0
- package/dist/utils/readonly-db.js +21 -0
- package/dist/utils/readonly-db.js.map +1 -0
- package/dist/utils/setup-prompt.d.ts +11 -0
- package/dist/utils/setup-prompt.d.ts.map +1 -0
- package/dist/utils/setup-prompt.js +863 -0
- package/dist/utils/setup-prompt.js.map +1 -0
- package/dist/utils/swagger.d.ts +5 -0
- package/dist/utils/swagger.d.ts.map +1 -0
- package/dist/utils/swagger.js +51 -0
- package/dist/utils/swagger.js.map +1 -0
- package/dist/utils/where-sanitizer.d.ts +10 -0
- package/dist/utils/where-sanitizer.d.ts.map +1 -0
- package/dist/utils/where-sanitizer.js +63 -0
- package/dist/utils/where-sanitizer.js.map +1 -0
- package/dist/where-sanitizer-DQIWTQZW.js +50 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
6
|
var __export = (target, all) => {
|
|
9
7
|
for (var name in all)
|
|
@@ -17,20 +15,11 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
15
|
}
|
|
18
16
|
return to;
|
|
19
17
|
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
19
|
|
|
30
20
|
// src/index.ts
|
|
31
21
|
var index_exports = {};
|
|
32
22
|
__export(index_exports, {
|
|
33
|
-
createDyrectedApp: () => createDyrectedApp,
|
|
34
23
|
defineCollection: () => defineCollection,
|
|
35
24
|
defineConfig: () => defineConfig,
|
|
36
25
|
defineGlobal: () => defineGlobal,
|
|
@@ -1040,10 +1029,10 @@ function parseSqlWhere(where, getJsonField, placeholder = "?") {
|
|
|
1040
1029
|
function buildSingleOp(c, op, operand) {
|
|
1041
1030
|
switch (op) {
|
|
1042
1031
|
case "equals":
|
|
1043
|
-
params.push(operand);
|
|
1032
|
+
params.push(typeof operand === "boolean" ? String(operand) : operand);
|
|
1044
1033
|
return `${c} = ${next()}`;
|
|
1045
1034
|
case "not_equals":
|
|
1046
|
-
params.push(operand);
|
|
1035
|
+
params.push(typeof operand === "boolean" ? String(operand) : operand);
|
|
1047
1036
|
return `${c} != ${next()}`;
|
|
1048
1037
|
case "in": {
|
|
1049
1038
|
const vals = Array.isArray(operand) ? operand : [operand];
|
|
@@ -1195,7 +1184,7 @@ async function runCollectionHooks(hooks, args, options = {}) {
|
|
|
1195
1184
|
}
|
|
1196
1185
|
return currentPayload;
|
|
1197
1186
|
}
|
|
1198
|
-
async function executeFieldBeforeChange(fields, data, originalDoc, user) {
|
|
1187
|
+
async function executeFieldBeforeChange(fields, data, originalDoc, user, db) {
|
|
1199
1188
|
if (!data || typeof data !== "object") return data;
|
|
1200
1189
|
const result = { ...data };
|
|
1201
1190
|
for (const field of fields) {
|
|
@@ -1209,7 +1198,8 @@ async function executeFieldBeforeChange(fields, data, originalDoc, user) {
|
|
|
1209
1198
|
value: updatedValue,
|
|
1210
1199
|
originalDoc: originalDoc ?? void 0,
|
|
1211
1200
|
data: result,
|
|
1212
|
-
user
|
|
1201
|
+
user,
|
|
1202
|
+
db
|
|
1213
1203
|
});
|
|
1214
1204
|
}
|
|
1215
1205
|
result[field.name] = updatedValue;
|
|
@@ -1220,7 +1210,8 @@ async function executeFieldBeforeChange(fields, data, originalDoc, user) {
|
|
|
1220
1210
|
field.fields,
|
|
1221
1211
|
updatedValue,
|
|
1222
1212
|
origValue,
|
|
1223
|
-
user
|
|
1213
|
+
user,
|
|
1214
|
+
db
|
|
1224
1215
|
);
|
|
1225
1216
|
} else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
|
|
1226
1217
|
const arrayResult = [];
|
|
@@ -1228,7 +1219,7 @@ async function executeFieldBeforeChange(fields, data, originalDoc, user) {
|
|
|
1228
1219
|
const item = updatedValue[i];
|
|
1229
1220
|
const origItem = Array.isArray(origValue) ? origValue[i] : null;
|
|
1230
1221
|
arrayResult.push(
|
|
1231
|
-
await executeFieldBeforeChange(field.fields, item, origItem, user)
|
|
1222
|
+
await executeFieldBeforeChange(field.fields, item, origItem, user, db)
|
|
1232
1223
|
);
|
|
1233
1224
|
}
|
|
1234
1225
|
result[field.name] = arrayResult;
|
|
@@ -1246,7 +1237,8 @@ async function executeFieldBeforeChange(fields, data, originalDoc, user) {
|
|
|
1246
1237
|
blockConfig.fields,
|
|
1247
1238
|
blockData,
|
|
1248
1239
|
origBlock,
|
|
1249
|
-
user
|
|
1240
|
+
user,
|
|
1241
|
+
db
|
|
1250
1242
|
)
|
|
1251
1243
|
);
|
|
1252
1244
|
} else {
|
|
@@ -1259,7 +1251,7 @@ async function executeFieldBeforeChange(fields, data, originalDoc, user) {
|
|
|
1259
1251
|
}
|
|
1260
1252
|
return result;
|
|
1261
1253
|
}
|
|
1262
|
-
async function executeFieldAfterRead(fields, doc, user) {
|
|
1254
|
+
async function executeFieldAfterRead(fields, doc, user, db) {
|
|
1263
1255
|
if (!doc || typeof doc !== "object") return doc;
|
|
1264
1256
|
const result = { ...doc };
|
|
1265
1257
|
for (const field of fields) {
|
|
@@ -1271,7 +1263,8 @@ async function executeFieldAfterRead(fields, doc, user) {
|
|
|
1271
1263
|
updatedValue = await hook({
|
|
1272
1264
|
value: updatedValue,
|
|
1273
1265
|
doc: result,
|
|
1274
|
-
user
|
|
1266
|
+
user,
|
|
1267
|
+
db
|
|
1275
1268
|
});
|
|
1276
1269
|
}
|
|
1277
1270
|
result[field.name] = updatedValue;
|
|
@@ -1281,7 +1274,8 @@ async function executeFieldAfterRead(fields, doc, user) {
|
|
|
1281
1274
|
result[field.name] = await executeFieldAfterRead(
|
|
1282
1275
|
field.fields,
|
|
1283
1276
|
updatedValue,
|
|
1284
|
-
user
|
|
1277
|
+
user,
|
|
1278
|
+
db
|
|
1285
1279
|
);
|
|
1286
1280
|
} else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
|
|
1287
1281
|
const arrayResult = [];
|
|
@@ -1290,7 +1284,8 @@ async function executeFieldAfterRead(fields, doc, user) {
|
|
|
1290
1284
|
await executeFieldAfterRead(
|
|
1291
1285
|
field.fields,
|
|
1292
1286
|
item,
|
|
1293
|
-
user
|
|
1287
|
+
user,
|
|
1288
|
+
db
|
|
1294
1289
|
)
|
|
1295
1290
|
);
|
|
1296
1291
|
}
|
|
@@ -1307,7 +1302,8 @@ async function executeFieldAfterRead(fields, doc, user) {
|
|
|
1307
1302
|
await executeFieldAfterRead(
|
|
1308
1303
|
blockConfig.fields,
|
|
1309
1304
|
typedBlock,
|
|
1310
|
-
user
|
|
1305
|
+
user,
|
|
1306
|
+
db
|
|
1311
1307
|
)
|
|
1312
1308
|
);
|
|
1313
1309
|
} else {
|
|
@@ -1321,2306 +1317,6 @@ async function executeFieldAfterRead(fields, doc, user) {
|
|
|
1321
1317
|
return result;
|
|
1322
1318
|
}
|
|
1323
1319
|
|
|
1324
|
-
// src/app.ts
|
|
1325
|
-
var import_hono = require("hono");
|
|
1326
|
-
var import_cors = require("hono/cors");
|
|
1327
|
-
var import_request_id = require("hono/request-id");
|
|
1328
|
-
|
|
1329
|
-
// src/services/defaults.service.ts
|
|
1330
|
-
var DefaultsService = class {
|
|
1331
|
-
/**
|
|
1332
|
-
* Recursively apply default values to a data object based on field definitions.
|
|
1333
|
-
*/
|
|
1334
|
-
static apply(fields, data = {}) {
|
|
1335
|
-
const result = { ...data || {} };
|
|
1336
|
-
fields.forEach((field) => {
|
|
1337
|
-
if (field.type === "join") return;
|
|
1338
|
-
if (field.type === "row" && field.fields) {
|
|
1339
|
-
Object.assign(result, this.apply(field.fields, data));
|
|
1340
|
-
return;
|
|
1341
|
-
}
|
|
1342
|
-
if (!field.name) return;
|
|
1343
|
-
let value = result[field.name];
|
|
1344
|
-
if ((value === void 0 || value === null) && field.renameTo) {
|
|
1345
|
-
const legacyValue = result[field.renameTo];
|
|
1346
|
-
if (legacyValue !== void 0 && legacyValue !== null) {
|
|
1347
|
-
value = legacyValue;
|
|
1348
|
-
result[field.name] = legacyValue;
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
if (value === void 0 || value === null) {
|
|
1352
|
-
if (field.defaultValue !== void 0) {
|
|
1353
|
-
result[field.name] = field.defaultValue;
|
|
1354
|
-
} else {
|
|
1355
|
-
if (field.type === "boolean") result[field.name] = false;
|
|
1356
|
-
else if (field.type === "array") result[field.name] = [];
|
|
1357
|
-
else if (field.type === "multiSelect") result[field.name] = [];
|
|
1358
|
-
else if (field.type === "object") {
|
|
1359
|
-
result[field.name] = this.apply(field.fields || [], {});
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
} else if (field.type === "object" && field.fields) {
|
|
1363
|
-
result[field.name] = this.apply(field.fields, value);
|
|
1364
|
-
} else if (field.type === "array" && field.fields && Array.isArray(value)) {
|
|
1365
|
-
result[field.name] = value.map((item) => this.apply(field.fields, item));
|
|
1366
|
-
}
|
|
1367
|
-
});
|
|
1368
|
-
return result;
|
|
1369
|
-
}
|
|
1370
|
-
};
|
|
1371
|
-
|
|
1372
|
-
// src/services/population.service.ts
|
|
1373
|
-
var PopulationService = class {
|
|
1374
|
-
db;
|
|
1375
|
-
collections;
|
|
1376
|
-
constructor(db, collections) {
|
|
1377
|
-
this.db = db;
|
|
1378
|
-
this.collections = collections;
|
|
1379
|
-
}
|
|
1380
|
-
/**
|
|
1381
|
-
* Recursively populate relationship fields in a document or array of documents.
|
|
1382
|
-
*/
|
|
1383
|
-
async populate(args) {
|
|
1384
|
-
const { data, fields, currentDepth = 0, maxDepth = 10 } = args;
|
|
1385
|
-
if (currentDepth >= maxDepth || !data) {
|
|
1386
|
-
return data;
|
|
1387
|
-
}
|
|
1388
|
-
if (Array.isArray(data)) {
|
|
1389
|
-
return Promise.all(data.map((item) => this.populate({ ...args, data: item })));
|
|
1390
|
-
}
|
|
1391
|
-
const populatedDoc = { ...data };
|
|
1392
|
-
for (const field of fields) {
|
|
1393
|
-
if (field.type === "join") continue;
|
|
1394
|
-
if (field.type === "row" && field.fields) {
|
|
1395
|
-
const rowPopulated = await this.populate({ data, fields: field.fields, currentDepth, maxDepth });
|
|
1396
|
-
Object.assign(populatedDoc, rowPopulated);
|
|
1397
|
-
continue;
|
|
1398
|
-
}
|
|
1399
|
-
if (!field.name) continue;
|
|
1400
|
-
const value = populatedDoc[field.name];
|
|
1401
|
-
if (field.type === "relationship" && field.relationTo && value) {
|
|
1402
|
-
const relatedCollection = this.collections.find((c) => c.slug === field.relationTo);
|
|
1403
|
-
if (!relatedCollection) continue;
|
|
1404
|
-
if (Array.isArray(value)) {
|
|
1405
|
-
populatedDoc[field.name] = await Promise.all(
|
|
1406
|
-
value.map(async (id) => {
|
|
1407
|
-
if (!id) return id;
|
|
1408
|
-
let doc = id;
|
|
1409
|
-
if (typeof id === "string") {
|
|
1410
|
-
doc = await this.db.findOne({ collection: field.relationTo, id });
|
|
1411
|
-
}
|
|
1412
|
-
if (!doc || typeof doc !== "object") return id;
|
|
1413
|
-
const docWithDefaults = DefaultsService.apply(relatedCollection.fields, doc);
|
|
1414
|
-
return this.populate({
|
|
1415
|
-
data: docWithDefaults,
|
|
1416
|
-
fields: relatedCollection.fields,
|
|
1417
|
-
currentDepth: currentDepth + 1,
|
|
1418
|
-
maxDepth
|
|
1419
|
-
});
|
|
1420
|
-
})
|
|
1421
|
-
);
|
|
1422
|
-
} else if (value) {
|
|
1423
|
-
let doc = value;
|
|
1424
|
-
if (typeof value === "string") {
|
|
1425
|
-
doc = await this.db.findOne({ collection: field.relationTo, id: value });
|
|
1426
|
-
}
|
|
1427
|
-
if (doc && typeof doc === "object") {
|
|
1428
|
-
const docWithDefaults = DefaultsService.apply(relatedCollection.fields, doc);
|
|
1429
|
-
populatedDoc[field.name] = await this.populate({
|
|
1430
|
-
data: docWithDefaults,
|
|
1431
|
-
fields: relatedCollection.fields,
|
|
1432
|
-
currentDepth: currentDepth + 1,
|
|
1433
|
-
maxDepth
|
|
1434
|
-
});
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
if (field.type === "url" && value && typeof value === "object" && value.type === "internal" && value.relationTo && value.value) {
|
|
1439
|
-
const relatedCollection = this.collections.find((c) => c.slug === value.relationTo);
|
|
1440
|
-
if (relatedCollection) {
|
|
1441
|
-
const doc = await this.db.findOne({ collection: value.relationTo, id: value.value });
|
|
1442
|
-
if (doc && typeof doc === "object") {
|
|
1443
|
-
const docWithDefaults = DefaultsService.apply(relatedCollection.fields, doc);
|
|
1444
|
-
const populatedDocValue = await this.populate({
|
|
1445
|
-
data: docWithDefaults,
|
|
1446
|
-
fields: relatedCollection.fields,
|
|
1447
|
-
currentDepth: currentDepth + 1,
|
|
1448
|
-
maxDepth
|
|
1449
|
-
});
|
|
1450
|
-
const identifier = docWithDefaults.slug || docWithDefaults.id;
|
|
1451
|
-
const resolvedUrl = `/collections/${value.relationTo}/${identifier}`;
|
|
1452
|
-
populatedDoc[field.name] = {
|
|
1453
|
-
...value,
|
|
1454
|
-
url: resolvedUrl,
|
|
1455
|
-
doc: populatedDocValue
|
|
1456
|
-
};
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
if ((field.type === "array" || field.type === "object") && field.fields && value) {
|
|
1461
|
-
populatedDoc[field.name] = await this.populate({
|
|
1462
|
-
data: value,
|
|
1463
|
-
fields: field.fields,
|
|
1464
|
-
currentDepth,
|
|
1465
|
-
// Nested fields don't consume depth, only relationships do
|
|
1466
|
-
maxDepth
|
|
1467
|
-
});
|
|
1468
|
-
}
|
|
1469
|
-
if (field.type === "blocks" && field.blocks && Array.isArray(value)) {
|
|
1470
|
-
populatedDoc[field.name] = await Promise.all(
|
|
1471
|
-
value.map(async (blockData) => {
|
|
1472
|
-
const blockConfig = field.blocks.find((b) => b.slug === blockData.blockType);
|
|
1473
|
-
if (!blockConfig) return blockData;
|
|
1474
|
-
return this.populate({
|
|
1475
|
-
data: blockData,
|
|
1476
|
-
fields: blockConfig.fields,
|
|
1477
|
-
currentDepth,
|
|
1478
|
-
maxDepth
|
|
1479
|
-
});
|
|
1480
|
-
})
|
|
1481
|
-
);
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
return populatedDoc;
|
|
1485
|
-
}
|
|
1486
|
-
/**
|
|
1487
|
-
* Helper to populate a PaginatedResult
|
|
1488
|
-
*/
|
|
1489
|
-
async populateResult(result, fields, maxDepth) {
|
|
1490
|
-
if (maxDepth <= 0) return result;
|
|
1491
|
-
const populatedDocs = await this.populate({
|
|
1492
|
-
data: result.docs,
|
|
1493
|
-
fields,
|
|
1494
|
-
currentDepth: 0,
|
|
1495
|
-
maxDepth
|
|
1496
|
-
});
|
|
1497
|
-
return {
|
|
1498
|
-
...result,
|
|
1499
|
-
docs: populatedDocs
|
|
1500
|
-
};
|
|
1501
|
-
}
|
|
1502
|
-
};
|
|
1503
|
-
|
|
1504
|
-
// src/services/audit.service.ts
|
|
1505
|
-
var AuditService = class {
|
|
1506
|
-
/**
|
|
1507
|
-
* Writes a single entry to the __audit collection.
|
|
1508
|
-
* Called without await — runs asynchronously and never blocks the primary operation.
|
|
1509
|
-
*/
|
|
1510
|
-
static async log(db, args) {
|
|
1511
|
-
try {
|
|
1512
|
-
await db.create({
|
|
1513
|
-
collection: "__audit",
|
|
1514
|
-
data: {
|
|
1515
|
-
collection: args.collection,
|
|
1516
|
-
documentId: args.documentId ?? null,
|
|
1517
|
-
operation: args.operation,
|
|
1518
|
-
user: args.user?.id ?? null,
|
|
1519
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1520
|
-
changes: JSON.stringify({
|
|
1521
|
-
before: args.before ?? null,
|
|
1522
|
-
after: args.after ?? null
|
|
1523
|
-
})
|
|
1524
|
-
}
|
|
1525
|
-
});
|
|
1526
|
-
} catch (err) {
|
|
1527
|
-
console.error("[dyrected/audit] Failed to write audit log:", err);
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
};
|
|
1531
|
-
|
|
1532
|
-
// src/auth/password.ts
|
|
1533
|
-
var import_node_util = require("util");
|
|
1534
|
-
var import_node_crypto = require("crypto");
|
|
1535
|
-
var scryptAsync = (0, import_node_util.promisify)(import_node_crypto.scrypt);
|
|
1536
|
-
var SALT_LEN = 16;
|
|
1537
|
-
var KEY_LEN = 64;
|
|
1538
|
-
async function hashPassword(plain) {
|
|
1539
|
-
const salt = (0, import_node_crypto.randomBytes)(SALT_LEN).toString("hex");
|
|
1540
|
-
const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
|
|
1541
|
-
return `${salt}:${derivedKey.toString("hex")}`;
|
|
1542
|
-
}
|
|
1543
|
-
async function verifyPassword(plain, stored) {
|
|
1544
|
-
const [salt, storedHash] = stored.split(":");
|
|
1545
|
-
if (!salt || !storedHash) return false;
|
|
1546
|
-
const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
|
|
1547
|
-
const storedBuffer = Buffer.from(storedHash, "hex");
|
|
1548
|
-
if (derivedKey.length !== storedBuffer.length) return false;
|
|
1549
|
-
return (0, import_node_crypto.timingSafeEqual)(derivedKey, storedBuffer);
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
// src/controllers/collection.controller.ts
|
|
1553
|
-
var CollectionController = class {
|
|
1554
|
-
collection;
|
|
1555
|
-
constructor(collection) {
|
|
1556
|
-
this.collection = collection;
|
|
1557
|
-
}
|
|
1558
|
-
async find(c) {
|
|
1559
|
-
const config = c.get("config");
|
|
1560
|
-
const db = config.db;
|
|
1561
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
1562
|
-
const limit = Number(c.req.query("limit")) || 10;
|
|
1563
|
-
const page = Number(c.req.query("page")) || 1;
|
|
1564
|
-
const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 2;
|
|
1565
|
-
const sort = c.req.query("sort") || void 0;
|
|
1566
|
-
const user = c.get("user");
|
|
1567
|
-
let where = void 0;
|
|
1568
|
-
const whereRaw = c.req.query("where");
|
|
1569
|
-
if (whereRaw) {
|
|
1570
|
-
try {
|
|
1571
|
-
where = JSON.parse(decodeURIComponent(whereRaw));
|
|
1572
|
-
} catch {
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
const beforeReadResult = await runCollectionHooks(this.collection.hooks?.beforeRead, {
|
|
1576
|
-
req: c.req,
|
|
1577
|
-
query: where,
|
|
1578
|
-
user
|
|
1579
|
-
});
|
|
1580
|
-
if (beforeReadResult !== void 0) {
|
|
1581
|
-
where = beforeReadResult;
|
|
1582
|
-
}
|
|
1583
|
-
let result = await db.find({
|
|
1584
|
-
collection: this.collection.slug,
|
|
1585
|
-
limit,
|
|
1586
|
-
page,
|
|
1587
|
-
sort,
|
|
1588
|
-
where
|
|
1589
|
-
});
|
|
1590
|
-
if (result.total === 0 && this.collection.initialData && !where && page === 1) {
|
|
1591
|
-
console.log(`[dyrected/core] Auto-seeding collection "${this.collection.slug}" from config.initialData`);
|
|
1592
|
-
for (const data of this.collection.initialData) {
|
|
1593
|
-
await db.create({ collection: this.collection.slug, data });
|
|
1594
|
-
}
|
|
1595
|
-
result = await db.find({
|
|
1596
|
-
collection: this.collection.slug,
|
|
1597
|
-
limit,
|
|
1598
|
-
page,
|
|
1599
|
-
sort,
|
|
1600
|
-
where
|
|
1601
|
-
});
|
|
1602
|
-
}
|
|
1603
|
-
result.docs = result.docs.map((doc) => DefaultsService.apply(this.collection.fields, doc));
|
|
1604
|
-
const processedDocs = [];
|
|
1605
|
-
for (const doc of result.docs) {
|
|
1606
|
-
const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
1607
|
-
doc,
|
|
1608
|
-
req: c.req,
|
|
1609
|
-
user
|
|
1610
|
-
});
|
|
1611
|
-
const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user);
|
|
1612
|
-
processedDocs.push(docWithFieldHooks);
|
|
1613
|
-
}
|
|
1614
|
-
result.docs = processedDocs;
|
|
1615
|
-
if (depth > 0) {
|
|
1616
|
-
const populationService = new PopulationService(db, config.collections);
|
|
1617
|
-
result = await populationService.populateResult(result, this.collection.fields, depth);
|
|
1618
|
-
}
|
|
1619
|
-
return c.json(result);
|
|
1620
|
-
}
|
|
1621
|
-
async findOne(c) {
|
|
1622
|
-
const config = c.get("config");
|
|
1623
|
-
const db = config.db;
|
|
1624
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
1625
|
-
const id = c.req.param("id");
|
|
1626
|
-
const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 10;
|
|
1627
|
-
const user = c.get("user");
|
|
1628
|
-
if (!id) return c.json({ message: "Missing ID" }, 400);
|
|
1629
|
-
const doc = await db.findOne({ collection: this.collection.slug, id });
|
|
1630
|
-
if (!doc) return c.json({ message: "Not Found" }, 404);
|
|
1631
|
-
const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
|
|
1632
|
-
const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
1633
|
-
doc: docWithDefaults,
|
|
1634
|
-
req: c.req,
|
|
1635
|
-
user
|
|
1636
|
-
});
|
|
1637
|
-
const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user);
|
|
1638
|
-
if (depth > 0 && docWithFieldHooks) {
|
|
1639
|
-
const populationService = new PopulationService(db, config.collections);
|
|
1640
|
-
const populatedDoc = await populationService.populate({
|
|
1641
|
-
data: docWithFieldHooks,
|
|
1642
|
-
fields: this.collection.fields,
|
|
1643
|
-
currentDepth: 0,
|
|
1644
|
-
maxDepth: depth
|
|
1645
|
-
});
|
|
1646
|
-
return c.json(populatedDoc);
|
|
1647
|
-
}
|
|
1648
|
-
return c.json(docWithFieldHooks);
|
|
1649
|
-
}
|
|
1650
|
-
async create(c) {
|
|
1651
|
-
const config = c.get("config");
|
|
1652
|
-
const db = config.db;
|
|
1653
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
1654
|
-
const contentType = c.req.header("Content-Type") || "";
|
|
1655
|
-
if (contentType.toLowerCase().includes("multipart/form-data")) {
|
|
1656
|
-
return this.upload(c);
|
|
1657
|
-
}
|
|
1658
|
-
const body = await c.req.json();
|
|
1659
|
-
const user = c.get("user");
|
|
1660
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1661
|
-
let data = {
|
|
1662
|
-
...body,
|
|
1663
|
-
createdAt: now,
|
|
1664
|
-
updatedAt: now,
|
|
1665
|
-
createdBy: user?.sub ?? null,
|
|
1666
|
-
updatedBy: user?.sub ?? null
|
|
1667
|
-
};
|
|
1668
|
-
if (this.collection.auth && data.password) {
|
|
1669
|
-
data.password = await hashPassword(data.password);
|
|
1670
|
-
}
|
|
1671
|
-
data = await executeFieldBeforeChange(this.collection.fields, data, null, user);
|
|
1672
|
-
data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
|
|
1673
|
-
data,
|
|
1674
|
-
req: c.req,
|
|
1675
|
-
user,
|
|
1676
|
-
operation: "create"
|
|
1677
|
-
});
|
|
1678
|
-
const doc = await db.create({ collection: this.collection.slug, data });
|
|
1679
|
-
if (this.collection.audit && db) {
|
|
1680
|
-
AuditService.log(db, {
|
|
1681
|
-
operation: "create",
|
|
1682
|
-
collection: this.collection.slug,
|
|
1683
|
-
documentId: doc.id,
|
|
1684
|
-
user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
|
|
1685
|
-
before: null,
|
|
1686
|
-
after: doc
|
|
1687
|
-
});
|
|
1688
|
-
}
|
|
1689
|
-
await runCollectionHooks(this.collection.hooks?.afterChange, {
|
|
1690
|
-
doc,
|
|
1691
|
-
user,
|
|
1692
|
-
req: c.req,
|
|
1693
|
-
operation: "create"
|
|
1694
|
-
}, { isolated: true });
|
|
1695
|
-
const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
1696
|
-
doc,
|
|
1697
|
-
req: c.req,
|
|
1698
|
-
user
|
|
1699
|
-
});
|
|
1700
|
-
const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user);
|
|
1701
|
-
return c.json(finalDoc, 201);
|
|
1702
|
-
}
|
|
1703
|
-
async upload(c) {
|
|
1704
|
-
const config = c.get("config");
|
|
1705
|
-
const storage = config.storage;
|
|
1706
|
-
if (!storage) return c.json({ message: "Storage not configured" }, 500);
|
|
1707
|
-
const formData = await c.req.formData();
|
|
1708
|
-
const file = formData.get("file");
|
|
1709
|
-
if (!file) return c.json({ message: "No file uploaded" }, 400);
|
|
1710
|
-
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
1711
|
-
const siteId = c.get("siteId");
|
|
1712
|
-
const workspaceId = c.get("workspaceId");
|
|
1713
|
-
const prefix = workspaceId ? `${workspaceId}/${siteId}` : siteId;
|
|
1714
|
-
const fileData = await storage.upload({
|
|
1715
|
-
filename: file.name,
|
|
1716
|
-
buffer,
|
|
1717
|
-
mimeType: file.type,
|
|
1718
|
-
prefix
|
|
1719
|
-
});
|
|
1720
|
-
const otherData = {};
|
|
1721
|
-
formData.forEach((value, key) => {
|
|
1722
|
-
if (key !== "file" && typeof value === "string") {
|
|
1723
|
-
otherData[key] = value;
|
|
1724
|
-
}
|
|
1725
|
-
});
|
|
1726
|
-
const user = c.get("user");
|
|
1727
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1728
|
-
let data = {
|
|
1729
|
-
...otherData,
|
|
1730
|
-
...fileData,
|
|
1731
|
-
createdAt: now,
|
|
1732
|
-
updatedAt: now,
|
|
1733
|
-
createdBy: user?.sub ?? null,
|
|
1734
|
-
updatedBy: user?.sub ?? null
|
|
1735
|
-
};
|
|
1736
|
-
data = await executeFieldBeforeChange(this.collection.fields, data, null, user);
|
|
1737
|
-
data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
|
|
1738
|
-
data,
|
|
1739
|
-
req: c.req,
|
|
1740
|
-
user,
|
|
1741
|
-
operation: "create"
|
|
1742
|
-
});
|
|
1743
|
-
const doc = await config.db.create({
|
|
1744
|
-
collection: this.collection.slug,
|
|
1745
|
-
data
|
|
1746
|
-
});
|
|
1747
|
-
await runCollectionHooks(this.collection.hooks?.afterChange, {
|
|
1748
|
-
doc,
|
|
1749
|
-
user,
|
|
1750
|
-
req: c.req,
|
|
1751
|
-
operation: "create"
|
|
1752
|
-
}, { isolated: true });
|
|
1753
|
-
const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
1754
|
-
doc,
|
|
1755
|
-
req: c.req,
|
|
1756
|
-
user
|
|
1757
|
-
});
|
|
1758
|
-
const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user);
|
|
1759
|
-
return c.json(finalDoc, 201);
|
|
1760
|
-
}
|
|
1761
|
-
async update(c) {
|
|
1762
|
-
const config = c.get("config");
|
|
1763
|
-
const db = config.db;
|
|
1764
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
1765
|
-
const id = c.req.param("id");
|
|
1766
|
-
if (!id) return c.json({ message: "Missing ID" }, 400);
|
|
1767
|
-
const body = await c.req.json();
|
|
1768
|
-
const user = c.get("user");
|
|
1769
|
-
let data = { ...body };
|
|
1770
|
-
if (this.collection.auth) {
|
|
1771
|
-
delete data.password;
|
|
1772
|
-
delete data.oldPassword;
|
|
1773
|
-
delete data.confirmPassword;
|
|
1774
|
-
}
|
|
1775
|
-
Object.assign(data, {
|
|
1776
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1777
|
-
updatedBy: user?.sub ?? null
|
|
1778
|
-
});
|
|
1779
|
-
const originalDoc = await db.findOne({ collection: this.collection.slug, id });
|
|
1780
|
-
if (!originalDoc) return c.json({ message: "Not Found" }, 404);
|
|
1781
|
-
let before = null;
|
|
1782
|
-
if (this.collection.audit) {
|
|
1783
|
-
before = originalDoc;
|
|
1784
|
-
}
|
|
1785
|
-
data = await executeFieldBeforeChange(this.collection.fields, data, originalDoc, user);
|
|
1786
|
-
data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
|
|
1787
|
-
data,
|
|
1788
|
-
doc: originalDoc,
|
|
1789
|
-
req: c.req,
|
|
1790
|
-
user,
|
|
1791
|
-
operation: "update"
|
|
1792
|
-
});
|
|
1793
|
-
const doc = await db.update({ collection: this.collection.slug, id, data });
|
|
1794
|
-
if (this.collection.audit && db) {
|
|
1795
|
-
AuditService.log(db, {
|
|
1796
|
-
operation: "update",
|
|
1797
|
-
collection: this.collection.slug,
|
|
1798
|
-
documentId: id,
|
|
1799
|
-
user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
|
|
1800
|
-
before,
|
|
1801
|
-
after: doc
|
|
1802
|
-
});
|
|
1803
|
-
}
|
|
1804
|
-
await runCollectionHooks(this.collection.hooks?.afterChange, {
|
|
1805
|
-
doc,
|
|
1806
|
-
previousDoc: originalDoc,
|
|
1807
|
-
user,
|
|
1808
|
-
req: c.req,
|
|
1809
|
-
operation: "update"
|
|
1810
|
-
}, { isolated: true });
|
|
1811
|
-
const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
1812
|
-
doc,
|
|
1813
|
-
req: c.req,
|
|
1814
|
-
user
|
|
1815
|
-
});
|
|
1816
|
-
const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user);
|
|
1817
|
-
return c.json(finalDoc);
|
|
1818
|
-
}
|
|
1819
|
-
/**
|
|
1820
|
-
* POST /api/collections/:slug/:id/change-password
|
|
1821
|
-
*
|
|
1822
|
-
* Dedicated endpoint for password changes. Requires the caller to supply:
|
|
1823
|
-
* { oldPassword, newPassword, confirmPassword }
|
|
1824
|
-
*
|
|
1825
|
-
* Rules:
|
|
1826
|
-
* - Only the account owner or an admin may change the password.
|
|
1827
|
-
* - Non-admin callers MUST provide a valid oldPassword.
|
|
1828
|
-
* - newPassword and confirmPassword must match.
|
|
1829
|
-
*/
|
|
1830
|
-
async changePassword(c) {
|
|
1831
|
-
const config = c.get("config");
|
|
1832
|
-
const db = config.db;
|
|
1833
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
1834
|
-
if (!this.collection.auth) {
|
|
1835
|
-
return c.json({ message: "This collection does not support authentication" }, 400);
|
|
1836
|
-
}
|
|
1837
|
-
const id = c.req.param("id");
|
|
1838
|
-
if (!id) return c.json({ message: "Missing ID" }, 400);
|
|
1839
|
-
const user = c.get("user");
|
|
1840
|
-
if (!user) return c.json({ message: "Authentication required" }, 401);
|
|
1841
|
-
const body = await c.req.json().catch(() => null);
|
|
1842
|
-
const { oldPassword, newPassword, confirmPassword } = body ?? {};
|
|
1843
|
-
if (!newPassword) {
|
|
1844
|
-
return c.json({ message: "newPassword is required" }, 400);
|
|
1845
|
-
}
|
|
1846
|
-
if (newPassword !== confirmPassword) {
|
|
1847
|
-
return c.json({ message: "Passwords do not match" }, 400);
|
|
1848
|
-
}
|
|
1849
|
-
if (newPassword.length < 8) {
|
|
1850
|
-
return c.json({ message: "Password must be at least 8 characters" }, 400);
|
|
1851
|
-
}
|
|
1852
|
-
const isAdmin = Array.isArray(user.roles) && user.roles.includes("admin");
|
|
1853
|
-
const isSelf = user.sub === id;
|
|
1854
|
-
if (!isAdmin && !isSelf) {
|
|
1855
|
-
return c.json({ message: "You are not authorised to change this password" }, 403);
|
|
1856
|
-
}
|
|
1857
|
-
if (!isAdmin) {
|
|
1858
|
-
if (!oldPassword) {
|
|
1859
|
-
return c.json({ message: "Current password is required" }, 400);
|
|
1860
|
-
}
|
|
1861
|
-
const existing = await db.findOne({ collection: this.collection.slug, id });
|
|
1862
|
-
if (!existing) return c.json({ message: "User not found" }, 404);
|
|
1863
|
-
const valid = await verifyPassword(oldPassword, existing.password);
|
|
1864
|
-
if (!valid) {
|
|
1865
|
-
return c.json({ message: "Invalid current password" }, 400);
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
const hashed = await hashPassword(newPassword);
|
|
1869
|
-
const doc = await db.update({
|
|
1870
|
-
collection: this.collection.slug,
|
|
1871
|
-
id,
|
|
1872
|
-
data: {
|
|
1873
|
-
password: hashed,
|
|
1874
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1875
|
-
updatedBy: user.sub
|
|
1876
|
-
}
|
|
1877
|
-
});
|
|
1878
|
-
if (this.collection.audit) {
|
|
1879
|
-
AuditService.log(db, {
|
|
1880
|
-
operation: "update",
|
|
1881
|
-
collection: this.collection.slug,
|
|
1882
|
-
documentId: id,
|
|
1883
|
-
user: { id: user.sub, collection: user.collection, email: user.email },
|
|
1884
|
-
before: null,
|
|
1885
|
-
after: { id }
|
|
1886
|
-
});
|
|
1887
|
-
}
|
|
1888
|
-
return c.json({ success: true, message: "Password updated successfully" });
|
|
1889
|
-
}
|
|
1890
|
-
async delete(c) {
|
|
1891
|
-
const config = c.get("config");
|
|
1892
|
-
const db = config.db;
|
|
1893
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
1894
|
-
const id = c.req.param("id");
|
|
1895
|
-
if (!id) return c.json({ message: "Missing ID" }, 400);
|
|
1896
|
-
const user = c.get("user");
|
|
1897
|
-
const doc = await db.findOne({ collection: this.collection.slug, id });
|
|
1898
|
-
if (!doc) return c.json({ message: "Not Found" }, 404);
|
|
1899
|
-
let before = null;
|
|
1900
|
-
if (this.collection.audit) {
|
|
1901
|
-
before = doc;
|
|
1902
|
-
}
|
|
1903
|
-
await runCollectionHooks(this.collection.hooks?.beforeDelete, {
|
|
1904
|
-
id,
|
|
1905
|
-
doc,
|
|
1906
|
-
user,
|
|
1907
|
-
req: c.req
|
|
1908
|
-
});
|
|
1909
|
-
await db.delete({ collection: this.collection.slug, id });
|
|
1910
|
-
if (this.collection.audit && db) {
|
|
1911
|
-
AuditService.log(db, {
|
|
1912
|
-
operation: "delete",
|
|
1913
|
-
collection: this.collection.slug,
|
|
1914
|
-
documentId: id,
|
|
1915
|
-
user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
|
|
1916
|
-
before,
|
|
1917
|
-
after: null
|
|
1918
|
-
});
|
|
1919
|
-
}
|
|
1920
|
-
await runCollectionHooks(this.collection.hooks?.afterDelete, {
|
|
1921
|
-
id,
|
|
1922
|
-
doc,
|
|
1923
|
-
user,
|
|
1924
|
-
req: c.req
|
|
1925
|
-
}, { isolated: true });
|
|
1926
|
-
return c.json({ message: "Deleted" });
|
|
1927
|
-
}
|
|
1928
|
-
async deleteMany(c) {
|
|
1929
|
-
const config = c.get("config");
|
|
1930
|
-
const db = config.db;
|
|
1931
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
1932
|
-
const user = c.get("user");
|
|
1933
|
-
let ids = [];
|
|
1934
|
-
try {
|
|
1935
|
-
const body = await c.req.json().catch(() => null);
|
|
1936
|
-
if (body?.ids && Array.isArray(body.ids)) {
|
|
1937
|
-
ids = body.ids;
|
|
1938
|
-
}
|
|
1939
|
-
} catch {
|
|
1940
|
-
}
|
|
1941
|
-
if (!ids.length) {
|
|
1942
|
-
const raw = c.req.queries("ids") ?? c.req.queries("ids[]") ?? [];
|
|
1943
|
-
ids = raw.filter(Boolean);
|
|
1944
|
-
}
|
|
1945
|
-
if (!ids.length) return c.json({ message: "No IDs provided" }, 400);
|
|
1946
|
-
const deleted = [];
|
|
1947
|
-
const failed = [];
|
|
1948
|
-
for (const id of ids) {
|
|
1949
|
-
try {
|
|
1950
|
-
const doc = await db.findOne({ collection: this.collection.slug, id });
|
|
1951
|
-
if (!doc) {
|
|
1952
|
-
failed.push({ id, error: "Not Found" });
|
|
1953
|
-
continue;
|
|
1954
|
-
}
|
|
1955
|
-
let before = null;
|
|
1956
|
-
if (this.collection.audit) {
|
|
1957
|
-
before = doc;
|
|
1958
|
-
}
|
|
1959
|
-
await runCollectionHooks(this.collection.hooks?.beforeDelete, {
|
|
1960
|
-
id,
|
|
1961
|
-
doc,
|
|
1962
|
-
user,
|
|
1963
|
-
req: c.req
|
|
1964
|
-
});
|
|
1965
|
-
await db.delete({ collection: this.collection.slug, id });
|
|
1966
|
-
deleted.push(id);
|
|
1967
|
-
if (this.collection.audit) {
|
|
1968
|
-
AuditService.log(db, {
|
|
1969
|
-
operation: "delete",
|
|
1970
|
-
collection: this.collection.slug,
|
|
1971
|
-
documentId: id,
|
|
1972
|
-
user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
|
|
1973
|
-
before,
|
|
1974
|
-
after: null
|
|
1975
|
-
});
|
|
1976
|
-
}
|
|
1977
|
-
await runCollectionHooks(this.collection.hooks?.afterDelete, {
|
|
1978
|
-
id,
|
|
1979
|
-
doc,
|
|
1980
|
-
user,
|
|
1981
|
-
req: c.req
|
|
1982
|
-
}, { isolated: true });
|
|
1983
|
-
} catch (err) {
|
|
1984
|
-
failed.push({ id, error: err?.message ?? "Unknown error" });
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
return c.json({
|
|
1988
|
-
message: `Deleted ${deleted.length} document(s)`,
|
|
1989
|
-
deleted,
|
|
1990
|
-
...failed.length ? { failed } : {}
|
|
1991
|
-
});
|
|
1992
|
-
}
|
|
1993
|
-
async seed(c) {
|
|
1994
|
-
const config = c.get("config");
|
|
1995
|
-
const db = config.db;
|
|
1996
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
1997
|
-
const body = await c.req.json();
|
|
1998
|
-
const initialData = body.data;
|
|
1999
|
-
if (!initialData || !Array.isArray(initialData)) {
|
|
2000
|
-
return c.json({ message: "Invalid initial data" }, 400);
|
|
2001
|
-
}
|
|
2002
|
-
const result = await db.find({ collection: this.collection.slug, limit: 1 });
|
|
2003
|
-
if (result.total > 0) {
|
|
2004
|
-
return c.json({ message: "Collection is not empty, skipping seed" });
|
|
2005
|
-
}
|
|
2006
|
-
console.log(`[dyrected/core] Auto-seeding collection: ${this.collection.slug}`);
|
|
2007
|
-
const createdDocs = [];
|
|
2008
|
-
for (const data of initialData) {
|
|
2009
|
-
const doc = await db.create({ collection: this.collection.slug, data });
|
|
2010
|
-
createdDocs.push(doc);
|
|
2011
|
-
}
|
|
2012
|
-
return c.json({ message: "Seed successful", count: createdDocs.length }, 201);
|
|
2013
|
-
}
|
|
2014
|
-
};
|
|
2015
|
-
|
|
2016
|
-
// src/controllers/global.controller.ts
|
|
2017
|
-
var GlobalController = class {
|
|
2018
|
-
global;
|
|
2019
|
-
constructor(global) {
|
|
2020
|
-
this.global = global;
|
|
2021
|
-
}
|
|
2022
|
-
async get(c) {
|
|
2023
|
-
const config = c.get("config");
|
|
2024
|
-
const db = config.db;
|
|
2025
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2026
|
-
const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 10;
|
|
2027
|
-
const user = c.get("user");
|
|
2028
|
-
let query = void 0;
|
|
2029
|
-
const beforeReadResult = await runCollectionHooks(this.global.hooks?.beforeRead, {
|
|
2030
|
-
req: c.req,
|
|
2031
|
-
query,
|
|
2032
|
-
user
|
|
2033
|
-
});
|
|
2034
|
-
if (beforeReadResult !== void 0) {
|
|
2035
|
-
query = beforeReadResult;
|
|
2036
|
-
}
|
|
2037
|
-
let data = await db.getGlobal({ slug: this.global.slug });
|
|
2038
|
-
const isEmpty = !data || Object.keys(data).length === 0;
|
|
2039
|
-
if (isEmpty && this.global.initialData) {
|
|
2040
|
-
console.log(`[dyrected/core] Auto-seeding global "${this.global.slug}" from config.initialData`);
|
|
2041
|
-
await db.updateGlobal({ slug: this.global.slug, data: this.global.initialData });
|
|
2042
|
-
data = this.global.initialData;
|
|
2043
|
-
}
|
|
2044
|
-
const dataWithDefaults = DefaultsService.apply(this.global.fields, data);
|
|
2045
|
-
const docWithCollectionHooks = await runCollectionHooks(this.global.hooks?.afterRead, {
|
|
2046
|
-
doc: dataWithDefaults,
|
|
2047
|
-
req: c.req,
|
|
2048
|
-
user
|
|
2049
|
-
});
|
|
2050
|
-
const docWithFieldHooks = await executeFieldAfterRead(this.global.fields, docWithCollectionHooks, user);
|
|
2051
|
-
if (depth > 0 && docWithFieldHooks) {
|
|
2052
|
-
const populationService = new PopulationService(db, config.collections);
|
|
2053
|
-
const populatedData = await populationService.populate({
|
|
2054
|
-
data: docWithFieldHooks,
|
|
2055
|
-
fields: this.global.fields,
|
|
2056
|
-
currentDepth: 0,
|
|
2057
|
-
maxDepth: depth
|
|
2058
|
-
});
|
|
2059
|
-
return c.json(populatedData);
|
|
2060
|
-
}
|
|
2061
|
-
return c.json(docWithFieldHooks);
|
|
2062
|
-
}
|
|
2063
|
-
async update(c) {
|
|
2064
|
-
const db = c.get("config").db;
|
|
2065
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2066
|
-
const body = await c.req.json();
|
|
2067
|
-
const user = c.get("user");
|
|
2068
|
-
const originalDoc = await db.getGlobal({ slug: this.global.slug }) || {};
|
|
2069
|
-
let data = await executeFieldBeforeChange(this.global.fields, body, originalDoc, user);
|
|
2070
|
-
data = await runCollectionHooks(this.global.hooks?.beforeChange, {
|
|
2071
|
-
data,
|
|
2072
|
-
doc: originalDoc,
|
|
2073
|
-
req: c.req,
|
|
2074
|
-
user,
|
|
2075
|
-
operation: "update"
|
|
2076
|
-
});
|
|
2077
|
-
const updated = await db.updateGlobal({ slug: this.global.slug, data });
|
|
2078
|
-
await runCollectionHooks(this.global.hooks?.afterChange, {
|
|
2079
|
-
doc: updated,
|
|
2080
|
-
previousDoc: originalDoc,
|
|
2081
|
-
user,
|
|
2082
|
-
req: c.req,
|
|
2083
|
-
operation: "update"
|
|
2084
|
-
}, { isolated: true });
|
|
2085
|
-
const readDoc = await runCollectionHooks(this.global.hooks?.afterRead, {
|
|
2086
|
-
doc: updated,
|
|
2087
|
-
req: c.req,
|
|
2088
|
-
user
|
|
2089
|
-
});
|
|
2090
|
-
const finalDoc = await executeFieldAfterRead(this.global.fields, readDoc, user);
|
|
2091
|
-
return c.json(finalDoc);
|
|
2092
|
-
}
|
|
2093
|
-
async seed(c) {
|
|
2094
|
-
const config = c.get("config");
|
|
2095
|
-
const db = config.db;
|
|
2096
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2097
|
-
const body = await c.req.json();
|
|
2098
|
-
const initialData = body.data;
|
|
2099
|
-
if (!initialData) {
|
|
2100
|
-
return c.json({ message: "Invalid initial data" }, 400);
|
|
2101
|
-
}
|
|
2102
|
-
const existing = await db.getGlobal({ slug: this.global.slug });
|
|
2103
|
-
if (existing && Object.keys(existing).length > 0) {
|
|
2104
|
-
return c.json({ message: "Global is not empty, skipping seed" });
|
|
2105
|
-
}
|
|
2106
|
-
console.log(`[dyrected/core] Auto-seeding global: ${this.global.slug}`);
|
|
2107
|
-
await db.updateGlobal({ slug: this.global.slug, data: initialData });
|
|
2108
|
-
return c.json({ message: "Seed successful", data: initialData }, 201);
|
|
2109
|
-
}
|
|
2110
|
-
};
|
|
2111
|
-
|
|
2112
|
-
// src/controllers/media.controller.ts
|
|
2113
|
-
var MediaController = class {
|
|
2114
|
-
collection;
|
|
2115
|
-
constructor(collection = "media") {
|
|
2116
|
-
this.collection = collection;
|
|
2117
|
-
}
|
|
2118
|
-
async upload(c) {
|
|
2119
|
-
const config = c.get("config");
|
|
2120
|
-
const storage = config.storage;
|
|
2121
|
-
const imageService = config.image;
|
|
2122
|
-
if (!storage) {
|
|
2123
|
-
return c.json({ message: "Storage not configured" }, 500);
|
|
2124
|
-
}
|
|
2125
|
-
const body = await c.req.parseBody();
|
|
2126
|
-
const file = body["file"];
|
|
2127
|
-
const focalPointStr = body["focalPoint"];
|
|
2128
|
-
const focalPoint = focalPointStr ? JSON.parse(focalPointStr) : void 0;
|
|
2129
|
-
if (!file) {
|
|
2130
|
-
return c.json({ message: "No file uploaded" }, 400);
|
|
2131
|
-
}
|
|
2132
|
-
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
2133
|
-
const siteId = c.get("siteId");
|
|
2134
|
-
const workspaceId = c.get("workspaceId");
|
|
2135
|
-
const prefix = workspaceId ? `${workspaceId}/${siteId}` : siteId || "default";
|
|
2136
|
-
let imageMetadata = {};
|
|
2137
|
-
let imageSizes = {};
|
|
2138
|
-
if (imageService && file.type.startsWith("image/")) {
|
|
2139
|
-
let colConfig = config.collections.find((col) => col.slug === this.collection);
|
|
2140
|
-
if (!colConfig && config.onSchemaFetch && siteId) {
|
|
2141
|
-
const dynamic = await config.onSchemaFetch(siteId);
|
|
2142
|
-
colConfig = dynamic.collections?.find((col) => col.slug === this.collection);
|
|
2143
|
-
}
|
|
2144
|
-
try {
|
|
2145
|
-
const processed = await imageService.process({
|
|
2146
|
-
buffer,
|
|
2147
|
-
mimeType: file.type,
|
|
2148
|
-
config: colConfig?.upload,
|
|
2149
|
-
focalPoint
|
|
2150
|
-
});
|
|
2151
|
-
imageMetadata = processed.metadata;
|
|
2152
|
-
imageSizes = processed.sizes;
|
|
2153
|
-
} catch (err) {
|
|
2154
|
-
console.error("[MediaController] Image processing failed:", err);
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
const fileData = await storage.upload({
|
|
2158
|
-
filename: file.name,
|
|
2159
|
-
buffer,
|
|
2160
|
-
mimeType: file.type,
|
|
2161
|
-
prefix
|
|
2162
|
-
});
|
|
2163
|
-
const finalFileData = {
|
|
2164
|
-
...fileData,
|
|
2165
|
-
...imageMetadata,
|
|
2166
|
-
focalPoint,
|
|
2167
|
-
sizes: {}
|
|
2168
|
-
};
|
|
2169
|
-
if (imageSizes) {
|
|
2170
|
-
for (const [sizeName, sizeData] of Object.entries(imageSizes)) {
|
|
2171
|
-
const ext = file.name.split(".").pop();
|
|
2172
|
-
const baseName = file.name.substring(0, file.name.lastIndexOf("."));
|
|
2173
|
-
const sizeFilename = `${baseName}-${sizeName}.${ext}`;
|
|
2174
|
-
try {
|
|
2175
|
-
const sizeFileData = await storage.upload({
|
|
2176
|
-
filename: sizeFilename,
|
|
2177
|
-
buffer: sizeData.buffer,
|
|
2178
|
-
mimeType: file.type,
|
|
2179
|
-
prefix
|
|
2180
|
-
});
|
|
2181
|
-
finalFileData.sizes[sizeName] = {
|
|
2182
|
-
...sizeFileData,
|
|
2183
|
-
width: sizeData.width,
|
|
2184
|
-
height: sizeData.height
|
|
2185
|
-
};
|
|
2186
|
-
} catch (err) {
|
|
2187
|
-
console.error(`[MediaController] Failed to upload size ${sizeName}:`, err);
|
|
2188
|
-
}
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
const db = config.db;
|
|
2192
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2193
|
-
const doc = await db.create({
|
|
2194
|
-
collection: this.collection,
|
|
2195
|
-
data: finalFileData
|
|
2196
|
-
});
|
|
2197
|
-
return c.json(doc, 201);
|
|
2198
|
-
}
|
|
2199
|
-
async find(c) {
|
|
2200
|
-
const db = c.get("config").db;
|
|
2201
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2202
|
-
const limit = Number(c.req.query("limit")) || 10;
|
|
2203
|
-
const page = Number(c.req.query("page")) || 1;
|
|
2204
|
-
const result = await db.find({
|
|
2205
|
-
collection: this.collection,
|
|
2206
|
-
limit,
|
|
2207
|
-
page
|
|
2208
|
-
});
|
|
2209
|
-
return c.json(result);
|
|
2210
|
-
}
|
|
2211
|
-
async delete(c) {
|
|
2212
|
-
const config = c.get("config");
|
|
2213
|
-
const storage = config.storage;
|
|
2214
|
-
const db = config.db;
|
|
2215
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2216
|
-
const id = c.req.param("id");
|
|
2217
|
-
if (!id) return c.json({ message: "Missing ID" }, 400);
|
|
2218
|
-
const doc = await db.findOne({ collection: this.collection, id });
|
|
2219
|
-
if (!doc) return c.json({ message: "Not Found" }, 404);
|
|
2220
|
-
if (storage) {
|
|
2221
|
-
await storage.delete({ filename: doc.filename });
|
|
2222
|
-
if (doc.sizes) {
|
|
2223
|
-
for (const size of Object.values(doc.sizes)) {
|
|
2224
|
-
if (size.filename) {
|
|
2225
|
-
await storage.delete({ filename: size.filename });
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2230
|
-
await db.delete({ collection: this.collection, id });
|
|
2231
|
-
return c.json({ message: "Deleted" });
|
|
2232
|
-
}
|
|
2233
|
-
async serve(c) {
|
|
2234
|
-
const config = c.get("config");
|
|
2235
|
-
const storage = config.storage;
|
|
2236
|
-
if (!storage || !storage.resolve) {
|
|
2237
|
-
return c.json({ message: "Storage not configured for serving" }, 404);
|
|
2238
|
-
}
|
|
2239
|
-
const filename = c.req.param("filename");
|
|
2240
|
-
if (!filename) return c.json({ message: "Missing filename" }, 400);
|
|
2241
|
-
let res = await storage.resolve({ filename });
|
|
2242
|
-
if (!res && !filename.includes("/")) {
|
|
2243
|
-
res = await storage.resolve({ filename: `default/${filename}` });
|
|
2244
|
-
}
|
|
2245
|
-
if (!res) return c.json({ message: "Not Found" }, 404);
|
|
2246
|
-
c.header("Content-Type", res.mimeType);
|
|
2247
|
-
return c.body(res.buffer);
|
|
2248
|
-
}
|
|
2249
|
-
};
|
|
2250
|
-
|
|
2251
|
-
// src/auth/token.ts
|
|
2252
|
-
var import_jose = require("jose");
|
|
2253
|
-
var import_node_util2 = require("util");
|
|
2254
|
-
function getSecret() {
|
|
2255
|
-
const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET;
|
|
2256
|
-
if (!secret) {
|
|
2257
|
-
throw new Error(
|
|
2258
|
-
"[dyrected/core] DYRECTED_JWT_SECRET is not set. Add it to your environment variables to enable auth collections."
|
|
2259
|
-
);
|
|
2260
|
-
}
|
|
2261
|
-
return new import_node_util2.TextEncoder().encode(secret);
|
|
2262
|
-
}
|
|
2263
|
-
var DEFAULT_EXPIRY = "7d";
|
|
2264
|
-
async function signCollectionToken(payload, expiresIn = DEFAULT_EXPIRY) {
|
|
2265
|
-
return new import_jose.SignJWT({ ...payload }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(expiresIn).sign(getSecret());
|
|
2266
|
-
}
|
|
2267
|
-
async function verifyCollectionToken(token) {
|
|
2268
|
-
const { payload } = await (0, import_jose.jwtVerify)(token, getSecret());
|
|
2269
|
-
return payload;
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2272
|
-
// src/services/email.service.ts
|
|
2273
|
-
var _devSend = null;
|
|
2274
|
-
var _devSendPromise = null;
|
|
2275
|
-
async function getDevSend() {
|
|
2276
|
-
if (_devSend) return _devSend;
|
|
2277
|
-
if (_devSendPromise) return _devSendPromise;
|
|
2278
|
-
_devSendPromise = (async () => {
|
|
2279
|
-
try {
|
|
2280
|
-
const nodemailer = await import("nodemailer");
|
|
2281
|
-
const account = await nodemailer.default.createTestAccount();
|
|
2282
|
-
const transport = nodemailer.default.createTransport({
|
|
2283
|
-
host: "smtp.ethereal.email",
|
|
2284
|
-
port: 587,
|
|
2285
|
-
auth: { user: account.user, pass: account.pass }
|
|
2286
|
-
});
|
|
2287
|
-
console.log("[dyrected/core] No email config \u2014 using Ethereal for dev email preview.");
|
|
2288
|
-
console.log(`[dyrected/core] Ethereal login: https://ethereal.email user: ${account.user} pass: ${account.pass}`);
|
|
2289
|
-
_devSend = async ({ to, subject, html }) => {
|
|
2290
|
-
const info = await transport.sendMail({ from: '"Dyrected Dev" <dev@dyrected.local>', to, subject, html });
|
|
2291
|
-
console.log(`[dyrected/core] Email preview URL: ${nodemailer.default.getTestMessageUrl(info)}`);
|
|
2292
|
-
};
|
|
2293
|
-
return _devSend;
|
|
2294
|
-
} catch {
|
|
2295
|
-
console.warn("[dyrected/core] nodemailer not available \u2014 emails will not be sent in dev.");
|
|
2296
|
-
return null;
|
|
2297
|
-
}
|
|
2298
|
-
})();
|
|
2299
|
-
return _devSendPromise;
|
|
2300
|
-
}
|
|
2301
|
-
async function sendEmail(config, payload) {
|
|
2302
|
-
if (config.email) {
|
|
2303
|
-
await config.email.send(payload);
|
|
2304
|
-
return;
|
|
2305
|
-
}
|
|
2306
|
-
if (process.env.NODE_ENV !== "production") {
|
|
2307
|
-
const devSend = await getDevSend();
|
|
2308
|
-
await devSend?.(payload);
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
function buildWelcomeEmail(config, args) {
|
|
2312
|
-
const custom = config.email?.templates?.welcome?.(args);
|
|
2313
|
-
return {
|
|
2314
|
-
subject: custom?.subject ?? "Welcome \u2014 your account is ready",
|
|
2315
|
-
html: custom?.html ?? `
|
|
2316
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f9fafb;table-layout:fixed">
|
|
2317
|
-
<tr>
|
|
2318
|
-
<td align="center" style="padding:40px 16px">
|
|
2319
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:600px;background-color:#ffffff;border-radius:12px;border:1px solid #e5e7eb;table-layout:fixed">
|
|
2320
|
-
<tr>
|
|
2321
|
-
<td style="padding:32px 32px 0">
|
|
2322
|
-
<p style="margin:0 0 4px;font-size:12px;font-weight:600;color:#6b7280;font-family:sans-serif;text-transform:uppercase;letter-spacing:0.05em">Dyrected</p>
|
|
2323
|
-
<h1 style="margin:0 0 24px;font-size:22px;font-weight:700;color:#111827;font-family:sans-serif">Welcome!</h1>
|
|
2324
|
-
</td>
|
|
2325
|
-
</tr>
|
|
2326
|
-
<tr>
|
|
2327
|
-
<td style="padding:0 32px">
|
|
2328
|
-
<p style="margin:0 0 12px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">Your account has been created. You can now log in with:</p>
|
|
2329
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f3f4f6;border-radius:6px;table-layout:fixed">
|
|
2330
|
-
<tr>
|
|
2331
|
-
<td style="padding:12px 16px;font-size:14px;font-weight:600;color:#111827;font-family:sans-serif;word-break:break-all">
|
|
2332
|
-
${args.email}
|
|
2333
|
-
</td>
|
|
2334
|
-
</tr>
|
|
2335
|
-
</table>
|
|
2336
|
-
</td>
|
|
2337
|
-
</tr>
|
|
2338
|
-
<tr>
|
|
2339
|
-
<td style="padding:32px">
|
|
2340
|
-
<p style="margin:0;font-size:12px;color:#9ca3af;font-family:sans-serif">If you didn't create this account, you can safely ignore this email.</p>
|
|
2341
|
-
</td>
|
|
2342
|
-
</tr>
|
|
2343
|
-
</table>
|
|
2344
|
-
</td>
|
|
2345
|
-
</tr>
|
|
2346
|
-
</table>`
|
|
2347
|
-
};
|
|
2348
|
-
}
|
|
2349
|
-
function buildInviteEmail(config, args) {
|
|
2350
|
-
const custom = config.email?.templates?.invite?.(args);
|
|
2351
|
-
return {
|
|
2352
|
-
subject: custom?.subject ?? "You've been invited",
|
|
2353
|
-
html: custom?.html ?? `
|
|
2354
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f9fafb;table-layout:fixed">
|
|
2355
|
-
<tr>
|
|
2356
|
-
<td align="center" style="padding:40px 16px">
|
|
2357
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:600px;background-color:#ffffff;border-radius:12px;border:1px solid #e5e7eb;table-layout:fixed">
|
|
2358
|
-
<tr>
|
|
2359
|
-
<td style="padding:32px 32px 0">
|
|
2360
|
-
<p style="margin:0 0 4px;font-size:12px;font-weight:600;color:#6b7280;font-family:sans-serif;text-transform:uppercase;letter-spacing:0.05em">Dyrected</p>
|
|
2361
|
-
<h1 style="margin:0 0 24px;font-size:22px;font-weight:700;color:#111827;font-family:sans-serif">You've been invited</h1>
|
|
2362
|
-
</td>
|
|
2363
|
-
</tr>
|
|
2364
|
-
<tr>
|
|
2365
|
-
<td style="padding:0 32px">
|
|
2366
|
-
${args.invitedByEmail ? `<p style="margin:0 0 12px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">You were invited by <strong style="color:#111827">${args.invitedByEmail}</strong>.</p>` : ""}
|
|
2367
|
-
<p style="margin:0 0 16px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">Use the token below to accept your invitation. It expires in 7 days.</p>
|
|
2368
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f3f4f6;border-radius:6px;table-layout:fixed">
|
|
2369
|
-
<tr>
|
|
2370
|
-
<td style="padding:12px 16px;font-family:monospace;font-size:12px;color:#374151;word-break:break-all;white-space:normal;line-height:1.4">
|
|
2371
|
-
${args.token}
|
|
2372
|
-
</td>
|
|
2373
|
-
</tr>
|
|
2374
|
-
</table>
|
|
2375
|
-
</td>
|
|
2376
|
-
</tr>
|
|
2377
|
-
<tr>
|
|
2378
|
-
<td style="padding:32px">
|
|
2379
|
-
<p style="margin:0;font-size:12px;color:#9ca3af;font-family:sans-serif">If you weren't expecting this invitation, you can safely ignore this email.</p>
|
|
2380
|
-
</td>
|
|
2381
|
-
</tr>
|
|
2382
|
-
</table>
|
|
2383
|
-
</td>
|
|
2384
|
-
</tr>
|
|
2385
|
-
</table>`
|
|
2386
|
-
};
|
|
2387
|
-
}
|
|
2388
|
-
function buildResetPasswordEmail(config, args) {
|
|
2389
|
-
const custom = config.email?.templates?.resetPassword?.(args);
|
|
2390
|
-
const resetLink = args.url;
|
|
2391
|
-
return {
|
|
2392
|
-
subject: custom?.subject ?? "Reset your password",
|
|
2393
|
-
html: custom?.html ?? `
|
|
2394
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f9fafb;table-layout:fixed">
|
|
2395
|
-
<tr>
|
|
2396
|
-
<td align="center" style="padding:40px 16px">
|
|
2397
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:600px;background-color:#ffffff;border-radius:12px;border:1px solid #e5e7eb;table-layout:fixed">
|
|
2398
|
-
<tr>
|
|
2399
|
-
<td style="padding:32px 32px 0">
|
|
2400
|
-
<p style="margin:0 0 4px;font-size:12px;font-weight:600;color:#6b7280;font-family:sans-serif;text-transform:uppercase;letter-spacing:0.05em">Dyrected</p>
|
|
2401
|
-
<h1 style="margin:0 0 24px;font-size:22px;font-weight:700;color:#111827;font-family:sans-serif">Reset your password</h1>
|
|
2402
|
-
</td>
|
|
2403
|
-
</tr>
|
|
2404
|
-
<tr>
|
|
2405
|
-
<td style="padding:0 32px">
|
|
2406
|
-
<p style="margin:0 0 24px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">We received a request to reset your password. Use the button below to set a new password. It will expire in 1 hour.</p>
|
|
2407
|
-
${resetLink ? `
|
|
2408
|
-
<table cellpadding="0" cellspacing="0" border="0" style="margin-bottom:24px">
|
|
2409
|
-
<tr>
|
|
2410
|
-
<td style="border-radius:6px;background-color:#111827">
|
|
2411
|
-
<a href="${resetLink}" style="display:inline-block;padding:12px 28px;font-size:14px;font-weight:600;color:#ffffff;text-decoration:none;font-family:sans-serif;border-radius:6px">
|
|
2412
|
-
Reset Password
|
|
2413
|
-
</a>
|
|
2414
|
-
</td>
|
|
2415
|
-
</tr>
|
|
2416
|
-
</table>
|
|
2417
|
-
` : ""}
|
|
2418
|
-
<p style="margin:0 0 8px;font-size:12px;color:#9ca3af;font-family:sans-serif">Or copy and paste this token manually in the admin dashboard:</p>
|
|
2419
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f3f4f6;border-radius:6px;table-layout:fixed">
|
|
2420
|
-
<tr>
|
|
2421
|
-
<td style="padding:12px 16px;font-family:monospace;font-size:12px;color:#374151;word-break:break-all;white-space:normal;line-height:1.4">
|
|
2422
|
-
${args.token}
|
|
2423
|
-
</td>
|
|
2424
|
-
</tr>
|
|
2425
|
-
</table>
|
|
2426
|
-
</td>
|
|
2427
|
-
</tr>
|
|
2428
|
-
<tr>
|
|
2429
|
-
<td style="padding:32px">
|
|
2430
|
-
<p style="margin:0;font-size:12px;color:#9ca3af;font-family:sans-serif">If you didn't request a password reset, you can safely ignore this email.</p>
|
|
2431
|
-
</td>
|
|
2432
|
-
</tr>
|
|
2433
|
-
</table>
|
|
2434
|
-
</td>
|
|
2435
|
-
</tr>
|
|
2436
|
-
</table>`
|
|
2437
|
-
};
|
|
2438
|
-
}
|
|
2439
|
-
function buildPasswordChangedEmail(config, args) {
|
|
2440
|
-
const custom = config.email?.templates?.passwordChanged?.(args);
|
|
2441
|
-
return {
|
|
2442
|
-
subject: custom?.subject ?? "Your password has been changed",
|
|
2443
|
-
html: custom?.html ?? `
|
|
2444
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f9fafb;table-layout:fixed">
|
|
2445
|
-
<tr>
|
|
2446
|
-
<td align="center" style="padding:40px 16px">
|
|
2447
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:600px;background-color:#ffffff;border-radius:12px;border:1px solid #e5e7eb;table-layout:fixed">
|
|
2448
|
-
<tr>
|
|
2449
|
-
<td style="padding:32px 32px 0">
|
|
2450
|
-
<p style="margin:0 0 4px;font-size:12px;font-weight:600;color:#6b7280;font-family:sans-serif;text-transform:uppercase;letter-spacing:0.05em">Dyrected</p>
|
|
2451
|
-
<h1 style="margin:0 0 24px;font-size:22px;font-weight:700;color:#111827;font-family:sans-serif">Password changed</h1>
|
|
2452
|
-
</td>
|
|
2453
|
-
</tr>
|
|
2454
|
-
<tr>
|
|
2455
|
-
<td style="padding:0 32px">
|
|
2456
|
-
<p style="margin:0 0 12px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">The password for your account was just changed:</p>
|
|
2457
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f3f4f6;border-radius:6px;table-layout:fixed">
|
|
2458
|
-
<tr>
|
|
2459
|
-
<td style="padding:12px 16px;font-size:14px;font-weight:600;color:#111827;font-family:sans-serif;word-break:break-all">
|
|
2460
|
-
${args.email}
|
|
2461
|
-
</td>
|
|
2462
|
-
</tr>
|
|
2463
|
-
</table>
|
|
2464
|
-
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;margin-top:16px;background-color:#fef2f2;border-radius:6px;border:1px solid #fecaca;table-layout:fixed">
|
|
2465
|
-
<tr>
|
|
2466
|
-
<td style="padding:12px 16px;font-size:13px;color:#b91c1c;line-height:1.5;font-family:sans-serif">
|
|
2467
|
-
If you did not make this change, please contact support immediately.
|
|
2468
|
-
</td>
|
|
2469
|
-
</tr>
|
|
2470
|
-
</table>
|
|
2471
|
-
</td>
|
|
2472
|
-
</tr>
|
|
2473
|
-
<tr>
|
|
2474
|
-
<td style="padding:32px">
|
|
2475
|
-
<p style="margin:0;font-size:12px;color:#9ca3af;font-family:sans-serif">This is an automated security notification.</p>
|
|
2476
|
-
</td>
|
|
2477
|
-
</tr>
|
|
2478
|
-
</table>
|
|
2479
|
-
</td>
|
|
2480
|
-
</tr>
|
|
2481
|
-
</table>`
|
|
2482
|
-
};
|
|
2483
|
-
}
|
|
2484
|
-
|
|
2485
|
-
// src/controllers/auth.controller.ts
|
|
2486
|
-
var AuthController = class {
|
|
2487
|
-
collection;
|
|
2488
|
-
constructor(collection) {
|
|
2489
|
-
this.collection = collection;
|
|
2490
|
-
}
|
|
2491
|
-
// ---------------------------------------------------------------------------
|
|
2492
|
-
// GET /init
|
|
2493
|
-
// Checks if the first user needs to be created.
|
|
2494
|
-
// ---------------------------------------------------------------------------
|
|
2495
|
-
async init(c) {
|
|
2496
|
-
const db = c.get("config").db;
|
|
2497
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2498
|
-
const result = await db.find({
|
|
2499
|
-
collection: this.collection.slug,
|
|
2500
|
-
limit: 1
|
|
2501
|
-
});
|
|
2502
|
-
return c.json({
|
|
2503
|
-
initialized: result.total > 0
|
|
2504
|
-
});
|
|
2505
|
-
}
|
|
2506
|
-
// ---------------------------------------------------------------------------
|
|
2507
|
-
// POST /first-user
|
|
2508
|
-
// Creates the first user if none exist.
|
|
2509
|
-
// ---------------------------------------------------------------------------
|
|
2510
|
-
async registerFirstUser(c) {
|
|
2511
|
-
const config = c.get("config");
|
|
2512
|
-
const db = config.db;
|
|
2513
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2514
|
-
const check = await db.find({
|
|
2515
|
-
collection: this.collection.slug,
|
|
2516
|
-
limit: 1
|
|
2517
|
-
});
|
|
2518
|
-
if (check.total > 0) {
|
|
2519
|
-
return c.json({ error: true, message: "Initial user already exists." }, 403);
|
|
2520
|
-
}
|
|
2521
|
-
const body = await c.req.json().catch(() => null);
|
|
2522
|
-
if (!body?.email || !body?.password) {
|
|
2523
|
-
return c.json({ error: true, message: "email and password are required." }, 400);
|
|
2524
|
-
}
|
|
2525
|
-
const hashedPassword = await hashPassword(body.password);
|
|
2526
|
-
const user = await db.create({
|
|
2527
|
-
collection: this.collection.slug,
|
|
2528
|
-
data: {
|
|
2529
|
-
...body,
|
|
2530
|
-
password: hashedPassword,
|
|
2531
|
-
roles: ["admin"]
|
|
2532
|
-
// Default first user to admin
|
|
2533
|
-
}
|
|
2534
|
-
});
|
|
2535
|
-
const token = await signCollectionToken({
|
|
2536
|
-
sub: user.id,
|
|
2537
|
-
email: user.email,
|
|
2538
|
-
collection: this.collection.slug
|
|
2539
|
-
});
|
|
2540
|
-
const { subject, html } = buildWelcomeEmail(config, { email: body.email });
|
|
2541
|
-
sendEmail(config, { to: body.email, subject, html }).catch(
|
|
2542
|
-
(err) => console.error("[dyrected/core] Failed to send welcome email:", err)
|
|
2543
|
-
);
|
|
2544
|
-
const { password: _, ...safeUser } = user;
|
|
2545
|
-
return c.json({ token, user: safeUser });
|
|
2546
|
-
}
|
|
2547
|
-
// ---------------------------------------------------------------------------
|
|
2548
|
-
// POST /login
|
|
2549
|
-
// ---------------------------------------------------------------------------
|
|
2550
|
-
async login(c) {
|
|
2551
|
-
const db = c.get("config").db;
|
|
2552
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2553
|
-
const body = await c.req.json().catch(() => null);
|
|
2554
|
-
if (!body?.email || !body?.password) {
|
|
2555
|
-
return c.json({ error: true, message: "email and password are required." }, 400);
|
|
2556
|
-
}
|
|
2557
|
-
const result = await db.find({
|
|
2558
|
-
collection: this.collection.slug,
|
|
2559
|
-
where: { email: body.email },
|
|
2560
|
-
limit: 1
|
|
2561
|
-
});
|
|
2562
|
-
const user = result.docs[0];
|
|
2563
|
-
if (!user) {
|
|
2564
|
-
return c.json({ error: true, message: "Invalid email or password." }, 401);
|
|
2565
|
-
}
|
|
2566
|
-
const valid = await verifyPassword(body.password, user.password);
|
|
2567
|
-
if (!valid) {
|
|
2568
|
-
return c.json({ error: true, message: "Invalid email or password." }, 401);
|
|
2569
|
-
}
|
|
2570
|
-
const token = await signCollectionToken({
|
|
2571
|
-
sub: user.id,
|
|
2572
|
-
email: user.email,
|
|
2573
|
-
collection: this.collection.slug
|
|
2574
|
-
});
|
|
2575
|
-
const { password: _, ...safeUser } = user;
|
|
2576
|
-
return c.json({ token, user: safeUser });
|
|
2577
|
-
}
|
|
2578
|
-
// ---------------------------------------------------------------------------
|
|
2579
|
-
// POST /logout
|
|
2580
|
-
// Auth collections use stateless JWTs — logout is handled client-side.
|
|
2581
|
-
// This endpoint exists so clients have a consistent API surface.
|
|
2582
|
-
// ---------------------------------------------------------------------------
|
|
2583
|
-
async logout(c) {
|
|
2584
|
-
return c.json({ success: true, message: "Logged out. Discard your token." });
|
|
2585
|
-
}
|
|
2586
|
-
// ---------------------------------------------------------------------------
|
|
2587
|
-
// GET /me
|
|
2588
|
-
// ---------------------------------------------------------------------------
|
|
2589
|
-
async me(c) {
|
|
2590
|
-
const db = c.get("config").db;
|
|
2591
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2592
|
-
const requestUser = c.get("user");
|
|
2593
|
-
if (!requestUser) {
|
|
2594
|
-
return c.json({ error: true, message: "Authentication required." }, 401);
|
|
2595
|
-
}
|
|
2596
|
-
const user = await db.findOne({ collection: this.collection.slug, id: requestUser.sub });
|
|
2597
|
-
if (!user) {
|
|
2598
|
-
return c.json({ error: true, message: "User not found." }, 404);
|
|
2599
|
-
}
|
|
2600
|
-
const { password: _, ...safeUser } = user;
|
|
2601
|
-
return c.json(safeUser);
|
|
2602
|
-
}
|
|
2603
|
-
// ---------------------------------------------------------------------------
|
|
2604
|
-
// POST /refresh-token
|
|
2605
|
-
// ---------------------------------------------------------------------------
|
|
2606
|
-
async refreshToken(c) {
|
|
2607
|
-
const requestUser = c.get("user");
|
|
2608
|
-
if (!requestUser) {
|
|
2609
|
-
return c.json({ error: true, message: "Authentication required." }, 401);
|
|
2610
|
-
}
|
|
2611
|
-
const token = await signCollectionToken({
|
|
2612
|
-
sub: requestUser.sub,
|
|
2613
|
-
email: requestUser.email,
|
|
2614
|
-
collection: this.collection.slug
|
|
2615
|
-
});
|
|
2616
|
-
return c.json({ token });
|
|
2617
|
-
}
|
|
2618
|
-
// ---------------------------------------------------------------------------
|
|
2619
|
-
// POST /forgot-password
|
|
2620
|
-
// Requires config.email to be set. Silently succeeds if email not found
|
|
2621
|
-
// to prevent email enumeration.
|
|
2622
|
-
// ---------------------------------------------------------------------------
|
|
2623
|
-
async forgotPassword(c) {
|
|
2624
|
-
const config = c.get("config");
|
|
2625
|
-
const db = config.db;
|
|
2626
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2627
|
-
const body = await c.req.json().catch(() => null);
|
|
2628
|
-
if (!body?.email) {
|
|
2629
|
-
return c.json({ error: true, message: "email is required." }, 400);
|
|
2630
|
-
}
|
|
2631
|
-
const result = await db.find({
|
|
2632
|
-
collection: this.collection.slug,
|
|
2633
|
-
where: { email: body.email },
|
|
2634
|
-
limit: 1
|
|
2635
|
-
});
|
|
2636
|
-
const user = result.docs[0];
|
|
2637
|
-
if (user) {
|
|
2638
|
-
const resetToken = await signCollectionToken(
|
|
2639
|
-
{ sub: user.id, email: user.email, collection: this.collection.slug, purpose: "reset" },
|
|
2640
|
-
"1h"
|
|
2641
|
-
);
|
|
2642
|
-
const resetUrl = body?.resetUrl;
|
|
2643
|
-
const url = resetUrl ? `${resetUrl}${resetUrl.includes("?") ? "&" : "?"}token=${encodeURIComponent(resetToken)}` : void 0;
|
|
2644
|
-
try {
|
|
2645
|
-
const { subject, html } = buildResetPasswordEmail(config, { token: resetToken, url });
|
|
2646
|
-
await sendEmail(config, { to: user.email, subject, html });
|
|
2647
|
-
} catch (err) {
|
|
2648
|
-
console.error("[dyrected/core] Failed to send password reset email:", err);
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
return c.json({
|
|
2652
|
-
success: true,
|
|
2653
|
-
message: "If an account with that email exists, a reset link has been sent."
|
|
2654
|
-
});
|
|
2655
|
-
}
|
|
2656
|
-
// ---------------------------------------------------------------------------
|
|
2657
|
-
// POST /reset-password
|
|
2658
|
-
// Expects { token: string, password: string } in body.
|
|
2659
|
-
// The token is the reset JWT issued by /forgot-password.
|
|
2660
|
-
// ---------------------------------------------------------------------------
|
|
2661
|
-
async resetPassword(c) {
|
|
2662
|
-
const config = c.get("config");
|
|
2663
|
-
const db = config.db;
|
|
2664
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2665
|
-
const body = await c.req.json().catch(() => null);
|
|
2666
|
-
if (!body?.token || !body?.password) {
|
|
2667
|
-
return c.json({ error: true, message: "token and password are required." }, 400);
|
|
2668
|
-
}
|
|
2669
|
-
let payload;
|
|
2670
|
-
try {
|
|
2671
|
-
payload = await verifyCollectionToken(body.token);
|
|
2672
|
-
} catch {
|
|
2673
|
-
return c.json({ error: true, message: "Reset token is invalid or has expired." }, 400);
|
|
2674
|
-
}
|
|
2675
|
-
if (payload.collection !== this.collection.slug || payload.purpose !== "reset") {
|
|
2676
|
-
return c.json({ error: true, message: "Reset token is invalid or has expired." }, 400);
|
|
2677
|
-
}
|
|
2678
|
-
const hashedPassword = await hashPassword(body.password);
|
|
2679
|
-
await db.update({
|
|
2680
|
-
collection: this.collection.slug,
|
|
2681
|
-
id: payload.sub,
|
|
2682
|
-
data: { password: hashedPassword }
|
|
2683
|
-
});
|
|
2684
|
-
const { subject, html } = buildPasswordChangedEmail(config, { email: payload.email });
|
|
2685
|
-
sendEmail(config, { to: payload.email, subject, html }).catch(
|
|
2686
|
-
(err) => console.error("[dyrected/core] Failed to send password-changed email:", err)
|
|
2687
|
-
);
|
|
2688
|
-
return c.json({ success: true, message: "Password has been reset. You can now log in." });
|
|
2689
|
-
}
|
|
2690
|
-
// ---------------------------------------------------------------------------
|
|
2691
|
-
// POST /invite
|
|
2692
|
-
// Requires auth. Issues a signed invite token and emails it to the invitee.
|
|
2693
|
-
// ---------------------------------------------------------------------------
|
|
2694
|
-
async invite(c) {
|
|
2695
|
-
const config = c.get("config");
|
|
2696
|
-
const db = config.db;
|
|
2697
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2698
|
-
const requestUser = c.get("user");
|
|
2699
|
-
if (!requestUser) {
|
|
2700
|
-
return c.json({ error: true, message: "Authentication required." }, 401);
|
|
2701
|
-
}
|
|
2702
|
-
const body = await c.req.json().catch(() => null);
|
|
2703
|
-
if (!body?.email) {
|
|
2704
|
-
return c.json({ error: true, message: "email is required." }, 400);
|
|
2705
|
-
}
|
|
2706
|
-
const existing = await db.find({
|
|
2707
|
-
collection: this.collection.slug,
|
|
2708
|
-
where: { email: body.email },
|
|
2709
|
-
limit: 1
|
|
2710
|
-
});
|
|
2711
|
-
if (existing.total > 0) {
|
|
2712
|
-
return c.json({ error: true, message: "An account with that email already exists." }, 409);
|
|
2713
|
-
}
|
|
2714
|
-
const inviteToken = await signCollectionToken(
|
|
2715
|
-
{ sub: body.email, email: body.email, collection: this.collection.slug, purpose: "invite" },
|
|
2716
|
-
"7d"
|
|
2717
|
-
);
|
|
2718
|
-
try {
|
|
2719
|
-
const { subject, html } = buildInviteEmail(config, {
|
|
2720
|
-
token: inviteToken,
|
|
2721
|
-
invitedByEmail: requestUser.email
|
|
2722
|
-
});
|
|
2723
|
-
await sendEmail(config, { to: body.email, subject, html });
|
|
2724
|
-
} catch (err) {
|
|
2725
|
-
console.error("[dyrected/core] Failed to send invite email:", err);
|
|
2726
|
-
}
|
|
2727
|
-
return c.json({ success: true, message: `Invite sent to ${body.email}.` });
|
|
2728
|
-
}
|
|
2729
|
-
// ---------------------------------------------------------------------------
|
|
2730
|
-
// POST /accept-invite
|
|
2731
|
-
// Public. Validates the invite token and creates the user account.
|
|
2732
|
-
// Body: { token, password, ...extraFields }
|
|
2733
|
-
// ---------------------------------------------------------------------------
|
|
2734
|
-
async acceptInvite(c) {
|
|
2735
|
-
const config = c.get("config");
|
|
2736
|
-
const db = config.db;
|
|
2737
|
-
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2738
|
-
const body = await c.req.json().catch(() => null);
|
|
2739
|
-
if (!body?.token || !body?.password) {
|
|
2740
|
-
return c.json({ error: true, message: "token and password are required." }, 400);
|
|
2741
|
-
}
|
|
2742
|
-
let payload;
|
|
2743
|
-
try {
|
|
2744
|
-
payload = await verifyCollectionToken(body.token);
|
|
2745
|
-
} catch {
|
|
2746
|
-
return c.json({ error: true, message: "Invite token is invalid or has expired." }, 400);
|
|
2747
|
-
}
|
|
2748
|
-
if (payload.collection !== this.collection.slug || payload.purpose !== "invite") {
|
|
2749
|
-
return c.json({ error: true, message: "Invite token is invalid or has expired." }, 400);
|
|
2750
|
-
}
|
|
2751
|
-
const inviteeEmail = payload.sub;
|
|
2752
|
-
const existing = await db.find({
|
|
2753
|
-
collection: this.collection.slug,
|
|
2754
|
-
where: { email: inviteeEmail },
|
|
2755
|
-
limit: 1
|
|
2756
|
-
});
|
|
2757
|
-
if (existing.total > 0) {
|
|
2758
|
-
return c.json({ error: true, message: "An account with that email already exists." }, 409);
|
|
2759
|
-
}
|
|
2760
|
-
const { token: _t, password: _p, ...extraFields } = body;
|
|
2761
|
-
const hashedPassword = await hashPassword(body.password);
|
|
2762
|
-
const user = await db.create({
|
|
2763
|
-
collection: this.collection.slug,
|
|
2764
|
-
data: { ...extraFields, email: inviteeEmail, password: hashedPassword }
|
|
2765
|
-
});
|
|
2766
|
-
const sessionToken = await signCollectionToken({
|
|
2767
|
-
sub: user.id,
|
|
2768
|
-
email: inviteeEmail,
|
|
2769
|
-
collection: this.collection.slug
|
|
2770
|
-
});
|
|
2771
|
-
const { subject, html } = buildWelcomeEmail(config, { email: inviteeEmail });
|
|
2772
|
-
sendEmail(config, { to: inviteeEmail, subject, html }).catch(
|
|
2773
|
-
(err) => console.error("[dyrected/core] Failed to send welcome email:", err)
|
|
2774
|
-
);
|
|
2775
|
-
const { password: _, ...safeUser } = user;
|
|
2776
|
-
return c.json({ token: sessionToken, user: safeUser }, 201);
|
|
2777
|
-
}
|
|
2778
|
-
};
|
|
2779
|
-
|
|
2780
|
-
// src/controllers/preview.controller.ts
|
|
2781
|
-
var import_jose2 = require("jose");
|
|
2782
|
-
var import_node_util3 = require("util");
|
|
2783
|
-
var PreviewController = class {
|
|
2784
|
-
getSecret() {
|
|
2785
|
-
const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET || "dyrected-preview-secret-change-me";
|
|
2786
|
-
return new import_node_util3.TextEncoder().encode(secret);
|
|
2787
|
-
}
|
|
2788
|
-
/**
|
|
2789
|
-
* POST /api/preview-token
|
|
2790
|
-
* Generates a short-lived token for previewing unsaved data.
|
|
2791
|
-
*/
|
|
2792
|
-
async createToken(c) {
|
|
2793
|
-
const body = await c.req.json().catch(() => null);
|
|
2794
|
-
if (!body?.collectionSlug || !body?.data) {
|
|
2795
|
-
return c.json({ error: true, message: "collectionSlug and data are required." }, 400);
|
|
2796
|
-
}
|
|
2797
|
-
const token = await new import_jose2.SignJWT({
|
|
2798
|
-
collectionSlug: body.collectionSlug,
|
|
2799
|
-
documentId: body.documentId,
|
|
2800
|
-
data: body.data
|
|
2801
|
-
}).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime("15m").sign(this.getSecret());
|
|
2802
|
-
const expiresAt = new Date(Date.now() + 15 * 60 * 1e3).toISOString();
|
|
2803
|
-
return c.json({ token, expiresAt });
|
|
2804
|
-
}
|
|
2805
|
-
/**
|
|
2806
|
-
* GET /api/preview-data?token=<jwt>
|
|
2807
|
-
* Returns the data stored in the preview token.
|
|
2808
|
-
*/
|
|
2809
|
-
async getData(c) {
|
|
2810
|
-
const token = c.req.query("token");
|
|
2811
|
-
if (!token) {
|
|
2812
|
-
return c.json({ error: true, message: "token query parameter is required." }, 400);
|
|
2813
|
-
}
|
|
2814
|
-
try {
|
|
2815
|
-
const { payload } = await (0, import_jose2.jwtVerify)(token, this.getSecret());
|
|
2816
|
-
return c.json(payload);
|
|
2817
|
-
} catch (err) {
|
|
2818
|
-
return c.json({ error: true, message: "Invalid or expired preview token." }, 401);
|
|
2819
|
-
}
|
|
2820
|
-
}
|
|
2821
|
-
};
|
|
2822
|
-
|
|
2823
|
-
// src/middleware/auth.ts
|
|
2824
|
-
function requireAuth() {
|
|
2825
|
-
return async (c, next) => {
|
|
2826
|
-
const authHeader = c.req.header("Authorization");
|
|
2827
|
-
const token = authHeader?.replace(/^Bearer\s+/i, "");
|
|
2828
|
-
if (!token) {
|
|
2829
|
-
return c.json({ error: true, message: "Authentication required." }, 401);
|
|
2830
|
-
}
|
|
2831
|
-
try {
|
|
2832
|
-
const user = await verifyCollectionToken(token);
|
|
2833
|
-
c.set("user", user);
|
|
2834
|
-
await next();
|
|
2835
|
-
} catch {
|
|
2836
|
-
return c.json({ error: true, message: "Invalid or expired token." }, 401);
|
|
2837
|
-
}
|
|
2838
|
-
};
|
|
2839
|
-
}
|
|
2840
|
-
function optionalAuth() {
|
|
2841
|
-
return async (c, next) => {
|
|
2842
|
-
const authHeader = c.req.header("Authorization");
|
|
2843
|
-
const token = authHeader?.replace(/^Bearer\s+/i, "");
|
|
2844
|
-
if (token) {
|
|
2845
|
-
try {
|
|
2846
|
-
const user = await verifyCollectionToken(token);
|
|
2847
|
-
c.set("user", user);
|
|
2848
|
-
} catch {
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
await next();
|
|
2852
|
-
};
|
|
2853
|
-
}
|
|
2854
|
-
|
|
2855
|
-
// src/utils/openapi.ts
|
|
2856
|
-
function generateOpenApi(config) {
|
|
2857
|
-
const spec = {
|
|
2858
|
-
openapi: "3.0.0",
|
|
2859
|
-
info: {
|
|
2860
|
-
title: "Dyrected API",
|
|
2861
|
-
version: "1.0.0",
|
|
2862
|
-
description: "Automatically generated OpenAPI specification for the Dyrected project."
|
|
2863
|
-
},
|
|
2864
|
-
components: {
|
|
2865
|
-
schemas: {},
|
|
2866
|
-
securitySchemes: {
|
|
2867
|
-
ApiKeyAuth: {
|
|
2868
|
-
type: "apiKey",
|
|
2869
|
-
in: "header",
|
|
2870
|
-
name: "x-api-key"
|
|
2871
|
-
}
|
|
2872
|
-
}
|
|
2873
|
-
},
|
|
2874
|
-
paths: {},
|
|
2875
|
-
security: [{ ApiKeyAuth: [] }]
|
|
2876
|
-
};
|
|
2877
|
-
for (const collection of config.collections) {
|
|
2878
|
-
spec.components.schemas[collection.slug] = collectionToSchema(collection);
|
|
2879
|
-
}
|
|
2880
|
-
for (const global of config.globals) {
|
|
2881
|
-
spec.components.schemas[global.slug] = globalToSchema(global);
|
|
2882
|
-
}
|
|
2883
|
-
for (const collection of config.collections) {
|
|
2884
|
-
const slug = collection.slug;
|
|
2885
|
-
const path = `/api/collections/${slug}`;
|
|
2886
|
-
const labels = collection.labels || { singular: slug, plural: `${slug}s` };
|
|
2887
|
-
spec.paths[path] = {
|
|
2888
|
-
get: {
|
|
2889
|
-
tags: ["Collections"],
|
|
2890
|
-
summary: `Find ${labels.plural}`,
|
|
2891
|
-
parameters: [
|
|
2892
|
-
{ name: "limit", in: "query", schema: { type: "integer", default: 10 } },
|
|
2893
|
-
{ name: "page", in: "query", schema: { type: "integer", default: 1 } },
|
|
2894
|
-
{ name: "where", in: "query", schema: { type: "string" }, description: "JSON filter" },
|
|
2895
|
-
{ name: "sort", in: "query", schema: { type: "string" }, description: "Sort field (e.g. -createdAt)" }
|
|
2896
|
-
],
|
|
2897
|
-
responses: {
|
|
2898
|
-
200: {
|
|
2899
|
-
description: "Success",
|
|
2900
|
-
content: {
|
|
2901
|
-
"application/json": {
|
|
2902
|
-
schema: {
|
|
2903
|
-
type: "object",
|
|
2904
|
-
properties: {
|
|
2905
|
-
docs: { type: "array", items: { $ref: `#/components/schemas/${slug}` } },
|
|
2906
|
-
total: { type: "integer" },
|
|
2907
|
-
limit: { type: "integer" },
|
|
2908
|
-
page: { type: "integer" }
|
|
2909
|
-
}
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
}
|
|
2913
|
-
}
|
|
2914
|
-
}
|
|
2915
|
-
},
|
|
2916
|
-
post: {
|
|
2917
|
-
tags: ["Collections"],
|
|
2918
|
-
summary: `Create ${labels.singular}`,
|
|
2919
|
-
requestBody: {
|
|
2920
|
-
required: true,
|
|
2921
|
-
content: {
|
|
2922
|
-
"application/json": {
|
|
2923
|
-
schema: { $ref: `#/components/schemas/${slug}` }
|
|
2924
|
-
}
|
|
2925
|
-
}
|
|
2926
|
-
},
|
|
2927
|
-
responses: {
|
|
2928
|
-
201: {
|
|
2929
|
-
description: "Created",
|
|
2930
|
-
content: {
|
|
2931
|
-
"application/json": {
|
|
2932
|
-
schema: { $ref: `#/components/schemas/${slug}` }
|
|
2933
|
-
}
|
|
2934
|
-
}
|
|
2935
|
-
}
|
|
2936
|
-
}
|
|
2937
|
-
}
|
|
2938
|
-
};
|
|
2939
|
-
spec.paths[`${path}/{id}`] = {
|
|
2940
|
-
get: {
|
|
2941
|
-
tags: ["Collections"],
|
|
2942
|
-
summary: `Get a single ${labels.singular}`,
|
|
2943
|
-
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
2944
|
-
responses: {
|
|
2945
|
-
200: {
|
|
2946
|
-
description: "Success",
|
|
2947
|
-
content: {
|
|
2948
|
-
"application/json": {
|
|
2949
|
-
schema: { $ref: `#/components/schemas/${slug}` }
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
}
|
|
2953
|
-
}
|
|
2954
|
-
},
|
|
2955
|
-
patch: {
|
|
2956
|
-
tags: ["Collections"],
|
|
2957
|
-
summary: `Update ${labels.singular}`,
|
|
2958
|
-
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
2959
|
-
requestBody: {
|
|
2960
|
-
required: true,
|
|
2961
|
-
content: {
|
|
2962
|
-
"application/json": {
|
|
2963
|
-
schema: { $ref: `#/components/schemas/${slug}` }
|
|
2964
|
-
}
|
|
2965
|
-
}
|
|
2966
|
-
},
|
|
2967
|
-
responses: {
|
|
2968
|
-
200: {
|
|
2969
|
-
description: "Updated",
|
|
2970
|
-
content: {
|
|
2971
|
-
"application/json": {
|
|
2972
|
-
schema: { $ref: `#/components/schemas/${slug}` }
|
|
2973
|
-
}
|
|
2974
|
-
}
|
|
2975
|
-
}
|
|
2976
|
-
}
|
|
2977
|
-
},
|
|
2978
|
-
delete: {
|
|
2979
|
-
tags: ["Collections"],
|
|
2980
|
-
summary: `Delete ${labels.singular}`,
|
|
2981
|
-
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
2982
|
-
responses: {
|
|
2983
|
-
204: { description: "Deleted" }
|
|
2984
|
-
}
|
|
2985
|
-
}
|
|
2986
|
-
};
|
|
2987
|
-
}
|
|
2988
|
-
for (const global of config.globals) {
|
|
2989
|
-
const slug = global.slug;
|
|
2990
|
-
const path = `/api/globals/${slug}`;
|
|
2991
|
-
spec.paths[path] = {
|
|
2992
|
-
get: {
|
|
2993
|
-
tags: ["Globals"],
|
|
2994
|
-
summary: `Get ${global.label || slug}`,
|
|
2995
|
-
responses: {
|
|
2996
|
-
200: {
|
|
2997
|
-
description: "Success",
|
|
2998
|
-
content: {
|
|
2999
|
-
"application/json": {
|
|
3000
|
-
schema: { $ref: `#/components/schemas/${slug}` }
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
3003
|
-
}
|
|
3004
|
-
}
|
|
3005
|
-
},
|
|
3006
|
-
patch: {
|
|
3007
|
-
tags: ["Globals"],
|
|
3008
|
-
summary: `Update ${global.label || slug}`,
|
|
3009
|
-
requestBody: {
|
|
3010
|
-
required: true,
|
|
3011
|
-
content: {
|
|
3012
|
-
"application/json": {
|
|
3013
|
-
schema: { $ref: `#/components/schemas/${slug}` }
|
|
3014
|
-
}
|
|
3015
|
-
}
|
|
3016
|
-
},
|
|
3017
|
-
responses: {
|
|
3018
|
-
200: {
|
|
3019
|
-
description: "Updated",
|
|
3020
|
-
content: {
|
|
3021
|
-
"application/json": {
|
|
3022
|
-
schema: { $ref: `#/components/schemas/${slug}` }
|
|
3023
|
-
}
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
3026
|
-
}
|
|
3027
|
-
}
|
|
3028
|
-
};
|
|
3029
|
-
}
|
|
3030
|
-
if (config.storage) {
|
|
3031
|
-
spec.paths["/api/media"] = {
|
|
3032
|
-
get: {
|
|
3033
|
-
tags: ["Media"],
|
|
3034
|
-
summary: "List Media",
|
|
3035
|
-
responses: {
|
|
3036
|
-
200: {
|
|
3037
|
-
description: "Success",
|
|
3038
|
-
content: {
|
|
3039
|
-
"application/json": {
|
|
3040
|
-
schema: {
|
|
3041
|
-
type: "object",
|
|
3042
|
-
properties: {
|
|
3043
|
-
docs: { type: "array", items: { type: "object", additionalProperties: true } }
|
|
3044
|
-
}
|
|
3045
|
-
}
|
|
3046
|
-
}
|
|
3047
|
-
}
|
|
3048
|
-
}
|
|
3049
|
-
}
|
|
3050
|
-
},
|
|
3051
|
-
post: {
|
|
3052
|
-
tags: ["Media"],
|
|
3053
|
-
summary: "Upload Media",
|
|
3054
|
-
requestBody: {
|
|
3055
|
-
content: {
|
|
3056
|
-
"multipart/form-data": {
|
|
3057
|
-
schema: {
|
|
3058
|
-
type: "object",
|
|
3059
|
-
properties: {
|
|
3060
|
-
file: { type: "string", format: "binary" }
|
|
3061
|
-
}
|
|
3062
|
-
}
|
|
3063
|
-
}
|
|
3064
|
-
}
|
|
3065
|
-
},
|
|
3066
|
-
responses: {
|
|
3067
|
-
201: { description: "Uploaded" }
|
|
3068
|
-
}
|
|
3069
|
-
}
|
|
3070
|
-
};
|
|
3071
|
-
}
|
|
3072
|
-
return spec;
|
|
3073
|
-
}
|
|
3074
|
-
function collectionToSchema(collection) {
|
|
3075
|
-
const { properties, required } = fieldsToProperties(collection.fields);
|
|
3076
|
-
return {
|
|
3077
|
-
type: "object",
|
|
3078
|
-
properties: {
|
|
3079
|
-
id: { type: "string" },
|
|
3080
|
-
createdAt: { type: "string", format: "date-time" },
|
|
3081
|
-
updatedAt: { type: "string", format: "date-time" },
|
|
3082
|
-
...properties
|
|
3083
|
-
},
|
|
3084
|
-
required: ["id", ...required]
|
|
3085
|
-
};
|
|
3086
|
-
}
|
|
3087
|
-
function globalToSchema(global) {
|
|
3088
|
-
const { properties, required } = fieldsToProperties(global.fields);
|
|
3089
|
-
return {
|
|
3090
|
-
type: "object",
|
|
3091
|
-
properties,
|
|
3092
|
-
required
|
|
3093
|
-
};
|
|
3094
|
-
}
|
|
3095
|
-
function fieldsToProperties(fields) {
|
|
3096
|
-
const props = {};
|
|
3097
|
-
const required = [];
|
|
3098
|
-
for (const field of fields) {
|
|
3099
|
-
if (!field.name || field.type === "join" || field.type === "row") continue;
|
|
3100
|
-
props[field.name] = fieldToSchema(field);
|
|
3101
|
-
if (field.required) {
|
|
3102
|
-
required.push(field.name);
|
|
3103
|
-
}
|
|
3104
|
-
}
|
|
3105
|
-
return { properties: props, required };
|
|
3106
|
-
}
|
|
3107
|
-
function fieldToSchema(field) {
|
|
3108
|
-
let schema = {};
|
|
3109
|
-
switch (field.type) {
|
|
3110
|
-
case "text":
|
|
3111
|
-
case "textarea":
|
|
3112
|
-
case "email":
|
|
3113
|
-
case "url":
|
|
3114
|
-
schema = { type: "string" };
|
|
3115
|
-
break;
|
|
3116
|
-
case "number":
|
|
3117
|
-
schema = { type: "number" };
|
|
3118
|
-
break;
|
|
3119
|
-
case "boolean":
|
|
3120
|
-
schema = { type: "boolean" };
|
|
3121
|
-
break;
|
|
3122
|
-
case "date":
|
|
3123
|
-
schema = { type: "string", format: "date-time" };
|
|
3124
|
-
break;
|
|
3125
|
-
case "select":
|
|
3126
|
-
case "radio":
|
|
3127
|
-
schema = { type: "string", enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0 };
|
|
3128
|
-
break;
|
|
3129
|
-
case "multiSelect":
|
|
3130
|
-
schema = {
|
|
3131
|
-
type: "array",
|
|
3132
|
-
items: { type: "string", enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0 }
|
|
3133
|
-
};
|
|
3134
|
-
break;
|
|
3135
|
-
case "relationship":
|
|
3136
|
-
schema = { type: "string", description: `ID of a ${field.relationTo} record` };
|
|
3137
|
-
break;
|
|
3138
|
-
case "object": {
|
|
3139
|
-
const { properties, required } = fieldsToProperties(field.fields || []);
|
|
3140
|
-
schema = { type: "object", properties, required };
|
|
3141
|
-
break;
|
|
3142
|
-
}
|
|
3143
|
-
case "array": {
|
|
3144
|
-
const { properties, required } = fieldsToProperties(field.fields || []);
|
|
3145
|
-
schema = { type: "array", items: { type: "object", properties, required } };
|
|
3146
|
-
break;
|
|
3147
|
-
}
|
|
3148
|
-
case "json":
|
|
3149
|
-
case "richText":
|
|
3150
|
-
schema = { type: "object", additionalProperties: true };
|
|
3151
|
-
break;
|
|
3152
|
-
case "blocks":
|
|
3153
|
-
schema = {
|
|
3154
|
-
type: "array",
|
|
3155
|
-
items: {
|
|
3156
|
-
oneOf: field.blocks?.map((block) => {
|
|
3157
|
-
const { properties, required } = fieldsToProperties(block.fields);
|
|
3158
|
-
return {
|
|
3159
|
-
type: "object",
|
|
3160
|
-
properties: {
|
|
3161
|
-
blockType: { type: "string", enum: [block.slug] },
|
|
3162
|
-
...properties
|
|
3163
|
-
},
|
|
3164
|
-
required: ["blockType", ...required]
|
|
3165
|
-
};
|
|
3166
|
-
})
|
|
3167
|
-
}
|
|
3168
|
-
};
|
|
3169
|
-
break;
|
|
3170
|
-
default:
|
|
3171
|
-
schema = { type: "string" };
|
|
3172
|
-
}
|
|
3173
|
-
if (field.label) schema.description = field.label;
|
|
3174
|
-
return schema;
|
|
3175
|
-
}
|
|
3176
|
-
|
|
3177
|
-
// src/utils/swagger.ts
|
|
3178
|
-
function getSwaggerHtml(specUrl = "/api/openapi.json") {
|
|
3179
|
-
return `
|
|
3180
|
-
<!DOCTYPE html>
|
|
3181
|
-
<html lang="en">
|
|
3182
|
-
<head>
|
|
3183
|
-
<meta charset="utf-8" />
|
|
3184
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
3185
|
-
<meta name="description" content="SwaggerUI" />
|
|
3186
|
-
<title>Dyrected API Documentation</title>
|
|
3187
|
-
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
|
|
3188
|
-
</head>
|
|
3189
|
-
<body>
|
|
3190
|
-
<div id="swagger-ui"></div>
|
|
3191
|
-
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js" charset="UTF-8"></script>
|
|
3192
|
-
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-standalone-preset.js" charset="UTF-8"></script>
|
|
3193
|
-
<script>
|
|
3194
|
-
window.onload = () => {
|
|
3195
|
-
// Forward the apikey query param when loading the spec and making API calls
|
|
3196
|
-
const params = new URLSearchParams(window.location.search);
|
|
3197
|
-
const apiKey = params.get('apikey');
|
|
3198
|
-
const specUrlWithKey = apiKey ? '${specUrl}?apikey=' + encodeURIComponent(apiKey) : '${specUrl}';
|
|
3199
|
-
|
|
3200
|
-
window.ui = SwaggerUIBundle({
|
|
3201
|
-
url: specUrlWithKey,
|
|
3202
|
-
dom_id: '#swagger-ui',
|
|
3203
|
-
presets: [
|
|
3204
|
-
SwaggerUIBundle.presets.apis,
|
|
3205
|
-
SwaggerUIStandalonePreset
|
|
3206
|
-
],
|
|
3207
|
-
layout: "BaseLayout",
|
|
3208
|
-
deepLinking: true,
|
|
3209
|
-
showExtensions: true,
|
|
3210
|
-
showCommonExtensions: true,
|
|
3211
|
-
// Inject x-api-key header on every request made from the Swagger UI
|
|
3212
|
-
requestInterceptor: (request) => {
|
|
3213
|
-
if (apiKey) {
|
|
3214
|
-
request.headers['x-api-key'] = apiKey;
|
|
3215
|
-
}
|
|
3216
|
-
return request;
|
|
3217
|
-
}
|
|
3218
|
-
});
|
|
3219
|
-
};
|
|
3220
|
-
</script>
|
|
3221
|
-
</body>
|
|
3222
|
-
</html>
|
|
3223
|
-
`;
|
|
3224
|
-
}
|
|
3225
|
-
|
|
3226
|
-
// src/auth/jexl.ts
|
|
3227
|
-
var import_jexl = __toESM(require("jexl"), 1);
|
|
3228
|
-
async function evaluateAccess(expression, context) {
|
|
3229
|
-
if (expression === void 0 || expression === null) return false;
|
|
3230
|
-
if (typeof expression === "boolean") return expression;
|
|
3231
|
-
try {
|
|
3232
|
-
const result = await import_jexl.default.eval(expression, context);
|
|
3233
|
-
return !!result;
|
|
3234
|
-
} catch (err) {
|
|
3235
|
-
console.error("[dyrected/core] Jexl evaluation failed:", err);
|
|
3236
|
-
return false;
|
|
3237
|
-
}
|
|
3238
|
-
}
|
|
3239
|
-
|
|
3240
|
-
// src/router.ts
|
|
3241
|
-
function accessGate(target, action) {
|
|
3242
|
-
return async (c, next) => {
|
|
3243
|
-
const user = c.get("user");
|
|
3244
|
-
const accessExpr = target.access?.[action];
|
|
3245
|
-
if (accessExpr === void 0 || accessExpr === null) {
|
|
3246
|
-
return await next();
|
|
3247
|
-
}
|
|
3248
|
-
const accessArgs = { user, req: c.req, doc: null };
|
|
3249
|
-
const allowed = await evaluateAccess(accessExpr, accessArgs);
|
|
3250
|
-
if (!allowed) {
|
|
3251
|
-
return c.json({ error: true, message: `Access denied: ${action} on ${target.slug}` }, 403);
|
|
3252
|
-
}
|
|
3253
|
-
await next();
|
|
3254
|
-
};
|
|
3255
|
-
}
|
|
3256
|
-
async function checkAccess(access, accessArgs) {
|
|
3257
|
-
if (access === void 0 || access === null) return true;
|
|
3258
|
-
if (typeof access === "function") {
|
|
3259
|
-
try {
|
|
3260
|
-
const result = await access(accessArgs);
|
|
3261
|
-
return typeof result === "boolean" ? result : !!result;
|
|
3262
|
-
} catch (err) {
|
|
3263
|
-
console.error("[dyrected/core] Functional access check failed:", err);
|
|
3264
|
-
return false;
|
|
3265
|
-
}
|
|
3266
|
-
}
|
|
3267
|
-
if (typeof access === "string" || typeof access === "boolean") {
|
|
3268
|
-
return evaluateAccess(access, accessArgs);
|
|
3269
|
-
}
|
|
3270
|
-
return true;
|
|
3271
|
-
}
|
|
3272
|
-
function serializeFieldForApi(f) {
|
|
3273
|
-
if (!f) return f;
|
|
3274
|
-
const serialized = { ...f };
|
|
3275
|
-
if (serialized.admin?.hooks) {
|
|
3276
|
-
const hooks = { ...serialized.admin.hooks };
|
|
3277
|
-
if (typeof hooks.onChange === "function") {
|
|
3278
|
-
hooks.onChange = hooks.onChange.toString();
|
|
3279
|
-
}
|
|
3280
|
-
if (typeof hooks.options === "function") {
|
|
3281
|
-
hooks.options = hooks.options.toString();
|
|
3282
|
-
}
|
|
3283
|
-
serialized.admin = { ...serialized.admin, hooks };
|
|
3284
|
-
}
|
|
3285
|
-
if (typeof serialized.options === "function" || serialized.options && typeof serialized.options === "object" && "resolve" in serialized.options) {
|
|
3286
|
-
serialized.options = { _dynamic: true };
|
|
3287
|
-
}
|
|
3288
|
-
if (serialized.fields) {
|
|
3289
|
-
serialized.fields = serialized.fields.map(serializeFieldForApi);
|
|
3290
|
-
}
|
|
3291
|
-
if (serialized.blocks) {
|
|
3292
|
-
serialized.blocks = serialized.blocks.map((b) => ({
|
|
3293
|
-
...b,
|
|
3294
|
-
fields: b.fields?.map(serializeFieldForApi)
|
|
3295
|
-
}));
|
|
3296
|
-
}
|
|
3297
|
-
return serialized;
|
|
3298
|
-
}
|
|
3299
|
-
function registerRoutes(app, config) {
|
|
3300
|
-
app.get("/api/schemas", optionalAuth(), async (c) => {
|
|
3301
|
-
const siteId = c.req.header("X-Site-Id");
|
|
3302
|
-
let collections = [...config.collections];
|
|
3303
|
-
let globals = [...config.globals];
|
|
3304
|
-
if (siteId && config.onSchemaFetch) {
|
|
3305
|
-
const dynamic = await config.onSchemaFetch(siteId);
|
|
3306
|
-
if (dynamic.collections) collections = [...collections, ...dynamic.collections];
|
|
3307
|
-
if (dynamic.globals) globals = [...globals, ...dynamic.globals];
|
|
3308
|
-
if (dynamic.admin) {
|
|
3309
|
-
config.admin = { ...config.admin, ...dynamic.admin };
|
|
3310
|
-
}
|
|
3311
|
-
}
|
|
3312
|
-
const user = c.get("user");
|
|
3313
|
-
const accessArgs = { user, req: c.req, doc: null };
|
|
3314
|
-
const serializeAccess = async (access) => {
|
|
3315
|
-
if (typeof access === "string") return access;
|
|
3316
|
-
if (typeof access === "boolean") return access;
|
|
3317
|
-
return checkAccess(access, accessArgs);
|
|
3318
|
-
};
|
|
3319
|
-
const filteredCollections = await Promise.all(collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
|
|
3320
|
-
slug: col.slug,
|
|
3321
|
-
labels: col.labels,
|
|
3322
|
-
access: {
|
|
3323
|
-
read: await serializeAccess(col.access?.read),
|
|
3324
|
-
create: await serializeAccess(col.access?.create),
|
|
3325
|
-
update: await serializeAccess(col.access?.update),
|
|
3326
|
-
delete: await serializeAccess(col.access?.delete)
|
|
3327
|
-
},
|
|
3328
|
-
fields: await Promise.all(col.fields.map(serializeFieldForApi).map(async (f) => ({
|
|
3329
|
-
name: f.name,
|
|
3330
|
-
type: f.type,
|
|
3331
|
-
label: f.label,
|
|
3332
|
-
required: f.required,
|
|
3333
|
-
defaultValue: f.defaultValue,
|
|
3334
|
-
options: f.options,
|
|
3335
|
-
relationTo: f.relationTo,
|
|
3336
|
-
hasMany: f.hasMany,
|
|
3337
|
-
fields: f.fields,
|
|
3338
|
-
blocks: f.blocks,
|
|
3339
|
-
admin: f.admin,
|
|
3340
|
-
access: {
|
|
3341
|
-
read: await serializeAccess(f.access?.read),
|
|
3342
|
-
update: await serializeAccess(f.access?.update)
|
|
3343
|
-
}
|
|
3344
|
-
}))),
|
|
3345
|
-
upload: !!col.upload,
|
|
3346
|
-
auth: !!col.auth,
|
|
3347
|
-
admin: col.admin
|
|
3348
|
-
})));
|
|
3349
|
-
const filteredGlobals = await Promise.all(globals.filter((glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId).map(async (glb) => ({
|
|
3350
|
-
slug: glb.slug,
|
|
3351
|
-
label: glb.label,
|
|
3352
|
-
access: {
|
|
3353
|
-
read: await serializeAccess(glb.access?.read),
|
|
3354
|
-
update: await serializeAccess(glb.access?.update)
|
|
3355
|
-
},
|
|
3356
|
-
fields: await Promise.all(glb.fields.map(serializeFieldForApi).map(async (f) => ({
|
|
3357
|
-
name: f.name,
|
|
3358
|
-
type: f.type,
|
|
3359
|
-
label: f.label,
|
|
3360
|
-
required: f.required,
|
|
3361
|
-
defaultValue: f.defaultValue,
|
|
3362
|
-
options: f.options,
|
|
3363
|
-
relationTo: f.relationTo,
|
|
3364
|
-
hasMany: f.hasMany,
|
|
3365
|
-
fields: f.fields,
|
|
3366
|
-
blocks: f.blocks,
|
|
3367
|
-
admin: f.admin,
|
|
3368
|
-
access: {
|
|
3369
|
-
read: await serializeAccess(f.access?.read),
|
|
3370
|
-
update: await serializeAccess(f.access?.update)
|
|
3371
|
-
}
|
|
3372
|
-
}))),
|
|
3373
|
-
admin: glb.admin
|
|
3374
|
-
})));
|
|
3375
|
-
return c.json({
|
|
3376
|
-
collections: filteredCollections,
|
|
3377
|
-
globals: filteredGlobals,
|
|
3378
|
-
admin: config.admin || {}
|
|
3379
|
-
});
|
|
3380
|
-
});
|
|
3381
|
-
app.get("/api/dyrected/options/:collection/:field", optionalAuth(), async (c) => {
|
|
3382
|
-
const { collection: colSlug, field: fieldName } = c.req.param();
|
|
3383
|
-
const siteId = c.req.header("X-Site-Id");
|
|
3384
|
-
let collections = [...config.collections];
|
|
3385
|
-
if (siteId && config.onSchemaFetch) {
|
|
3386
|
-
const dynamic = await config.onSchemaFetch(siteId);
|
|
3387
|
-
if (dynamic.collections) collections = [...collections, ...dynamic.collections];
|
|
3388
|
-
}
|
|
3389
|
-
const user = c.get("user");
|
|
3390
|
-
let collection = collections.find((col) => col.slug === colSlug);
|
|
3391
|
-
let field;
|
|
3392
|
-
if (collection) {
|
|
3393
|
-
const accessExpr = collection.access?.read;
|
|
3394
|
-
if (accessExpr !== void 0 && accessExpr !== null) {
|
|
3395
|
-
const accessArgs = { user, req: c.req, doc: null };
|
|
3396
|
-
const allowed = await checkAccess(accessExpr, accessArgs);
|
|
3397
|
-
if (!allowed) {
|
|
3398
|
-
return c.json({ error: true, message: `Access denied: read on ${colSlug}` }, 403);
|
|
3399
|
-
}
|
|
3400
|
-
}
|
|
3401
|
-
field = collection.fields.find((f) => f.name === fieldName);
|
|
3402
|
-
} else {
|
|
3403
|
-
let globals = [...config.globals];
|
|
3404
|
-
if (siteId && config.onSchemaFetch) {
|
|
3405
|
-
const dynamic = await config.onSchemaFetch(siteId);
|
|
3406
|
-
if (dynamic.globals) globals = [...globals, ...dynamic.globals];
|
|
3407
|
-
}
|
|
3408
|
-
const glb = globals.find((g) => g.slug === colSlug);
|
|
3409
|
-
if (!glb) {
|
|
3410
|
-
return c.json({ error: true, message: `${colSlug} not found as collection or global` }, 404);
|
|
3411
|
-
}
|
|
3412
|
-
const accessExpr = glb.access?.read;
|
|
3413
|
-
if (accessExpr !== void 0 && accessExpr !== null) {
|
|
3414
|
-
const accessArgs = { user, req: c.req, doc: null };
|
|
3415
|
-
const allowed = await checkAccess(accessExpr, accessArgs);
|
|
3416
|
-
if (!allowed) {
|
|
3417
|
-
return c.json({ error: true, message: `Access denied: read on global ${colSlug}` }, 403);
|
|
3418
|
-
}
|
|
3419
|
-
}
|
|
3420
|
-
field = glb.fields.find((f) => f.name === fieldName);
|
|
3421
|
-
}
|
|
3422
|
-
if (!field) {
|
|
3423
|
-
return c.json({ error: true, message: `Field ${fieldName} not found in ${colSlug}` }, 404);
|
|
3424
|
-
}
|
|
3425
|
-
let resolver;
|
|
3426
|
-
if (typeof field.options === "function") {
|
|
3427
|
-
resolver = field.options;
|
|
3428
|
-
} else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
|
|
3429
|
-
resolver = field.options.resolve;
|
|
3430
|
-
}
|
|
3431
|
-
if (!resolver) {
|
|
3432
|
-
return c.json({ error: true, message: `Field ${fieldName} in ${colSlug} is not dynamic` }, 400);
|
|
3433
|
-
}
|
|
3434
|
-
try {
|
|
3435
|
-
const db = c.get("db") || config.db;
|
|
3436
|
-
const queryParams = c.req.query();
|
|
3437
|
-
const reqContext = {
|
|
3438
|
-
query: queryParams,
|
|
3439
|
-
headers: c.req.header(),
|
|
3440
|
-
raw: c.req.raw
|
|
3441
|
-
};
|
|
3442
|
-
const result = await resolver({
|
|
3443
|
-
db,
|
|
3444
|
-
user,
|
|
3445
|
-
req: reqContext
|
|
3446
|
-
});
|
|
3447
|
-
return c.json(result);
|
|
3448
|
-
} catch (err) {
|
|
3449
|
-
console.error(`[dyrected/core] Failed to resolve dynamic options for field ${fieldName}:`, err);
|
|
3450
|
-
return c.json({ error: true, message: err.message || "Failed to resolve dynamic options" }, 500);
|
|
3451
|
-
}
|
|
3452
|
-
});
|
|
3453
|
-
app.get("/api/openapi.json", (c) => {
|
|
3454
|
-
return c.json(generateOpenApi(config));
|
|
3455
|
-
});
|
|
3456
|
-
app.get("/api/docs", (c) => {
|
|
3457
|
-
return c.html(getSwaggerHtml());
|
|
3458
|
-
});
|
|
3459
|
-
app.get("/api/media/:filename{.+$}", async (c) => {
|
|
3460
|
-
const mediaController = new MediaController("media");
|
|
3461
|
-
return mediaController.serve(c);
|
|
3462
|
-
});
|
|
3463
|
-
app.get("/media/:filename{.+$}", async (c) => {
|
|
3464
|
-
const mediaController = new MediaController("media");
|
|
3465
|
-
return mediaController.serve(c);
|
|
3466
|
-
});
|
|
3467
|
-
if (config.storage) {
|
|
3468
|
-
const uploadCollections = config.collections.filter((c) => c.upload);
|
|
3469
|
-
for (const col of uploadCollections) {
|
|
3470
|
-
const mediaController = new MediaController(col.slug);
|
|
3471
|
-
const prefix = `/api/collections/${col.slug}`;
|
|
3472
|
-
app.get(`${prefix}/media`, accessGate(col, "read"), (c) => mediaController.find(c));
|
|
3473
|
-
app.get(`${prefix}/media/:filename{.+$}`, (c) => mediaController.serve(c));
|
|
3474
|
-
app.post(`${prefix}/media`, accessGate(col, "create"), (c) => mediaController.upload(c));
|
|
3475
|
-
app.delete(`${prefix}/media/:id`, accessGate(col, "delete"), (c) => mediaController.delete(c));
|
|
3476
|
-
}
|
|
3477
|
-
}
|
|
3478
|
-
for (const collection of config.collections) {
|
|
3479
|
-
if (!collection.auth) continue;
|
|
3480
|
-
const path = `/api/collections/${collection.slug}`;
|
|
3481
|
-
const authController = new AuthController(collection);
|
|
3482
|
-
app.post(`${path}/login`, (c) => authController.login(c));
|
|
3483
|
-
app.post(`${path}/logout`, (c) => authController.logout(c));
|
|
3484
|
-
app.get(`${path}/init`, (c) => authController.init(c));
|
|
3485
|
-
app.post(`${path}/first-user`, (c) => authController.registerFirstUser(c));
|
|
3486
|
-
app.get(`${path}/me`, requireAuth(), (c) => authController.me(c));
|
|
3487
|
-
app.post(`${path}/refresh-token`, requireAuth(), (c) => authController.refreshToken(c));
|
|
3488
|
-
app.post(`${path}/forgot-password`, (c) => authController.forgotPassword(c));
|
|
3489
|
-
app.post(`${path}/reset-password`, (c) => authController.resetPassword(c));
|
|
3490
|
-
app.post(`${path}/invite`, requireAuth(), (c) => authController.invite(c));
|
|
3491
|
-
app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
|
|
3492
|
-
}
|
|
3493
|
-
for (const collection of config.collections) {
|
|
3494
|
-
const path = `/api/collections/${collection.slug}`;
|
|
3495
|
-
const controller = new CollectionController(collection);
|
|
3496
|
-
app.get(path, accessGate(collection, "read"), (c) => controller.find(c));
|
|
3497
|
-
app.post(path, accessGate(collection, "create"), (c) => controller.create(c));
|
|
3498
|
-
app.post(`${path}/media`, accessGate(collection, "create"), (c) => controller.create(c));
|
|
3499
|
-
app.delete(`${path}/delete-many`, accessGate(collection, "delete"), (c) => controller.deleteMany(c));
|
|
3500
|
-
app.get(`${path}/:id`, accessGate(collection, "read"), (c) => controller.findOne(c));
|
|
3501
|
-
app.patch(`${path}/:id`, accessGate(collection, "update"), (c) => controller.update(c));
|
|
3502
|
-
app.delete(`${path}/:id`, accessGate(collection, "delete"), (c) => controller.delete(c));
|
|
3503
|
-
app.post(`${path}/seed`, (c) => controller.seed(c));
|
|
3504
|
-
if (collection.auth) {
|
|
3505
|
-
app.post(`${path}/:id/change-password`, requireAuth(), (c) => controller.changePassword(c));
|
|
3506
|
-
}
|
|
3507
|
-
}
|
|
3508
|
-
for (const global of config.globals) {
|
|
3509
|
-
const path = `/api/globals/${global.slug}`;
|
|
3510
|
-
const controller = new GlobalController(global);
|
|
3511
|
-
app.get(path, accessGate(global, "read"), (c) => controller.get(c));
|
|
3512
|
-
app.patch(path, accessGate(global, "update"), (c) => controller.update(c));
|
|
3513
|
-
app.post(`${path}/seed`, (c) => controller.seed(c));
|
|
3514
|
-
}
|
|
3515
|
-
const previewController = new PreviewController();
|
|
3516
|
-
app.post("/api/preview-token", requireAuth(), (c) => previewController.createToken(c));
|
|
3517
|
-
app.get("/api/preview-data", (c) => previewController.getData(c));
|
|
3518
|
-
app.all("/api/collections/:slug/:id?", async (c) => {
|
|
3519
|
-
const slug = c.req.param("slug");
|
|
3520
|
-
const id = c.req.param("id");
|
|
3521
|
-
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
3522
|
-
const config2 = c.get("config");
|
|
3523
|
-
if (config2.collections.some((col) => col.slug === slug)) {
|
|
3524
|
-
return c.json({ message: "Method Not Allowed" }, 405);
|
|
3525
|
-
}
|
|
3526
|
-
if (config2.onSchemaFetch && siteId) {
|
|
3527
|
-
const dynamic = await config2.onSchemaFetch(siteId);
|
|
3528
|
-
let collection = dynamic.collections?.find((col) => col.slug === slug);
|
|
3529
|
-
if (!collection && slug === "media") {
|
|
3530
|
-
collection = {
|
|
3531
|
-
slug: "media",
|
|
3532
|
-
labels: { singular: "Media", plural: "Media" },
|
|
3533
|
-
upload: true,
|
|
3534
|
-
fields: []
|
|
3535
|
-
};
|
|
3536
|
-
}
|
|
3537
|
-
if (collection) {
|
|
3538
|
-
if (collection.auth && id) {
|
|
3539
|
-
const authController = new AuthController(collection);
|
|
3540
|
-
const method2 = c.req.method;
|
|
3541
|
-
if (method2 === "POST" && id === "login") return authController.login(c);
|
|
3542
|
-
if (method2 === "POST" && id === "logout") return authController.logout(c);
|
|
3543
|
-
if (method2 === "GET" && id === "me") return authController.me(c);
|
|
3544
|
-
if (method2 === "POST" && id === "refresh-token") return authController.refreshToken(c);
|
|
3545
|
-
if (method2 === "POST" && id === "forgot-password") return authController.forgotPassword(c);
|
|
3546
|
-
if (method2 === "POST" && id === "reset-password") return authController.resetPassword(c);
|
|
3547
|
-
}
|
|
3548
|
-
const controller = new CollectionController(collection);
|
|
3549
|
-
const method = c.req.method;
|
|
3550
|
-
if (id) {
|
|
3551
|
-
if (method === "GET") return controller.findOne(c);
|
|
3552
|
-
if (method === "PATCH") return controller.update(c);
|
|
3553
|
-
if (method === "DELETE" && id === "delete-many") return controller.deleteMany(c);
|
|
3554
|
-
if (method === "DELETE") return controller.delete(c);
|
|
3555
|
-
if (method === "POST" && id === "media") return controller.create(c);
|
|
3556
|
-
if (method === "POST" && id === "seed") return controller.seed(c);
|
|
3557
|
-
} else {
|
|
3558
|
-
if (method === "GET") return controller.find(c);
|
|
3559
|
-
if (method === "POST") return controller.create(c);
|
|
3560
|
-
}
|
|
3561
|
-
}
|
|
3562
|
-
}
|
|
3563
|
-
return c.json({ message: `Collection "${slug}" not found` }, 404);
|
|
3564
|
-
});
|
|
3565
|
-
app.all("/api/globals/:slug", async (c) => {
|
|
3566
|
-
const slug = c.req.param("slug");
|
|
3567
|
-
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
3568
|
-
const config2 = c.get("config");
|
|
3569
|
-
if (config2.globals.some((glb) => glb.slug === slug)) {
|
|
3570
|
-
return c.json({ message: "Method Not Allowed" }, 405);
|
|
3571
|
-
}
|
|
3572
|
-
if (config2.onSchemaFetch && siteId) {
|
|
3573
|
-
const dynamic = await config2.onSchemaFetch(siteId);
|
|
3574
|
-
const global = dynamic.globals?.find((glb) => glb.slug === slug);
|
|
3575
|
-
if (global) {
|
|
3576
|
-
const controller = new GlobalController(global);
|
|
3577
|
-
if (c.req.method === "GET") return controller.get(c);
|
|
3578
|
-
if (c.req.method === "PATCH") return controller.update(c);
|
|
3579
|
-
}
|
|
3580
|
-
}
|
|
3581
|
-
return c.json({ message: `Global "${slug}" not found` }, 404);
|
|
3582
|
-
});
|
|
3583
|
-
}
|
|
3584
|
-
|
|
3585
|
-
// src/app.ts
|
|
3586
|
-
async function createDyrectedApp(rawConfig) {
|
|
3587
|
-
const config = normalizeConfig(rawConfig);
|
|
3588
|
-
const app = new import_hono.Hono();
|
|
3589
|
-
if (config.db?.sync) {
|
|
3590
|
-
await config.db.sync(config.collections, config.globals);
|
|
3591
|
-
}
|
|
3592
|
-
app.use("*", (0, import_request_id.requestId)());
|
|
3593
|
-
app.use("*", optionalAuth());
|
|
3594
|
-
app.use("*", async (c, next) => {
|
|
3595
|
-
const start = Date.now();
|
|
3596
|
-
await next();
|
|
3597
|
-
const ms = Date.now() - start;
|
|
3598
|
-
console.log(`[dyrected/api] ${c.req.method} ${c.req.path} ${c.res.status} - ${ms}ms`);
|
|
3599
|
-
});
|
|
3600
|
-
app.use("*", (0, import_cors.cors)());
|
|
3601
|
-
app.use("*", async (c, next) => {
|
|
3602
|
-
c.set("config", config);
|
|
3603
|
-
if (!c.get("siteId")) {
|
|
3604
|
-
c.set("siteId", "default");
|
|
3605
|
-
}
|
|
3606
|
-
await next();
|
|
3607
|
-
});
|
|
3608
|
-
app.get("/health", (c) => c.json({ status: "ok", version: "0.0.1" }));
|
|
3609
|
-
app.get("/routes", (c) => {
|
|
3610
|
-
const routes = app.routes.map((r) => ({ method: r.method, path: r.path }));
|
|
3611
|
-
return c.json({ routes });
|
|
3612
|
-
});
|
|
3613
|
-
app.onError((err, c) => {
|
|
3614
|
-
console.error(`[dyrected/core] Uncaught Error:`, err);
|
|
3615
|
-
return c.json({
|
|
3616
|
-
message: err.message || "Internal Server Error",
|
|
3617
|
-
stack: process.env.NODE_ENV === "development" ? err.stack : void 0
|
|
3618
|
-
}, 500);
|
|
3619
|
-
});
|
|
3620
|
-
registerRoutes(app, config);
|
|
3621
|
-
return app;
|
|
3622
|
-
}
|
|
3623
|
-
|
|
3624
1320
|
// src/index.ts
|
|
3625
1321
|
function defineCollection(config) {
|
|
3626
1322
|
return config;
|
|
@@ -3633,7 +1329,6 @@ function defineConfig(config) {
|
|
|
3633
1329
|
}
|
|
3634
1330
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3635
1331
|
0 && (module.exports = {
|
|
3636
|
-
createDyrectedApp,
|
|
3637
1332
|
defineCollection,
|
|
3638
1333
|
defineConfig,
|
|
3639
1334
|
defineGlobal,
|