@dyrected/core 2.5.61 → 2.5.62
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/app-config-BcuZIpvL.d.cts +1951 -0
- package/dist/app-config-BcuZIpvL.d.ts +1951 -0
- package/dist/app-config-Bm9-2Am6.d.cts +1875 -0
- package/dist/app-config-Bm9-2Am6.d.ts +1875 -0
- package/dist/app-config-CJAGGPrk.d.cts +1900 -0
- package/dist/app-config-CJAGGPrk.d.ts +1900 -0
- package/dist/app-config-CiEDJm0e.d.cts +1912 -0
- package/dist/app-config-CiEDJm0e.d.ts +1912 -0
- package/dist/app-config-DCDh8Gbx.d.cts +1926 -0
- package/dist/app-config-DCDh8Gbx.d.ts +1926 -0
- package/dist/app-config-Dv5XACR4.d.cts +2065 -0
- package/dist/app-config-Dv5XACR4.d.ts +2065 -0
- package/dist/app-config-_kkj71CB.d.cts +2010 -0
- package/dist/app-config-_kkj71CB.d.ts +2010 -0
- package/dist/app-config-tITj_0sn.d.cts +1926 -0
- package/dist/app-config-tITj_0sn.d.ts +1926 -0
- package/dist/chunk-57FNM42D.js +2392 -0
- package/dist/chunk-BAMX7YUC.js +1815 -0
- package/dist/chunk-BQV3QW3Y.js +2588 -0
- package/dist/chunk-EH3MJGB5.js +2571 -0
- package/dist/chunk-M3HKRN7E.js +1665 -0
- package/dist/chunk-MQZH7RQC.js +1667 -0
- package/dist/chunk-VCYYBN5J.js +1873 -0
- package/dist/chunk-WVD7PORQ.js +1672 -0
- package/dist/chunk-XZLIBQSO.js +2397 -0
- package/dist/index.cjs +336 -39
- package/dist/index.d.cts +34 -4
- package/dist/index.d.ts +34 -4
- package/dist/index.js +3 -1
- package/dist/server.cjs +2934 -708
- package/dist/server.d.cts +124 -12
- package/dist/server.d.ts +124 -12
- package/dist/server.js +2059 -697
- package/package.json +10 -1
package/dist/server.cjs
CHANGED
|
@@ -171,12 +171,16 @@ var PopulationService = class {
|
|
|
171
171
|
return data;
|
|
172
172
|
}
|
|
173
173
|
if (Array.isArray(data)) {
|
|
174
|
-
return Promise.all(
|
|
174
|
+
return Promise.all(
|
|
175
|
+
data.map((item) => this.populate({ ...args, data: item }))
|
|
176
|
+
);
|
|
175
177
|
}
|
|
176
178
|
const populatedDoc = { ...data };
|
|
177
179
|
for (const field of fields) {
|
|
178
180
|
if (field.type === "join" && field.collection && field.on && currentDepth === 0) {
|
|
179
|
-
const targetCollection = this.collections.find(
|
|
181
|
+
const targetCollection = this.collections.find(
|
|
182
|
+
(c) => c.slug === field.collection
|
|
183
|
+
);
|
|
180
184
|
if (targetCollection) {
|
|
181
185
|
const docId = populatedDoc.id;
|
|
182
186
|
if (docId) {
|
|
@@ -188,7 +192,9 @@ var PopulationService = class {
|
|
|
188
192
|
limit: joinLimit
|
|
189
193
|
});
|
|
190
194
|
const populatedDocs = await this.populate({
|
|
191
|
-
data: result.docs.map(
|
|
195
|
+
data: result.docs.map(
|
|
196
|
+
(doc) => DefaultsService.apply(targetCollection.fields, doc)
|
|
197
|
+
),
|
|
192
198
|
fields: targetCollection.fields,
|
|
193
199
|
currentDepth: 1,
|
|
194
200
|
maxDepth
|
|
@@ -203,14 +209,21 @@ var PopulationService = class {
|
|
|
203
209
|
continue;
|
|
204
210
|
}
|
|
205
211
|
if (field.type === "row" && field.fields) {
|
|
206
|
-
const rowPopulated = await this.populate({
|
|
212
|
+
const rowPopulated = await this.populate({
|
|
213
|
+
data,
|
|
214
|
+
fields: field.fields,
|
|
215
|
+
currentDepth,
|
|
216
|
+
maxDepth
|
|
217
|
+
});
|
|
207
218
|
Object.assign(populatedDoc, rowPopulated);
|
|
208
219
|
continue;
|
|
209
220
|
}
|
|
210
221
|
if (!field.name) continue;
|
|
211
222
|
const value = populatedDoc[field.name];
|
|
212
223
|
if (field.type === "relationship" && field.relationTo && value) {
|
|
213
|
-
const relatedCollection = this.collections.find(
|
|
224
|
+
const relatedCollection = this.collections.find(
|
|
225
|
+
(c) => c.slug === field.relationTo
|
|
226
|
+
);
|
|
214
227
|
if (!relatedCollection) continue;
|
|
215
228
|
if (Array.isArray(value)) {
|
|
216
229
|
populatedDoc[field.name] = await Promise.all(
|
|
@@ -218,10 +231,16 @@ var PopulationService = class {
|
|
|
218
231
|
if (!id) return id;
|
|
219
232
|
let doc = id;
|
|
220
233
|
if (typeof id === "string") {
|
|
221
|
-
doc = await this.db.findOne({
|
|
234
|
+
doc = await this.db.findOne({
|
|
235
|
+
collection: field.relationTo,
|
|
236
|
+
id
|
|
237
|
+
});
|
|
222
238
|
}
|
|
223
239
|
if (!doc || typeof doc !== "object") return id;
|
|
224
|
-
const docWithDefaults = DefaultsService.apply(
|
|
240
|
+
const docWithDefaults = DefaultsService.apply(
|
|
241
|
+
relatedCollection.fields,
|
|
242
|
+
doc
|
|
243
|
+
);
|
|
225
244
|
return this.populate({
|
|
226
245
|
data: docWithDefaults,
|
|
227
246
|
fields: relatedCollection.fields,
|
|
@@ -233,10 +252,16 @@ var PopulationService = class {
|
|
|
233
252
|
} else if (value) {
|
|
234
253
|
let doc = value;
|
|
235
254
|
if (typeof value === "string") {
|
|
236
|
-
doc = await this.db.findOne({
|
|
255
|
+
doc = await this.db.findOne({
|
|
256
|
+
collection: field.relationTo,
|
|
257
|
+
id: value
|
|
258
|
+
});
|
|
237
259
|
}
|
|
238
260
|
if (doc && typeof doc === "object") {
|
|
239
|
-
const docWithDefaults = DefaultsService.apply(
|
|
261
|
+
const docWithDefaults = DefaultsService.apply(
|
|
262
|
+
relatedCollection.fields,
|
|
263
|
+
doc
|
|
264
|
+
);
|
|
240
265
|
populatedDoc[field.name] = await this.populate({
|
|
241
266
|
data: docWithDefaults,
|
|
242
267
|
fields: relatedCollection.fields,
|
|
@@ -247,11 +272,19 @@ var PopulationService = class {
|
|
|
247
272
|
}
|
|
248
273
|
}
|
|
249
274
|
if (field.type === "url" && value && typeof value === "object" && value.type === "internal" && value.relationTo && value.value) {
|
|
250
|
-
const relatedCollection = this.collections.find(
|
|
275
|
+
const relatedCollection = this.collections.find(
|
|
276
|
+
(c) => c.slug === value.relationTo
|
|
277
|
+
);
|
|
251
278
|
if (relatedCollection) {
|
|
252
|
-
const doc = await this.db.findOne({
|
|
279
|
+
const doc = await this.db.findOne({
|
|
280
|
+
collection: value.relationTo,
|
|
281
|
+
id: value.value
|
|
282
|
+
});
|
|
253
283
|
if (doc && typeof doc === "object") {
|
|
254
|
-
const docWithDefaults = DefaultsService.apply(
|
|
284
|
+
const docWithDefaults = DefaultsService.apply(
|
|
285
|
+
relatedCollection.fields,
|
|
286
|
+
doc
|
|
287
|
+
);
|
|
255
288
|
const populatedDocValue = await this.populate({
|
|
256
289
|
data: docWithDefaults,
|
|
257
290
|
fields: relatedCollection.fields,
|
|
@@ -269,15 +302,27 @@ var PopulationService = class {
|
|
|
269
302
|
}
|
|
270
303
|
} else if (typeof previewUrlPattern === "string") {
|
|
271
304
|
if (previewUrlPattern.includes("{{")) {
|
|
272
|
-
resolvedUrl = previewUrlPattern.replace(
|
|
305
|
+
resolvedUrl = previewUrlPattern.replace(
|
|
306
|
+
/{{(.*?)}}/g,
|
|
307
|
+
(_, key) => String(docWithDefaults[key.trim()] || "")
|
|
308
|
+
);
|
|
273
309
|
} else if (previewUrlPattern.includes("+") || previewUrlPattern.includes("?") || previewUrlPattern.includes("==") || previewUrlPattern.includes("siteUrl")) {
|
|
274
310
|
try {
|
|
275
|
-
resolvedUrl = import_jexl.default.evalSync(
|
|
276
|
-
|
|
277
|
-
|
|
311
|
+
resolvedUrl = import_jexl.default.evalSync(
|
|
312
|
+
previewUrlPattern,
|
|
313
|
+
docWithDefaults
|
|
314
|
+
);
|
|
315
|
+
} catch (_err) {
|
|
316
|
+
resolvedUrl = previewUrlPattern.replace(
|
|
317
|
+
/:([a-zA-Z0-9_]+)/g,
|
|
318
|
+
(_, key) => String(docWithDefaults[key] || "")
|
|
319
|
+
);
|
|
278
320
|
}
|
|
279
321
|
} else {
|
|
280
|
-
resolvedUrl = previewUrlPattern.replace(
|
|
322
|
+
resolvedUrl = previewUrlPattern.replace(
|
|
323
|
+
/:([a-zA-Z0-9_]+)/g,
|
|
324
|
+
(_, key) => String(docWithDefaults[key] || "")
|
|
325
|
+
);
|
|
281
326
|
}
|
|
282
327
|
}
|
|
283
328
|
}
|
|
@@ -301,7 +346,9 @@ var PopulationService = class {
|
|
|
301
346
|
if (field.type === "blocks" && field.blocks && Array.isArray(value)) {
|
|
302
347
|
populatedDoc[field.name] = await Promise.all(
|
|
303
348
|
value.map(async (blockData) => {
|
|
304
|
-
const blockConfig = field.blocks.find(
|
|
349
|
+
const blockConfig = field.blocks.find(
|
|
350
|
+
(b) => b.slug === blockData.blockType
|
|
351
|
+
);
|
|
305
352
|
if (!blockConfig) return blockData;
|
|
306
353
|
return this.populate({
|
|
307
354
|
data: blockData,
|
|
@@ -333,13 +380,494 @@ var PopulationService = class {
|
|
|
333
380
|
}
|
|
334
381
|
};
|
|
335
382
|
|
|
383
|
+
// src/observability.ts
|
|
384
|
+
var import_api = require("@opentelemetry/api");
|
|
385
|
+
var import_exporter_metrics_otlp_http = require("@opentelemetry/exporter-metrics-otlp-http");
|
|
386
|
+
var import_exporter_prometheus = require("@opentelemetry/exporter-prometheus");
|
|
387
|
+
var import_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
|
|
388
|
+
var import_resources = require("@opentelemetry/resources");
|
|
389
|
+
var import_sdk_metrics = require("@opentelemetry/sdk-metrics");
|
|
390
|
+
var import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
|
|
391
|
+
var import_pino = __toESM(require("pino"), 1);
|
|
392
|
+
var import_pino_pretty = __toESM(require("pino-pretty"), 1);
|
|
393
|
+
var import_node_stream = require("stream");
|
|
394
|
+
var DEFAULT_REDACT_HEADERS = ["authorization", "cookie", "set-cookie", "x-api-key"];
|
|
395
|
+
var DEFAULT_REDACT_PATHS = [
|
|
396
|
+
"password",
|
|
397
|
+
"currentPassword",
|
|
398
|
+
"newPassword",
|
|
399
|
+
"confirmPassword",
|
|
400
|
+
"token",
|
|
401
|
+
"refreshToken",
|
|
402
|
+
"accessToken",
|
|
403
|
+
"secret",
|
|
404
|
+
"apiKey",
|
|
405
|
+
"inviteToken",
|
|
406
|
+
"resetToken"
|
|
407
|
+
];
|
|
408
|
+
var DEFAULT_MAX_BODY_BYTES = 8192;
|
|
409
|
+
var DEFAULT_SUCCESS_SAMPLE_RATE = 0.1;
|
|
410
|
+
var DEFAULT_BODY_SAMPLE_RATE = 0.02;
|
|
411
|
+
var REDACTED_VALUE = "[REDACTED]";
|
|
412
|
+
var runtimeByConfig = /* @__PURE__ */ new WeakMap();
|
|
413
|
+
var fallbackLogger = (0, import_pino.default)({
|
|
414
|
+
name: "dyrected",
|
|
415
|
+
enabled: process.env.DISABLE_LOGGING !== "true",
|
|
416
|
+
timestamp: import_pino.stdTimeFunctions.isoTime
|
|
417
|
+
});
|
|
418
|
+
function resolveObservabilityConfig(config) {
|
|
419
|
+
const requestLogging = config?.requestLogging;
|
|
420
|
+
const sampling = config?.sampling;
|
|
421
|
+
const tracing = config?.tracing;
|
|
422
|
+
const metrics = config?.metrics;
|
|
423
|
+
return {
|
|
424
|
+
requestLogging: {
|
|
425
|
+
enabled: requestLogging?.enabled ?? true,
|
|
426
|
+
logBodies: requestLogging?.logBodies ?? false,
|
|
427
|
+
maxBodyBytes: requestLogging?.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES,
|
|
428
|
+
redactPaths: uniqueValues([
|
|
429
|
+
...DEFAULT_REDACT_PATHS,
|
|
430
|
+
...requestLogging?.redactPaths ?? []
|
|
431
|
+
]),
|
|
432
|
+
includeHeaders: normalizeHeaderNames(requestLogging?.includeHeaders ?? []),
|
|
433
|
+
redactHeaders: uniqueValues(
|
|
434
|
+
normalizeHeaderNames([
|
|
435
|
+
...DEFAULT_REDACT_HEADERS,
|
|
436
|
+
...requestLogging?.redactHeaders ?? []
|
|
437
|
+
])
|
|
438
|
+
)
|
|
439
|
+
},
|
|
440
|
+
sampling: {
|
|
441
|
+
successRate: clampRate(sampling?.successRate ?? DEFAULT_SUCCESS_SAMPLE_RATE),
|
|
442
|
+
traceSuccessRate: clampRate(
|
|
443
|
+
sampling?.traceSuccessRate ?? sampling?.successRate ?? DEFAULT_SUCCESS_SAMPLE_RATE
|
|
444
|
+
),
|
|
445
|
+
bodySuccessRate: clampRate(
|
|
446
|
+
sampling?.bodySuccessRate ?? Math.min(DEFAULT_BODY_SAMPLE_RATE, sampling?.successRate ?? DEFAULT_SUCCESS_SAMPLE_RATE)
|
|
447
|
+
),
|
|
448
|
+
alwaysKeep4xx: sampling?.alwaysKeep4xx ?? true,
|
|
449
|
+
alwaysKeep5xx: sampling?.alwaysKeep5xx ?? true
|
|
450
|
+
},
|
|
451
|
+
tracing: {
|
|
452
|
+
enabled: tracing?.enabled ?? false,
|
|
453
|
+
serviceName: tracing?.serviceName ?? "dyrected",
|
|
454
|
+
exporter: tracing?.exporter ?? "none",
|
|
455
|
+
headers: tracing?.headers ?? {},
|
|
456
|
+
endpoint: tracing?.endpoint
|
|
457
|
+
},
|
|
458
|
+
metrics: {
|
|
459
|
+
enabled: metrics?.enabled ?? false,
|
|
460
|
+
exporter: metrics?.exporter ?? "none",
|
|
461
|
+
endpoint: metrics?.endpoint,
|
|
462
|
+
path: metrics?.path ?? "/metrics"
|
|
463
|
+
},
|
|
464
|
+
transports: {
|
|
465
|
+
targets: config?.transports?.targets && config.transports.targets.length > 0 ? config.transports.targets : [{ type: "stdout" }]
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function createObservabilityRuntime(config) {
|
|
470
|
+
const resolved = resolveObservabilityConfig(config.observability);
|
|
471
|
+
const logger = createRootLogger(config.logger, resolved);
|
|
472
|
+
const shutdownTasks = [];
|
|
473
|
+
const meterRuntime = resolved.metrics.enabled || resolved.metrics.exporter === "prometheus" ? createMeterProvider(resolved) : void 0;
|
|
474
|
+
const tracerProvider = resolved.tracing.enabled ? createTracerProvider(resolved) : void 0;
|
|
475
|
+
const tracer = tracerProvider?.getTracer("dyrected");
|
|
476
|
+
const meter = meterRuntime?.provider.getMeter("dyrected");
|
|
477
|
+
const metrics = meter ? {
|
|
478
|
+
requestCount: meter.createCounter("dyrected_http_requests_total", {
|
|
479
|
+
description: "Total HTTP requests served by Dyrected"
|
|
480
|
+
}),
|
|
481
|
+
authFailureCount: meter.createCounter("dyrected_auth_failures_total", {
|
|
482
|
+
description: "Authentication failures"
|
|
483
|
+
}),
|
|
484
|
+
uncaughtErrorCount: meter.createCounter("dyrected_uncaught_errors_total", {
|
|
485
|
+
description: "Uncaught application errors"
|
|
486
|
+
}),
|
|
487
|
+
auditWriteFailureCount: meter.createCounter("dyrected_audit_write_failures_total", {
|
|
488
|
+
description: "Audit log persistence failures"
|
|
489
|
+
}),
|
|
490
|
+
emailSendFailureCount: meter.createCounter("dyrected_email_send_failures_total", {
|
|
491
|
+
description: "Transactional email delivery failures"
|
|
492
|
+
}),
|
|
493
|
+
workflowHookFailureCount: meter.createCounter("dyrected_workflow_hook_failures_total", {
|
|
494
|
+
description: "Workflow hook failures isolated after successful writes"
|
|
495
|
+
}),
|
|
496
|
+
requestDuration: meter.createHistogram("dyrected_http_request_duration_ms", {
|
|
497
|
+
description: "HTTP request duration in milliseconds",
|
|
498
|
+
unit: "ms"
|
|
499
|
+
})
|
|
500
|
+
} : void 0;
|
|
501
|
+
if (meterRuntime) {
|
|
502
|
+
shutdownTasks.push(() => meterRuntime.provider.shutdown());
|
|
503
|
+
}
|
|
504
|
+
if (tracerProvider) {
|
|
505
|
+
shutdownTasks.push(() => tracerProvider.shutdown());
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
logger,
|
|
509
|
+
config: resolved,
|
|
510
|
+
tracer,
|
|
511
|
+
metrics,
|
|
512
|
+
prometheusExporter: meterRuntime?.prometheusExporter,
|
|
513
|
+
shutdown: async () => {
|
|
514
|
+
for (const task of shutdownTasks) {
|
|
515
|
+
await task();
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
recordAuthFailure: (attributes) => metrics?.authFailureCount.add(1, attributes),
|
|
519
|
+
recordUncaughtError: (attributes) => metrics?.uncaughtErrorCount.add(1, attributes),
|
|
520
|
+
recordAuditWriteFailure: (attributes) => metrics?.auditWriteFailureCount.add(1, attributes),
|
|
521
|
+
recordEmailSendFailure: (attributes) => metrics?.emailSendFailureCount.add(1, attributes),
|
|
522
|
+
recordWorkflowHookFailure: (attributes) => metrics?.workflowHookFailureCount.add(1, attributes)
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function bindObservabilityRuntime(config, runtime) {
|
|
526
|
+
runtimeByConfig.set(config, runtime);
|
|
527
|
+
}
|
|
528
|
+
function getObservabilityRuntime(config) {
|
|
529
|
+
if (!config || typeof config !== "object") return void 0;
|
|
530
|
+
return runtimeByConfig.get(config);
|
|
531
|
+
}
|
|
532
|
+
function createRootLogger(loggerConfig, observability) {
|
|
533
|
+
if (loggerConfig && !("options" in loggerConfig)) {
|
|
534
|
+
return loggerConfig;
|
|
535
|
+
}
|
|
536
|
+
const options = loggerConfig?.options ?? {};
|
|
537
|
+
const enabled = options.enabled ?? process.env.DISABLE_LOGGING !== "true";
|
|
538
|
+
const name = options.name ?? "dyrected";
|
|
539
|
+
if (loggerConfig?.destination) {
|
|
540
|
+
return (0, import_pino.default)(
|
|
541
|
+
{
|
|
542
|
+
...options,
|
|
543
|
+
enabled,
|
|
544
|
+
name,
|
|
545
|
+
timestamp: options.timestamp ?? import_pino.stdTimeFunctions.isoTime
|
|
546
|
+
},
|
|
547
|
+
loggerConfig.destination
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
const streams = buildTransportStreams(observability);
|
|
551
|
+
const destination = streams.length === 1 ? streams[0] : (0, import_pino.multistream)(streams.map((stream) => ({ stream })));
|
|
552
|
+
return (0, import_pino.default)(
|
|
553
|
+
{
|
|
554
|
+
...options,
|
|
555
|
+
enabled,
|
|
556
|
+
name,
|
|
557
|
+
timestamp: options.timestamp ?? import_pino.stdTimeFunctions.isoTime
|
|
558
|
+
},
|
|
559
|
+
destination
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
function buildTransportStreams(observability) {
|
|
563
|
+
const targets = observability.transports.targets;
|
|
564
|
+
if (targets.length === 0) {
|
|
565
|
+
return [defaultLoggerDestination()];
|
|
566
|
+
}
|
|
567
|
+
return targets.map((target) => {
|
|
568
|
+
switch (target.type) {
|
|
569
|
+
case "stdout":
|
|
570
|
+
return defaultLoggerDestination();
|
|
571
|
+
case "stderr":
|
|
572
|
+
return (0, import_pino.destination)(2);
|
|
573
|
+
case "file":
|
|
574
|
+
return (0, import_pino.destination)(target.path);
|
|
575
|
+
case "otlp":
|
|
576
|
+
return new OtlpLogWritable(target.endpoint, target.headers);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
function defaultLoggerDestination() {
|
|
581
|
+
if (process.env.NODE_ENV !== "production") {
|
|
582
|
+
return (0, import_pino_pretty.default)({
|
|
583
|
+
colorize: true,
|
|
584
|
+
ignore: "pid,hostname",
|
|
585
|
+
translateTime: "SYS:HH:MM:ss",
|
|
586
|
+
destination: 1,
|
|
587
|
+
sync: true
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
return (0, import_pino.destination)(1);
|
|
591
|
+
}
|
|
592
|
+
var OtlpLogWritable = class extends import_node_stream.Writable {
|
|
593
|
+
constructor(endpoint, headers = {}) {
|
|
594
|
+
super();
|
|
595
|
+
this.endpoint = endpoint;
|
|
596
|
+
this.headers = headers;
|
|
597
|
+
}
|
|
598
|
+
endpoint;
|
|
599
|
+
headers;
|
|
600
|
+
_write(chunk, _encoding, callback) {
|
|
601
|
+
const payload = chunk.toString();
|
|
602
|
+
void fetch(this.endpoint, {
|
|
603
|
+
method: "POST",
|
|
604
|
+
headers: {
|
|
605
|
+
"content-type": "application/json",
|
|
606
|
+
...this.headers
|
|
607
|
+
},
|
|
608
|
+
body: JSON.stringify({ logs: payload.trim().split("\n").filter(Boolean) })
|
|
609
|
+
}).then(() => callback()).catch(
|
|
610
|
+
(error) => callback(error instanceof Error ? error : new Error(String(error)))
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
function createTracerProvider(observability) {
|
|
615
|
+
const exporter = createTraceExporter(observability);
|
|
616
|
+
const provider = new import_sdk_trace_base.BasicTracerProvider({
|
|
617
|
+
resource: (0, import_resources.resourceFromAttributes)({
|
|
618
|
+
"service.name": observability.tracing.serviceName
|
|
619
|
+
}),
|
|
620
|
+
spanProcessors: exporter ? [new import_sdk_trace_base.BatchSpanProcessor(exporter)] : []
|
|
621
|
+
});
|
|
622
|
+
import_api.trace.setGlobalTracerProvider(provider);
|
|
623
|
+
return provider;
|
|
624
|
+
}
|
|
625
|
+
function createTraceExporter(observability) {
|
|
626
|
+
switch (observability.tracing.exporter) {
|
|
627
|
+
case "console":
|
|
628
|
+
return new import_sdk_trace_base.ConsoleSpanExporter();
|
|
629
|
+
case "otlp":
|
|
630
|
+
return new import_exporter_trace_otlp_http.OTLPTraceExporter({
|
|
631
|
+
url: observability.tracing.endpoint,
|
|
632
|
+
headers: observability.tracing.headers
|
|
633
|
+
});
|
|
634
|
+
default:
|
|
635
|
+
return void 0;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function createMeterProvider(observability) {
|
|
639
|
+
const readers = [];
|
|
640
|
+
let prometheusExporter;
|
|
641
|
+
switch (observability.metrics.exporter) {
|
|
642
|
+
case "otlp":
|
|
643
|
+
readers.push(
|
|
644
|
+
new import_sdk_metrics.PeriodicExportingMetricReader({
|
|
645
|
+
exporter: new import_exporter_metrics_otlp_http.OTLPMetricExporter({
|
|
646
|
+
url: observability.metrics.endpoint
|
|
647
|
+
})
|
|
648
|
+
})
|
|
649
|
+
);
|
|
650
|
+
break;
|
|
651
|
+
case "prometheus":
|
|
652
|
+
prometheusExporter = new import_exporter_prometheus.PrometheusExporter({
|
|
653
|
+
endpoint: observability.metrics.path,
|
|
654
|
+
preventServerStart: true
|
|
655
|
+
});
|
|
656
|
+
readers.push(prometheusExporter);
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
const provider = new import_sdk_metrics.MeterProvider({
|
|
660
|
+
resource: (0, import_resources.resourceFromAttributes)({
|
|
661
|
+
"service.name": observability.tracing.serviceName
|
|
662
|
+
}),
|
|
663
|
+
readers
|
|
664
|
+
});
|
|
665
|
+
return { provider, prometheusExporter };
|
|
666
|
+
}
|
|
667
|
+
function getConfigLogger(config, component) {
|
|
668
|
+
const base = config?.logger && !("options" in config.logger) ? config.logger : fallbackLogger;
|
|
669
|
+
return base.child({ component });
|
|
670
|
+
}
|
|
671
|
+
function getRequestLogger(c, component) {
|
|
672
|
+
const base = c.get("logger") ?? fallbackLogger;
|
|
673
|
+
return component ? base.child({ component }) : base;
|
|
674
|
+
}
|
|
675
|
+
function buildRequestLogger(runtime, vars, requestId2) {
|
|
676
|
+
const trace2 = vars.requestTrace;
|
|
677
|
+
return runtime.logger.child({
|
|
678
|
+
requestId: requestId2,
|
|
679
|
+
siteId: vars.siteId,
|
|
680
|
+
workspaceId: vars.workspaceId,
|
|
681
|
+
traceId: trace2?.traceId,
|
|
682
|
+
spanId: trace2?.spanId
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
function shouldSampleRequest(requestId2, statusCode, sampling, kind) {
|
|
686
|
+
if (statusCode >= 500 && sampling.alwaysKeep5xx) return true;
|
|
687
|
+
if (statusCode >= 400 && sampling.alwaysKeep4xx) return true;
|
|
688
|
+
const rate = kind === "trace" ? sampling.traceSuccessRate : kind === "body" ? sampling.bodySuccessRate : sampling.successRate;
|
|
689
|
+
return deterministicSample(requestId2 ?? "no-request-id", rate);
|
|
690
|
+
}
|
|
691
|
+
function redactHeaders(headers, observability) {
|
|
692
|
+
const includeHeaders = new Set(observability.requestLogging.includeHeaders);
|
|
693
|
+
const redactHeaders2 = new Set(observability.requestLogging.redactHeaders);
|
|
694
|
+
const output = {};
|
|
695
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
696
|
+
const normalizedKey = key.toLowerCase();
|
|
697
|
+
if (includeHeaders.size > 0 && !includeHeaders.has(normalizedKey)) continue;
|
|
698
|
+
output[normalizedKey] = redactHeaders2.has(normalizedKey) ? REDACTED_VALUE : value;
|
|
699
|
+
}
|
|
700
|
+
return output;
|
|
701
|
+
}
|
|
702
|
+
async function captureRequestBody(request, observability) {
|
|
703
|
+
const contentType = request.headers.get("content-type") ?? void 0;
|
|
704
|
+
const contentLength = toNumber(request.headers.get("content-length"));
|
|
705
|
+
const jsonBody = contentType?.includes("application/json");
|
|
706
|
+
if (!jsonBody) {
|
|
707
|
+
return {
|
|
708
|
+
attempted: false,
|
|
709
|
+
contentType,
|
|
710
|
+
contentLength
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
try {
|
|
714
|
+
const text = await request.clone().text();
|
|
715
|
+
if (!text) {
|
|
716
|
+
return { attempted: true, contentType, contentLength };
|
|
717
|
+
}
|
|
718
|
+
const truncated = text.length > observability.requestLogging.maxBodyBytes;
|
|
719
|
+
const raw = truncated ? text.slice(0, observability.requestLogging.maxBodyBytes) : text;
|
|
720
|
+
try {
|
|
721
|
+
const parsed = JSON.parse(raw);
|
|
722
|
+
return {
|
|
723
|
+
attempted: true,
|
|
724
|
+
contentType,
|
|
725
|
+
contentLength: contentLength ?? text.length,
|
|
726
|
+
truncated,
|
|
727
|
+
body: redactBody(parsed, observability.requestLogging.redactPaths)
|
|
728
|
+
};
|
|
729
|
+
} catch {
|
|
730
|
+
return {
|
|
731
|
+
attempted: true,
|
|
732
|
+
contentType,
|
|
733
|
+
contentLength: contentLength ?? text.length,
|
|
734
|
+
truncated,
|
|
735
|
+
parseFailed: true
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
} catch {
|
|
739
|
+
return {
|
|
740
|
+
attempted: true,
|
|
741
|
+
contentType,
|
|
742
|
+
contentLength,
|
|
743
|
+
parseFailed: true
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function redactBody(value, redactPaths) {
|
|
748
|
+
const cloned = deepClone(value);
|
|
749
|
+
for (const path of redactPaths) {
|
|
750
|
+
applyRedaction(cloned, path.split("."));
|
|
751
|
+
}
|
|
752
|
+
return cloned;
|
|
753
|
+
}
|
|
754
|
+
function applyRedaction(target, segments) {
|
|
755
|
+
if (!target || segments.length === 0) return;
|
|
756
|
+
const [segment, ...rest] = segments;
|
|
757
|
+
if (Array.isArray(target)) {
|
|
758
|
+
for (const item of target) {
|
|
759
|
+
if (segment === "*") {
|
|
760
|
+
applyRedaction(item, rest);
|
|
761
|
+
} else {
|
|
762
|
+
applyRedaction(item, segments);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (typeof target !== "object") return;
|
|
768
|
+
const record = target;
|
|
769
|
+
if (segment === "*") {
|
|
770
|
+
for (const value of Object.values(record)) {
|
|
771
|
+
applyRedaction(value, rest);
|
|
772
|
+
}
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (!(segment in record)) return;
|
|
776
|
+
if (rest.length === 0) {
|
|
777
|
+
record[segment] = REDACTED_VALUE;
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
applyRedaction(record[segment], rest);
|
|
781
|
+
}
|
|
782
|
+
function deepClone(value) {
|
|
783
|
+
if (typeof structuredClone === "function") {
|
|
784
|
+
return structuredClone(value);
|
|
785
|
+
}
|
|
786
|
+
return JSON.parse(JSON.stringify(value));
|
|
787
|
+
}
|
|
788
|
+
function createRequestSpan(runtime, name, attributes) {
|
|
789
|
+
if (!runtime.tracer) return void 0;
|
|
790
|
+
const span = runtime.tracer.startSpan(name, { attributes }, import_api.context.active());
|
|
791
|
+
const spanContext = span.spanContext();
|
|
792
|
+
return {
|
|
793
|
+
span,
|
|
794
|
+
traceId: spanContext.traceId,
|
|
795
|
+
spanId: spanContext.spanId,
|
|
796
|
+
sampled: spanContext.traceFlags === import_api.TraceFlags.SAMPLED
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
function endRequestSpan(requestTrace, statusCode, error) {
|
|
800
|
+
if (!requestTrace) return;
|
|
801
|
+
requestTrace.span.setAttribute("http.status_code", statusCode);
|
|
802
|
+
if (error) {
|
|
803
|
+
requestTrace.span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
804
|
+
}
|
|
805
|
+
requestTrace.span.end();
|
|
806
|
+
}
|
|
807
|
+
function attachRequestMetrics(runtime, args) {
|
|
808
|
+
runtime.metrics?.requestCount.add(1, {
|
|
809
|
+
method: args.method,
|
|
810
|
+
route: args.route,
|
|
811
|
+
status_class: `${Math.floor(args.statusCode / 100)}xx`
|
|
812
|
+
});
|
|
813
|
+
runtime.metrics?.requestDuration.record(args.durationMs, {
|
|
814
|
+
method: args.method,
|
|
815
|
+
route: args.route,
|
|
816
|
+
status_class: `${Math.floor(args.statusCode / 100)}xx`
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
async function renderPrometheusMetrics(exporter) {
|
|
820
|
+
let statusCode = 200;
|
|
821
|
+
const headers = {};
|
|
822
|
+
let body = "";
|
|
823
|
+
await new Promise((resolve) => {
|
|
824
|
+
const response = {
|
|
825
|
+
statusCode,
|
|
826
|
+
setHeader(name, value) {
|
|
827
|
+
headers[name.toLowerCase()] = value;
|
|
828
|
+
},
|
|
829
|
+
end(chunk) {
|
|
830
|
+
if (chunk) body += chunk;
|
|
831
|
+
statusCode = this.statusCode;
|
|
832
|
+
resolve();
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
exporter.getMetricsRequestHandler({}, response);
|
|
836
|
+
});
|
|
837
|
+
return { body, headers, statusCode };
|
|
838
|
+
}
|
|
839
|
+
function normalizeHeaderNames(headers) {
|
|
840
|
+
return headers.map((value) => value.toLowerCase());
|
|
841
|
+
}
|
|
842
|
+
function uniqueValues(values) {
|
|
843
|
+
return [...new Set(values)];
|
|
844
|
+
}
|
|
845
|
+
function clampRate(value) {
|
|
846
|
+
if (Number.isNaN(value)) return 0;
|
|
847
|
+
return Math.max(0, Math.min(1, value));
|
|
848
|
+
}
|
|
849
|
+
function deterministicSample(seed, rate) {
|
|
850
|
+
if (rate >= 1) return true;
|
|
851
|
+
if (rate <= 0) return false;
|
|
852
|
+
let hash = 0;
|
|
853
|
+
for (let index = 0; index < seed.length; index += 1) {
|
|
854
|
+
hash = hash * 31 + seed.charCodeAt(index) >>> 0;
|
|
855
|
+
}
|
|
856
|
+
return hash / 4294967295 <= rate;
|
|
857
|
+
}
|
|
858
|
+
function toNumber(value) {
|
|
859
|
+
if (!value) return void 0;
|
|
860
|
+
const parsed = Number(value);
|
|
861
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
862
|
+
}
|
|
863
|
+
|
|
336
864
|
// src/services/audit.service.ts
|
|
337
865
|
var AuditService = class {
|
|
338
866
|
/**
|
|
339
867
|
* Writes a single entry to the __audit collection.
|
|
340
868
|
* Called without await — runs asynchronously and never blocks the primary operation.
|
|
341
869
|
*/
|
|
342
|
-
static async log(db, args) {
|
|
870
|
+
static async log(db, args, config) {
|
|
343
871
|
try {
|
|
344
872
|
await db.create({
|
|
345
873
|
collection: "__audit",
|
|
@@ -356,7 +884,17 @@ var AuditService = class {
|
|
|
356
884
|
}
|
|
357
885
|
});
|
|
358
886
|
} catch (err) {
|
|
359
|
-
|
|
887
|
+
getObservabilityRuntime(config)?.recordAuditWriteFailure({
|
|
888
|
+
collection: args.collection,
|
|
889
|
+
operation: args.operation
|
|
890
|
+
});
|
|
891
|
+
getConfigLogger(config, "audit").error({
|
|
892
|
+
err,
|
|
893
|
+
msg: "Failed to write audit log",
|
|
894
|
+
collection: args.collection,
|
|
895
|
+
operation: args.operation,
|
|
896
|
+
documentId: args.documentId
|
|
897
|
+
});
|
|
360
898
|
}
|
|
361
899
|
}
|
|
362
900
|
};
|
|
@@ -381,6 +919,151 @@ async function verifyPassword(plain, stored) {
|
|
|
381
919
|
return (0, import_node_crypto.timingSafeEqual)(derivedKey, storedBuffer);
|
|
382
920
|
}
|
|
383
921
|
|
|
922
|
+
// src/auth/token.ts
|
|
923
|
+
var import_jose = require("jose");
|
|
924
|
+
var import_node_util2 = require("util");
|
|
925
|
+
function getSecret() {
|
|
926
|
+
const secret = process.env.DYRECTED_JWT_SECRET;
|
|
927
|
+
if (!secret) {
|
|
928
|
+
throw new Error(
|
|
929
|
+
"[dyrected/core] DYRECTED_JWT_SECRET is not set. Add it to your environment variables to enable auth collections."
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
return new import_node_util2.TextEncoder().encode(secret);
|
|
933
|
+
}
|
|
934
|
+
var DEFAULT_EXPIRY = "7d";
|
|
935
|
+
async function signCollectionToken(payload, expiresIn = DEFAULT_EXPIRY) {
|
|
936
|
+
return new import_jose.SignJWT({ ...payload }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(expiresIn).sign(getSecret());
|
|
937
|
+
}
|
|
938
|
+
async function verifyCollectionToken(token) {
|
|
939
|
+
const { payload } = await (0, import_jose.jwtVerify)(token, getSecret());
|
|
940
|
+
return payload;
|
|
941
|
+
}
|
|
942
|
+
function decodeCollectionToken(token) {
|
|
943
|
+
try {
|
|
944
|
+
return (0, import_jose.decodeJwt)(token);
|
|
945
|
+
} catch {
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/auth/sessions.ts
|
|
951
|
+
var AUTH_SESSIONS_COLLECTION = "__auth_sessions";
|
|
952
|
+
async function issueAuthSessionToken(args) {
|
|
953
|
+
const sessionId = createSessionId();
|
|
954
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
955
|
+
const db = args.config.db;
|
|
956
|
+
if (db) {
|
|
957
|
+
await db.create({
|
|
958
|
+
collection: AUTH_SESSIONS_COLLECTION,
|
|
959
|
+
data: {
|
|
960
|
+
id: sessionId,
|
|
961
|
+
userId: args.userId,
|
|
962
|
+
email: args.email,
|
|
963
|
+
collection: args.collection,
|
|
964
|
+
authSource: args.authSource ?? "local",
|
|
965
|
+
providerId: args.providerId,
|
|
966
|
+
lastIp: args.ip,
|
|
967
|
+
lastSeenAt: now,
|
|
968
|
+
revokedAt: null,
|
|
969
|
+
expiresAt: null
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
const token = await signCollectionToken(
|
|
974
|
+
{
|
|
975
|
+
sub: args.userId,
|
|
976
|
+
email: args.email,
|
|
977
|
+
collection: args.collection,
|
|
978
|
+
authSource: args.authSource,
|
|
979
|
+
providerId: args.providerId,
|
|
980
|
+
sid: sessionId
|
|
981
|
+
},
|
|
982
|
+
args.expiresIn
|
|
983
|
+
);
|
|
984
|
+
const payload = decodeCollectionToken(token);
|
|
985
|
+
const expiresAt = payload?.exp != null ? new Date(payload.exp * 1e3).toISOString() : null;
|
|
986
|
+
if (db && expiresAt) {
|
|
987
|
+
await db.update({
|
|
988
|
+
collection: AUTH_SESSIONS_COLLECTION,
|
|
989
|
+
id: sessionId,
|
|
990
|
+
data: {
|
|
991
|
+
expiresAt,
|
|
992
|
+
lastSeenAt: now,
|
|
993
|
+
lastIp: args.ip
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
return token;
|
|
998
|
+
}
|
|
999
|
+
async function getAuthSession(config, sessionId) {
|
|
1000
|
+
const db = config.db;
|
|
1001
|
+
if (!db) return null;
|
|
1002
|
+
return await db.findOne({
|
|
1003
|
+
collection: AUTH_SESSIONS_COLLECTION,
|
|
1004
|
+
id: sessionId
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
function isAuthSessionActive(session) {
|
|
1008
|
+
if (!session) return false;
|
|
1009
|
+
if (session.revokedAt) return false;
|
|
1010
|
+
if (session.expiresAt && new Date(session.expiresAt).getTime() <= Date.now()) {
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
async function touchAuthSession(config, sessionId, args) {
|
|
1016
|
+
const db = config.db;
|
|
1017
|
+
if (!db) return;
|
|
1018
|
+
await db.update({
|
|
1019
|
+
collection: AUTH_SESSIONS_COLLECTION,
|
|
1020
|
+
id: sessionId,
|
|
1021
|
+
data: {
|
|
1022
|
+
lastSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1023
|
+
...args.ip ? { lastIp: args.ip } : {}
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
async function revokeAuthSession(config, sessionId) {
|
|
1028
|
+
const db = config.db;
|
|
1029
|
+
if (!db) return;
|
|
1030
|
+
await db.update({
|
|
1031
|
+
collection: AUTH_SESSIONS_COLLECTION,
|
|
1032
|
+
id: sessionId,
|
|
1033
|
+
data: {
|
|
1034
|
+
revokedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
async function revokeAllAuthSessions(config, args) {
|
|
1039
|
+
const db = config.db;
|
|
1040
|
+
if (!db) return 0;
|
|
1041
|
+
let page = 1;
|
|
1042
|
+
let revoked = 0;
|
|
1043
|
+
while (true) {
|
|
1044
|
+
const result = await db.find({
|
|
1045
|
+
collection: AUTH_SESSIONS_COLLECTION,
|
|
1046
|
+
where: {
|
|
1047
|
+
userId: { equals: args.userId },
|
|
1048
|
+
collection: { equals: args.collection }
|
|
1049
|
+
},
|
|
1050
|
+
page,
|
|
1051
|
+
limit: 100
|
|
1052
|
+
});
|
|
1053
|
+
for (const session of result.docs) {
|
|
1054
|
+
if (session.revokedAt) continue;
|
|
1055
|
+
await revokeAuthSession(config, session.id);
|
|
1056
|
+
revoked += 1;
|
|
1057
|
+
}
|
|
1058
|
+
if (!result.hasNextPage) break;
|
|
1059
|
+
page += 1;
|
|
1060
|
+
}
|
|
1061
|
+
return revoked;
|
|
1062
|
+
}
|
|
1063
|
+
function createSessionId() {
|
|
1064
|
+
return globalThis.crypto?.randomUUID?.() ? globalThis.crypto.randomUUID() : `sess_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
384
1067
|
// src/utils/hooks.ts
|
|
385
1068
|
async function runCollectionHooks(hooks, args, options = {}) {
|
|
386
1069
|
if (!hooks || hooks.length === 0) {
|
|
@@ -399,7 +1082,10 @@ async function runCollectionHooks(hooks, args, options = {}) {
|
|
|
399
1082
|
}
|
|
400
1083
|
} catch (err) {
|
|
401
1084
|
if (options.isolated) {
|
|
402
|
-
|
|
1085
|
+
getConfigLogger(void 0, "hooks").error({
|
|
1086
|
+
err,
|
|
1087
|
+
msg: "Side-effect hook failed after a successful write"
|
|
1088
|
+
});
|
|
403
1089
|
} else {
|
|
404
1090
|
throw err;
|
|
405
1091
|
}
|
|
@@ -444,7 +1130,13 @@ async function executeFieldBeforeChange(fields, data, originalDoc, user, db) {
|
|
|
444
1130
|
const item = updatedValue[i];
|
|
445
1131
|
const origItem = Array.isArray(origValue) ? origValue[i] : null;
|
|
446
1132
|
arrayResult.push(
|
|
447
|
-
await executeFieldBeforeChange(
|
|
1133
|
+
await executeFieldBeforeChange(
|
|
1134
|
+
field.fields,
|
|
1135
|
+
item,
|
|
1136
|
+
origItem,
|
|
1137
|
+
user,
|
|
1138
|
+
db
|
|
1139
|
+
)
|
|
448
1140
|
);
|
|
449
1141
|
}
|
|
450
1142
|
result[field.name] = arrayResult;
|
|
@@ -604,7 +1296,11 @@ async function evaluateAccess(expression, context) {
|
|
|
604
1296
|
}
|
|
605
1297
|
return !!result;
|
|
606
1298
|
} catch (err) {
|
|
607
|
-
|
|
1299
|
+
getConfigLogger(context.config, "access").error({
|
|
1300
|
+
err,
|
|
1301
|
+
msg: "Jexl evaluation failed",
|
|
1302
|
+
expression
|
|
1303
|
+
});
|
|
608
1304
|
return false;
|
|
609
1305
|
}
|
|
610
1306
|
}
|
|
@@ -616,30 +1312,40 @@ function isNamedAccessPolicy(value) {
|
|
|
616
1312
|
async function resolveAccess(config, access, args) {
|
|
617
1313
|
if (access === void 0 || access === null) return void 0;
|
|
618
1314
|
if (typeof access === "boolean" || typeof access === "string") {
|
|
619
|
-
return evaluateAccess(access, args);
|
|
1315
|
+
return evaluateAccess(access, { ...args, config });
|
|
620
1316
|
}
|
|
621
1317
|
if (typeof access === "function") {
|
|
622
1318
|
try {
|
|
623
1319
|
return await access(args);
|
|
624
1320
|
} catch (err) {
|
|
625
|
-
|
|
1321
|
+
getConfigLogger(config, "access").error({
|
|
1322
|
+
err,
|
|
1323
|
+
msg: "Functional access check failed"
|
|
1324
|
+
});
|
|
626
1325
|
return false;
|
|
627
1326
|
}
|
|
628
1327
|
}
|
|
629
1328
|
if (isNamedAccessPolicy(access)) {
|
|
630
1329
|
const policy = config.accessPolicies?.[access.policy];
|
|
631
1330
|
if (policy === void 0) {
|
|
632
|
-
|
|
1331
|
+
getConfigLogger(config, "access").error({
|
|
1332
|
+
msg: "Unknown access policy",
|
|
1333
|
+
policy: access.policy
|
|
1334
|
+
});
|
|
633
1335
|
return false;
|
|
634
1336
|
}
|
|
635
1337
|
if (typeof policy === "string" || typeof policy === "boolean") {
|
|
636
|
-
return evaluateAccess(policy, args);
|
|
1338
|
+
return evaluateAccess(policy, { ...args, config });
|
|
637
1339
|
}
|
|
638
1340
|
try {
|
|
639
1341
|
const resolver = policy;
|
|
640
1342
|
return await resolver({ ...args, params: access.params });
|
|
641
1343
|
} catch (err) {
|
|
642
|
-
|
|
1344
|
+
getConfigLogger(config, "access").error({
|
|
1345
|
+
err,
|
|
1346
|
+
msg: "Access policy failed",
|
|
1347
|
+
policy: access.policy
|
|
1348
|
+
});
|
|
643
1349
|
return false;
|
|
644
1350
|
}
|
|
645
1351
|
}
|
|
@@ -726,32 +1432,48 @@ async function applyFieldReadAccess(context, doc) {
|
|
|
726
1432
|
const result = { ...doc };
|
|
727
1433
|
for (const field of context.fields) {
|
|
728
1434
|
if (field.type === "row" && field.fields) {
|
|
729
|
-
Object.assign(
|
|
1435
|
+
Object.assign(
|
|
1436
|
+
result,
|
|
1437
|
+
await applyFieldReadAccess(
|
|
1438
|
+
{ ...context, fields: field.fields },
|
|
1439
|
+
result
|
|
1440
|
+
)
|
|
1441
|
+
);
|
|
730
1442
|
continue;
|
|
731
1443
|
}
|
|
732
1444
|
if (!field.name) continue;
|
|
733
1445
|
const value = result[field.name];
|
|
734
|
-
const canRead = await resolveBooleanAccess(
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1446
|
+
const canRead = await resolveBooleanAccess(
|
|
1447
|
+
context.config,
|
|
1448
|
+
field.access?.read,
|
|
1449
|
+
{
|
|
1450
|
+
user: context.user,
|
|
1451
|
+
req: context.req,
|
|
1452
|
+
doc: context.doc,
|
|
1453
|
+
data: context.data,
|
|
1454
|
+
id: resolveFieldAccessId(context)
|
|
1455
|
+
}
|
|
1456
|
+
);
|
|
741
1457
|
if (!canRead) {
|
|
742
1458
|
delete result[field.name];
|
|
743
1459
|
continue;
|
|
744
1460
|
}
|
|
745
1461
|
if (value === void 0 || value === null) continue;
|
|
746
1462
|
if (field.type === "object" && field.fields && typeof value === "object" && !Array.isArray(value)) {
|
|
747
|
-
result[field.name] = await applyFieldReadAccess(
|
|
1463
|
+
result[field.name] = await applyFieldReadAccess(
|
|
1464
|
+
{ ...context, fields: field.fields },
|
|
1465
|
+
value
|
|
1466
|
+
);
|
|
748
1467
|
continue;
|
|
749
1468
|
}
|
|
750
1469
|
if (field.type === "array" && field.fields && Array.isArray(value)) {
|
|
751
1470
|
const childFields = field.fields;
|
|
752
1471
|
result[field.name] = await Promise.all(
|
|
753
1472
|
value.map(
|
|
754
|
-
(item) => typeof item === "object" && item !== null ? applyFieldReadAccess(
|
|
1473
|
+
(item) => typeof item === "object" && item !== null ? applyFieldReadAccess(
|
|
1474
|
+
{ ...context, fields: childFields },
|
|
1475
|
+
item
|
|
1476
|
+
) : item
|
|
755
1477
|
)
|
|
756
1478
|
);
|
|
757
1479
|
continue;
|
|
@@ -761,9 +1483,14 @@ async function applyFieldReadAccess(context, doc) {
|
|
|
761
1483
|
value.map(async (item) => {
|
|
762
1484
|
if (typeof item !== "object" || item === null) return item;
|
|
763
1485
|
const typedItem = item;
|
|
764
|
-
const block = field.blocks?.find(
|
|
1486
|
+
const block = field.blocks?.find(
|
|
1487
|
+
(candidate) => candidate.slug === typedItem.blockType
|
|
1488
|
+
);
|
|
765
1489
|
if (!block || !block.fields) return item;
|
|
766
|
-
return applyFieldReadAccess(
|
|
1490
|
+
return applyFieldReadAccess(
|
|
1491
|
+
{ ...context, fields: block.fields },
|
|
1492
|
+
typedItem
|
|
1493
|
+
);
|
|
767
1494
|
})
|
|
768
1495
|
);
|
|
769
1496
|
}
|
|
@@ -775,7 +1502,13 @@ async function applyFieldWriteAccess(context, data) {
|
|
|
775
1502
|
const result = { ...data };
|
|
776
1503
|
for (const field of context.fields) {
|
|
777
1504
|
if (field.type === "row" && field.fields) {
|
|
778
|
-
Object.assign(
|
|
1505
|
+
Object.assign(
|
|
1506
|
+
result,
|
|
1507
|
+
await applyFieldWriteAccess(
|
|
1508
|
+
{ ...context, fields: field.fields },
|
|
1509
|
+
result
|
|
1510
|
+
)
|
|
1511
|
+
);
|
|
779
1512
|
continue;
|
|
780
1513
|
}
|
|
781
1514
|
if (!field.name || !(field.name in result)) continue;
|
|
@@ -794,14 +1527,20 @@ async function applyFieldWriteAccess(context, data) {
|
|
|
794
1527
|
const value = result[field.name];
|
|
795
1528
|
if (value === void 0 || value === null) continue;
|
|
796
1529
|
if (field.type === "object" && field.fields && typeof value === "object" && !Array.isArray(value)) {
|
|
797
|
-
result[field.name] = await applyFieldWriteAccess(
|
|
1530
|
+
result[field.name] = await applyFieldWriteAccess(
|
|
1531
|
+
{ ...context, fields: field.fields },
|
|
1532
|
+
value
|
|
1533
|
+
);
|
|
798
1534
|
continue;
|
|
799
1535
|
}
|
|
800
1536
|
if (field.type === "array" && field.fields && Array.isArray(value)) {
|
|
801
1537
|
const childFields = field.fields;
|
|
802
1538
|
result[field.name] = await Promise.all(
|
|
803
1539
|
value.map(
|
|
804
|
-
(item) => typeof item === "object" && item !== null ? applyFieldWriteAccess(
|
|
1540
|
+
(item) => typeof item === "object" && item !== null ? applyFieldWriteAccess(
|
|
1541
|
+
{ ...context, fields: childFields },
|
|
1542
|
+
item
|
|
1543
|
+
) : item
|
|
805
1544
|
)
|
|
806
1545
|
);
|
|
807
1546
|
continue;
|
|
@@ -811,9 +1550,14 @@ async function applyFieldWriteAccess(context, data) {
|
|
|
811
1550
|
value.map(async (item) => {
|
|
812
1551
|
if (typeof item !== "object" || item === null) return item;
|
|
813
1552
|
const typedItem = item;
|
|
814
|
-
const block = field.blocks?.find(
|
|
1553
|
+
const block = field.blocks?.find(
|
|
1554
|
+
(candidate) => candidate.slug === typedItem.blockType
|
|
1555
|
+
);
|
|
815
1556
|
if (!block || !block.fields) return item;
|
|
816
|
-
return applyFieldWriteAccess(
|
|
1557
|
+
return applyFieldWriteAccess(
|
|
1558
|
+
{ ...context, fields: block.fields },
|
|
1559
|
+
typedItem
|
|
1560
|
+
);
|
|
817
1561
|
})
|
|
818
1562
|
);
|
|
819
1563
|
}
|
|
@@ -839,6 +1583,9 @@ function simplePublishingWorkflow() {
|
|
|
839
1583
|
{ name: "publish", label: "Publish", from: "draft", to: "published" },
|
|
840
1584
|
{ name: "unpublish", label: "Unpublish", from: "published", to: "draft", unpublish: true }
|
|
841
1585
|
]
|
|
1586
|
+
// Intentionally no `roles`: this lightweight workflow (synthesized from
|
|
1587
|
+
// `drafts: true`) does no capability-based gating. Draft visibility for a
|
|
1588
|
+
// no-roles workflow is handled in `canViewWorkflowDraft`.
|
|
842
1589
|
};
|
|
843
1590
|
}
|
|
844
1591
|
function publicMetadata(meta) {
|
|
@@ -857,9 +1604,13 @@ function canViewWorkflowDraft(workflow, user) {
|
|
|
857
1604
|
if (!user) return false;
|
|
858
1605
|
const capabilities = workflowCapabilities(workflow, user);
|
|
859
1606
|
if (capabilities.has("entry.edit")) return true;
|
|
860
|
-
|
|
1607
|
+
if (workflow.transitions.some(
|
|
861
1608
|
(transition) => (transition.requiredCapabilities ?? []).some((capability) => capabilities.has(capability))
|
|
862
|
-
)
|
|
1609
|
+
)) {
|
|
1610
|
+
return true;
|
|
1611
|
+
}
|
|
1612
|
+
if (!workflow.roles || workflow.roles.length === 0) return true;
|
|
1613
|
+
return false;
|
|
863
1614
|
}
|
|
864
1615
|
function availableWorkflowTransitions(workflow, state, user) {
|
|
865
1616
|
const capabilities = workflowCapabilities(workflow, user);
|
|
@@ -1069,7 +1820,17 @@ async function transitionWorkflow(args) {
|
|
|
1069
1820
|
try {
|
|
1070
1821
|
await hook({ ...hookContext, doc: updated, event: events[0] });
|
|
1071
1822
|
} catch (error) {
|
|
1072
|
-
|
|
1823
|
+
getObservabilityRuntime(config)?.recordWorkflowHookFailure({
|
|
1824
|
+
collection: collection.slug,
|
|
1825
|
+
transition: transition.name
|
|
1826
|
+
});
|
|
1827
|
+
getConfigLogger(config, "workflow").error({
|
|
1828
|
+
err: error,
|
|
1829
|
+
msg: "afterTransition hook failed",
|
|
1830
|
+
collection: collection.slug,
|
|
1831
|
+
transition: transition.name,
|
|
1832
|
+
documentId: id
|
|
1833
|
+
});
|
|
1073
1834
|
}
|
|
1074
1835
|
}
|
|
1075
1836
|
return updated;
|
|
@@ -1126,7 +1887,13 @@ var CollectionController = class {
|
|
|
1126
1887
|
} catch {
|
|
1127
1888
|
}
|
|
1128
1889
|
}
|
|
1129
|
-
const paginatedResult = await provider.members.list({
|
|
1890
|
+
const paginatedResult = await provider.members.list({
|
|
1891
|
+
limit: limit2,
|
|
1892
|
+
page: page2,
|
|
1893
|
+
sort: sort2,
|
|
1894
|
+
where: where2,
|
|
1895
|
+
req: hookReq
|
|
1896
|
+
});
|
|
1130
1897
|
const mappedDocs = [];
|
|
1131
1898
|
for (const m of paginatedResult.docs) {
|
|
1132
1899
|
let localId = m.id;
|
|
@@ -1174,12 +1941,15 @@ var CollectionController = class {
|
|
|
1174
1941
|
}
|
|
1175
1942
|
}
|
|
1176
1943
|
}
|
|
1177
|
-
const beforeReadResult = await runCollectionHooks(
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1944
|
+
const beforeReadResult = await runCollectionHooks(
|
|
1945
|
+
this.collection.hooks?.beforeRead,
|
|
1946
|
+
{
|
|
1947
|
+
req: c.req,
|
|
1948
|
+
query: where,
|
|
1949
|
+
user,
|
|
1950
|
+
db: readonlyDb
|
|
1951
|
+
}
|
|
1952
|
+
);
|
|
1183
1953
|
if (beforeReadResult !== void 0) {
|
|
1184
1954
|
where = beforeReadResult;
|
|
1185
1955
|
}
|
|
@@ -1188,7 +1958,13 @@ var CollectionController = class {
|
|
|
1188
1958
|
}
|
|
1189
1959
|
const access = await this.evaluateAccess(c, "read");
|
|
1190
1960
|
if (!access.allowed) {
|
|
1191
|
-
return c.json(
|
|
1961
|
+
return c.json(
|
|
1962
|
+
{
|
|
1963
|
+
error: true,
|
|
1964
|
+
message: `Access denied: read on ${this.collection.slug}`
|
|
1965
|
+
},
|
|
1966
|
+
403
|
|
1967
|
+
);
|
|
1192
1968
|
}
|
|
1193
1969
|
if (access.constraint) {
|
|
1194
1970
|
where = mergeWhereConstraint(where, access.constraint);
|
|
@@ -1202,7 +1978,10 @@ var CollectionController = class {
|
|
|
1202
1978
|
fields: this.collection.fields
|
|
1203
1979
|
});
|
|
1204
1980
|
if (result.total === 0 && this.collection.initialData && !where && page === 1) {
|
|
1205
|
-
|
|
1981
|
+
getRequestLogger(c, "collection").info({
|
|
1982
|
+
msg: "Auto-seeding collection from config.initialData",
|
|
1983
|
+
collection: this.collection.slug
|
|
1984
|
+
});
|
|
1206
1985
|
for (const data of this.collection.initialData) {
|
|
1207
1986
|
await db.create({ collection: this.collection.slug, data });
|
|
1208
1987
|
}
|
|
@@ -1215,31 +1994,54 @@ var CollectionController = class {
|
|
|
1215
1994
|
fields: this.collection.fields
|
|
1216
1995
|
});
|
|
1217
1996
|
}
|
|
1218
|
-
result.docs = result.docs.map(
|
|
1997
|
+
result.docs = result.docs.map(
|
|
1998
|
+
(doc) => this.collection.workflow ? materializeWorkflowDocument(
|
|
1999
|
+
doc,
|
|
2000
|
+
this.collection.workflow,
|
|
2001
|
+
user
|
|
2002
|
+
) : doc
|
|
2003
|
+
).filter((doc) => doc !== null);
|
|
1219
2004
|
const processedDocs = [];
|
|
1220
|
-
const readonlyDbForHooks = createReadonlyDb(db);
|
|
1221
2005
|
for (const doc of result.docs) {
|
|
1222
|
-
const docWithDefaults = DefaultsService.apply(
|
|
1223
|
-
|
|
1224
|
-
doc
|
|
1225
|
-
|
|
2006
|
+
const docWithDefaults = DefaultsService.apply(
|
|
2007
|
+
this.collection.fields,
|
|
2008
|
+
doc
|
|
2009
|
+
);
|
|
2010
|
+
const docWithCollectionHooks = await runCollectionHooks(
|
|
2011
|
+
this.collection.hooks?.afterRead,
|
|
2012
|
+
{
|
|
2013
|
+
doc: docWithDefaults,
|
|
2014
|
+
req: c.req,
|
|
2015
|
+
user,
|
|
2016
|
+
db: readonlyDb
|
|
2017
|
+
}
|
|
2018
|
+
);
|
|
2019
|
+
const docWithFieldHooks = await executeFieldAfterRead(
|
|
2020
|
+
this.collection.fields,
|
|
2021
|
+
docWithCollectionHooks,
|
|
1226
2022
|
user,
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
2023
|
+
readonlyDb
|
|
2024
|
+
);
|
|
2025
|
+
const docWithFieldAccess = await applyFieldReadAccess(
|
|
2026
|
+
{
|
|
2027
|
+
config,
|
|
2028
|
+
fields: this.collection.fields,
|
|
2029
|
+
user,
|
|
2030
|
+
req: this.toHookRequestContext(c),
|
|
2031
|
+
doc: docWithFieldHooks
|
|
2032
|
+
},
|
|
2033
|
+
docWithFieldHooks
|
|
2034
|
+
);
|
|
1237
2035
|
processedDocs.push(docWithFieldAccess);
|
|
1238
2036
|
}
|
|
1239
2037
|
result.docs = processedDocs;
|
|
1240
2038
|
if (depth > 0) {
|
|
1241
2039
|
const populationService = new PopulationService(db, config.collections);
|
|
1242
|
-
result = await populationService.populateResult(
|
|
2040
|
+
result = await populationService.populateResult(
|
|
2041
|
+
result,
|
|
2042
|
+
this.collection.fields,
|
|
2043
|
+
depth
|
|
2044
|
+
);
|
|
1243
2045
|
}
|
|
1244
2046
|
return c.json(result);
|
|
1245
2047
|
}
|
|
@@ -1252,7 +2054,10 @@ var CollectionController = class {
|
|
|
1252
2054
|
const hookReq = this.toHookRequestContext(c);
|
|
1253
2055
|
const id2 = c.req.param("id");
|
|
1254
2056
|
if (!id2) return c.json({ message: "Missing ID" }, 400);
|
|
1255
|
-
const localDoc = await db.findOne({
|
|
2057
|
+
const localDoc = await db.findOne({
|
|
2058
|
+
collection: this.collection.slug,
|
|
2059
|
+
id: id2
|
|
2060
|
+
});
|
|
1256
2061
|
const externalSubject = localDoc?.externalSubject || id2;
|
|
1257
2062
|
let member = null;
|
|
1258
2063
|
if (provider.members.get) {
|
|
@@ -1281,23 +2086,40 @@ var CollectionController = class {
|
|
|
1281
2086
|
}
|
|
1282
2087
|
const access = await this.evaluateAccess(c, "read", { id, doc });
|
|
1283
2088
|
if (!access.allowed) {
|
|
1284
|
-
return c.json(
|
|
2089
|
+
return c.json(
|
|
2090
|
+
{
|
|
2091
|
+
error: true,
|
|
2092
|
+
message: `Access denied: read on ${this.collection.slug}`
|
|
2093
|
+
},
|
|
2094
|
+
403
|
|
2095
|
+
);
|
|
1285
2096
|
}
|
|
1286
2097
|
const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
|
|
1287
|
-
const docWithCollectionHooks = await runCollectionHooks(
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
2098
|
+
const docWithCollectionHooks = await runCollectionHooks(
|
|
2099
|
+
this.collection.hooks?.afterRead,
|
|
2100
|
+
{
|
|
2101
|
+
doc: docWithDefaults,
|
|
2102
|
+
req: c.req,
|
|
2103
|
+
user,
|
|
2104
|
+
db: readonlyDb
|
|
2105
|
+
}
|
|
2106
|
+
);
|
|
2107
|
+
const docWithFieldHooks = await executeFieldAfterRead(
|
|
2108
|
+
this.collection.fields,
|
|
2109
|
+
docWithCollectionHooks,
|
|
1297
2110
|
user,
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
2111
|
+
readonlyDb
|
|
2112
|
+
);
|
|
2113
|
+
const docWithFieldAccess = await applyFieldReadAccess(
|
|
2114
|
+
{
|
|
2115
|
+
config,
|
|
2116
|
+
fields: this.collection.fields,
|
|
2117
|
+
user,
|
|
2118
|
+
req: this.toHookRequestContext(c),
|
|
2119
|
+
doc: docWithFieldHooks
|
|
2120
|
+
},
|
|
2121
|
+
docWithFieldHooks
|
|
2122
|
+
);
|
|
1301
2123
|
if (depth > 0 && docWithFieldAccess) {
|
|
1302
2124
|
const populationService = new PopulationService(db, config.collections);
|
|
1303
2125
|
const populatedDoc = await populationService.populate({
|
|
@@ -1322,12 +2144,18 @@ var CollectionController = class {
|
|
|
1322
2144
|
return this.upload(c);
|
|
1323
2145
|
}
|
|
1324
2146
|
const body2 = await c.req.json();
|
|
1325
|
-
const member = await provider.members.create({
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
2147
|
+
const member = await provider.members.create({
|
|
2148
|
+
data: body2,
|
|
2149
|
+
req: hookReq
|
|
2150
|
+
});
|
|
2151
|
+
return c.json(
|
|
2152
|
+
{
|
|
2153
|
+
...member,
|
|
2154
|
+
id: member.id,
|
|
2155
|
+
externalSubject: member.id
|
|
2156
|
+
},
|
|
2157
|
+
201
|
|
2158
|
+
);
|
|
1331
2159
|
}
|
|
1332
2160
|
const readonlyDb = createReadonlyDb(db);
|
|
1333
2161
|
const contentType = c.req.header("Content-Type") || "";
|
|
@@ -1349,20 +2177,35 @@ var CollectionController = class {
|
|
|
1349
2177
|
}
|
|
1350
2178
|
const createAccess = await this.evaluateAccess(c, "create", { data });
|
|
1351
2179
|
if (!createAccess.allowed) {
|
|
1352
|
-
return c.json(
|
|
2180
|
+
return c.json(
|
|
2181
|
+
{
|
|
2182
|
+
error: true,
|
|
2183
|
+
message: `Access denied: create on ${this.collection.slug}`
|
|
2184
|
+
},
|
|
2185
|
+
403
|
|
2186
|
+
);
|
|
1353
2187
|
}
|
|
1354
2188
|
if (this.collection.auth && data.password) {
|
|
1355
2189
|
data.password = await hashPassword(data.password);
|
|
1356
2190
|
}
|
|
1357
|
-
data = await applyFieldWriteAccess(
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
2191
|
+
data = await applyFieldWriteAccess(
|
|
2192
|
+
{
|
|
2193
|
+
config,
|
|
2194
|
+
fields: this.collection.fields,
|
|
2195
|
+
user,
|
|
2196
|
+
req: this.toHookRequestContext(c),
|
|
2197
|
+
data,
|
|
2198
|
+
operation: "create"
|
|
2199
|
+
},
|
|
2200
|
+
data
|
|
2201
|
+
);
|
|
2202
|
+
data = await executeFieldBeforeChange(
|
|
2203
|
+
this.collection.fields,
|
|
1362
2204
|
data,
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
2205
|
+
null,
|
|
2206
|
+
user,
|
|
2207
|
+
readonlyDb
|
|
2208
|
+
);
|
|
1366
2209
|
data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
|
|
1367
2210
|
data,
|
|
1368
2211
|
req: c.req,
|
|
@@ -1370,7 +2213,12 @@ var CollectionController = class {
|
|
|
1370
2213
|
operation: "create",
|
|
1371
2214
|
db: readonlyDb
|
|
1372
2215
|
});
|
|
1373
|
-
const doc = this.collection.workflow ? (await createWorkflowDocument({
|
|
2216
|
+
const doc = this.collection.workflow ? (await createWorkflowDocument({
|
|
2217
|
+
config,
|
|
2218
|
+
collection: this.collection,
|
|
2219
|
+
data,
|
|
2220
|
+
user
|
|
2221
|
+
})).doc : await db.create({ collection: this.collection.slug, data });
|
|
1374
2222
|
if (this.collection.audit && db) {
|
|
1375
2223
|
AuditService.log(db, {
|
|
1376
2224
|
operation: "create",
|
|
@@ -1379,15 +2227,19 @@ var CollectionController = class {
|
|
|
1379
2227
|
user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
|
|
1380
2228
|
before: null,
|
|
1381
2229
|
after: doc
|
|
1382
|
-
});
|
|
2230
|
+
}, config);
|
|
1383
2231
|
}
|
|
1384
|
-
await runCollectionHooks(
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
2232
|
+
await runCollectionHooks(
|
|
2233
|
+
this.collection.hooks?.afterChange,
|
|
2234
|
+
{
|
|
2235
|
+
doc,
|
|
2236
|
+
user,
|
|
2237
|
+
req: c.req,
|
|
2238
|
+
operation: "create",
|
|
2239
|
+
db
|
|
2240
|
+
},
|
|
2241
|
+
{ isolated: true }
|
|
2242
|
+
);
|
|
1391
2243
|
const responseDoc = this.collection.workflow ? materializeWorkflowDocument(doc, this.collection.workflow, user) : doc;
|
|
1392
2244
|
const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
1393
2245
|
doc: responseDoc,
|
|
@@ -1395,14 +2247,22 @@ var CollectionController = class {
|
|
|
1395
2247
|
user,
|
|
1396
2248
|
db: readonlyDb
|
|
1397
2249
|
});
|
|
1398
|
-
const finalDoc = await executeFieldAfterRead(
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
fields: this.collection.fields,
|
|
2250
|
+
const finalDoc = await executeFieldAfterRead(
|
|
2251
|
+
this.collection.fields,
|
|
2252
|
+
readDoc,
|
|
1402
2253
|
user,
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
2254
|
+
readonlyDb
|
|
2255
|
+
);
|
|
2256
|
+
const accessibleDoc = await applyFieldReadAccess(
|
|
2257
|
+
{
|
|
2258
|
+
config,
|
|
2259
|
+
fields: this.collection.fields,
|
|
2260
|
+
user,
|
|
2261
|
+
req: this.toHookRequestContext(c),
|
|
2262
|
+
doc: finalDoc
|
|
2263
|
+
},
|
|
2264
|
+
finalDoc
|
|
2265
|
+
);
|
|
1406
2266
|
return c.json(accessibleDoc, 201);
|
|
1407
2267
|
}
|
|
1408
2268
|
async upload(c) {
|
|
@@ -1418,7 +2278,10 @@ var CollectionController = class {
|
|
|
1418
2278
|
const uploadConfig = typeof this.collection.upload === "object" ? this.collection.upload : void 0;
|
|
1419
2279
|
const validationError = validateUpload(file, uploadConfig);
|
|
1420
2280
|
if (validationError) {
|
|
1421
|
-
return c.json(
|
|
2281
|
+
return c.json(
|
|
2282
|
+
{ message: validationError.message },
|
|
2283
|
+
validationError.status
|
|
2284
|
+
);
|
|
1422
2285
|
}
|
|
1423
2286
|
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
1424
2287
|
const siteId = c.get("siteId");
|
|
@@ -1448,17 +2311,32 @@ var CollectionController = class {
|
|
|
1448
2311
|
};
|
|
1449
2312
|
const createAccess = await this.evaluateAccess(c, "create", { data });
|
|
1450
2313
|
if (!createAccess.allowed) {
|
|
1451
|
-
return c.json(
|
|
2314
|
+
return c.json(
|
|
2315
|
+
{
|
|
2316
|
+
error: true,
|
|
2317
|
+
message: `Access denied: create on ${this.collection.slug}`
|
|
2318
|
+
},
|
|
2319
|
+
403
|
|
2320
|
+
);
|
|
1452
2321
|
}
|
|
1453
|
-
data = await applyFieldWriteAccess(
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
2322
|
+
data = await applyFieldWriteAccess(
|
|
2323
|
+
{
|
|
2324
|
+
config,
|
|
2325
|
+
fields: this.collection.fields,
|
|
2326
|
+
user,
|
|
2327
|
+
req: this.toHookRequestContext(c),
|
|
2328
|
+
data,
|
|
2329
|
+
operation: "create"
|
|
2330
|
+
},
|
|
2331
|
+
data
|
|
2332
|
+
);
|
|
2333
|
+
data = await executeFieldBeforeChange(
|
|
2334
|
+
this.collection.fields,
|
|
1458
2335
|
data,
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
2336
|
+
null,
|
|
2337
|
+
user,
|
|
2338
|
+
readonlyDb
|
|
2339
|
+
);
|
|
1462
2340
|
data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
|
|
1463
2341
|
data,
|
|
1464
2342
|
req: c.req,
|
|
@@ -1470,13 +2348,17 @@ var CollectionController = class {
|
|
|
1470
2348
|
collection: this.collection.slug,
|
|
1471
2349
|
data
|
|
1472
2350
|
});
|
|
1473
|
-
await runCollectionHooks(
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
2351
|
+
await runCollectionHooks(
|
|
2352
|
+
this.collection.hooks?.afterChange,
|
|
2353
|
+
{
|
|
2354
|
+
doc,
|
|
2355
|
+
user,
|
|
2356
|
+
req: c.req,
|
|
2357
|
+
operation: "create",
|
|
2358
|
+
db
|
|
2359
|
+
},
|
|
2360
|
+
{ isolated: true }
|
|
2361
|
+
);
|
|
1480
2362
|
const responseDoc = this.collection.workflow ? materializeWorkflowDocument(doc, this.collection.workflow, user) : doc;
|
|
1481
2363
|
const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
1482
2364
|
doc: responseDoc,
|
|
@@ -1484,14 +2366,22 @@ var CollectionController = class {
|
|
|
1484
2366
|
user,
|
|
1485
2367
|
db: readonlyDb
|
|
1486
2368
|
});
|
|
1487
|
-
const finalDoc = await executeFieldAfterRead(
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
fields: this.collection.fields,
|
|
2369
|
+
const finalDoc = await executeFieldAfterRead(
|
|
2370
|
+
this.collection.fields,
|
|
2371
|
+
readDoc,
|
|
1491
2372
|
user,
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
2373
|
+
readonlyDb
|
|
2374
|
+
);
|
|
2375
|
+
const accessibleDoc = await applyFieldReadAccess(
|
|
2376
|
+
{
|
|
2377
|
+
config,
|
|
2378
|
+
fields: this.collection.fields,
|
|
2379
|
+
user,
|
|
2380
|
+
req: this.toHookRequestContext(c),
|
|
2381
|
+
doc: finalDoc
|
|
2382
|
+
},
|
|
2383
|
+
finalDoc
|
|
2384
|
+
);
|
|
1495
2385
|
return c.json(accessibleDoc, 201);
|
|
1496
2386
|
}
|
|
1497
2387
|
async update(c) {
|
|
@@ -1504,9 +2394,16 @@ var CollectionController = class {
|
|
|
1504
2394
|
const id2 = c.req.param("id");
|
|
1505
2395
|
if (!id2) return c.json({ message: "Missing ID" }, 400);
|
|
1506
2396
|
const body2 = await c.req.json();
|
|
1507
|
-
const localDoc = await db.findOne({
|
|
2397
|
+
const localDoc = await db.findOne({
|
|
2398
|
+
collection: this.collection.slug,
|
|
2399
|
+
id: id2
|
|
2400
|
+
});
|
|
1508
2401
|
const externalSubject = localDoc?.externalSubject || id2;
|
|
1509
|
-
const member = await provider.members.update({
|
|
2402
|
+
const member = await provider.members.update({
|
|
2403
|
+
externalSubject,
|
|
2404
|
+
data: body2,
|
|
2405
|
+
req: hookReq
|
|
2406
|
+
});
|
|
1510
2407
|
return c.json({
|
|
1511
2408
|
...member,
|
|
1512
2409
|
id: localDoc ? localDoc.id : member.id,
|
|
@@ -1528,25 +2425,47 @@ var CollectionController = class {
|
|
|
1528
2425
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1529
2426
|
updatedBy: user?.sub ?? null
|
|
1530
2427
|
});
|
|
1531
|
-
const originalDoc = await db.findOne({
|
|
2428
|
+
const originalDoc = await db.findOne({
|
|
2429
|
+
collection: this.collection.slug,
|
|
2430
|
+
id
|
|
2431
|
+
});
|
|
1532
2432
|
if (!originalDoc) return c.json({ message: "Not Found" }, 404);
|
|
1533
|
-
const updateAccess = await this.evaluateAccess(c, "update", {
|
|
2433
|
+
const updateAccess = await this.evaluateAccess(c, "update", {
|
|
2434
|
+
id,
|
|
2435
|
+
doc: originalDoc,
|
|
2436
|
+
data
|
|
2437
|
+
});
|
|
1534
2438
|
if (!updateAccess.allowed) {
|
|
1535
|
-
return c.json(
|
|
2439
|
+
return c.json(
|
|
2440
|
+
{
|
|
2441
|
+
error: true,
|
|
2442
|
+
message: `Access denied: update on ${this.collection.slug}`
|
|
2443
|
+
},
|
|
2444
|
+
403
|
|
2445
|
+
);
|
|
1536
2446
|
}
|
|
1537
2447
|
let before = null;
|
|
1538
2448
|
if (this.collection.audit) {
|
|
1539
2449
|
before = originalDoc;
|
|
1540
2450
|
}
|
|
1541
|
-
data = await applyFieldWriteAccess(
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
2451
|
+
data = await applyFieldWriteAccess(
|
|
2452
|
+
{
|
|
2453
|
+
config,
|
|
2454
|
+
fields: this.collection.fields,
|
|
2455
|
+
user,
|
|
2456
|
+
req: this.toHookRequestContext(c),
|
|
2457
|
+
doc: originalDoc,
|
|
2458
|
+
data
|
|
2459
|
+
},
|
|
1547
2460
|
data
|
|
1548
|
-
|
|
1549
|
-
data = await executeFieldBeforeChange(
|
|
2461
|
+
);
|
|
2462
|
+
data = await executeFieldBeforeChange(
|
|
2463
|
+
this.collection.fields,
|
|
2464
|
+
data,
|
|
2465
|
+
originalDoc,
|
|
2466
|
+
user,
|
|
2467
|
+
readonlyDb
|
|
2468
|
+
);
|
|
1550
2469
|
data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
|
|
1551
2470
|
data,
|
|
1552
2471
|
doc: originalDoc,
|
|
@@ -1555,7 +2474,14 @@ var CollectionController = class {
|
|
|
1555
2474
|
operation: "update",
|
|
1556
2475
|
db: readonlyDb
|
|
1557
2476
|
});
|
|
1558
|
-
const doc = this.collection.workflow ? (await saveWorkflowDraft({
|
|
2477
|
+
const doc = this.collection.workflow ? (await saveWorkflowDraft({
|
|
2478
|
+
config,
|
|
2479
|
+
collection: this.collection,
|
|
2480
|
+
id,
|
|
2481
|
+
originalDoc,
|
|
2482
|
+
data,
|
|
2483
|
+
user
|
|
2484
|
+
})).doc : await db.update({ collection: this.collection.slug, id, data });
|
|
1559
2485
|
if (this.collection.audit && db) {
|
|
1560
2486
|
AuditService.log(db, {
|
|
1561
2487
|
operation: "update",
|
|
@@ -1564,36 +2490,52 @@ var CollectionController = class {
|
|
|
1564
2490
|
user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
|
|
1565
2491
|
before,
|
|
1566
2492
|
after: doc
|
|
1567
|
-
});
|
|
2493
|
+
}, config);
|
|
1568
2494
|
}
|
|
1569
|
-
await runCollectionHooks(
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
2495
|
+
await runCollectionHooks(
|
|
2496
|
+
this.collection.hooks?.afterChange,
|
|
2497
|
+
{
|
|
2498
|
+
doc,
|
|
2499
|
+
previousDoc: originalDoc,
|
|
2500
|
+
user,
|
|
2501
|
+
req: c.req,
|
|
2502
|
+
operation: "update",
|
|
2503
|
+
db
|
|
2504
|
+
},
|
|
2505
|
+
{ isolated: true }
|
|
2506
|
+
);
|
|
1577
2507
|
const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
1578
2508
|
doc,
|
|
1579
2509
|
req: c.req,
|
|
1580
2510
|
user,
|
|
1581
2511
|
db: readonlyDb
|
|
1582
2512
|
});
|
|
1583
|
-
const finalDoc = await executeFieldAfterRead(
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
fields: this.collection.fields,
|
|
2513
|
+
const finalDoc = await executeFieldAfterRead(
|
|
2514
|
+
this.collection.fields,
|
|
2515
|
+
readDoc,
|
|
1587
2516
|
user,
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
2517
|
+
readonlyDb
|
|
2518
|
+
);
|
|
2519
|
+
const accessibleDoc = await applyFieldReadAccess(
|
|
2520
|
+
{
|
|
2521
|
+
config,
|
|
2522
|
+
fields: this.collection.fields,
|
|
2523
|
+
user,
|
|
2524
|
+
req: this.toHookRequestContext(c),
|
|
2525
|
+
doc: finalDoc
|
|
2526
|
+
},
|
|
2527
|
+
finalDoc
|
|
2528
|
+
);
|
|
1591
2529
|
return c.json(accessibleDoc);
|
|
1592
2530
|
}
|
|
1593
2531
|
async transition(c) {
|
|
1594
2532
|
const config = c.get("config");
|
|
1595
2533
|
if (!config.db) return c.json({ message: "Database not configured" }, 500);
|
|
1596
|
-
if (!this.collection.workflow)
|
|
2534
|
+
if (!this.collection.workflow)
|
|
2535
|
+
return c.json(
|
|
2536
|
+
{ message: "Workflows are not enabled for this collection" },
|
|
2537
|
+
404
|
|
2538
|
+
);
|
|
1597
2539
|
const id = c.req.param("id");
|
|
1598
2540
|
const transitionName = c.req.param("transition");
|
|
1599
2541
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -1608,19 +2550,38 @@ var CollectionController = class {
|
|
|
1608
2550
|
user: c.get("user"),
|
|
1609
2551
|
req: { query: c.req.query(), headers: c.req.header(), raw: c.req.raw }
|
|
1610
2552
|
});
|
|
1611
|
-
return c.json(
|
|
2553
|
+
return c.json(
|
|
2554
|
+
materializeWorkflowDocument(
|
|
2555
|
+
doc,
|
|
2556
|
+
this.collection.workflow,
|
|
2557
|
+
c.get("user")
|
|
2558
|
+
)
|
|
2559
|
+
);
|
|
1612
2560
|
} catch (error) {
|
|
1613
2561
|
const status = typeof error.statusCode === "number" ? error.statusCode : 500;
|
|
1614
|
-
return c.json(
|
|
2562
|
+
return c.json(
|
|
2563
|
+
{
|
|
2564
|
+
error: true,
|
|
2565
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2566
|
+
},
|
|
2567
|
+
status
|
|
2568
|
+
);
|
|
1615
2569
|
}
|
|
1616
2570
|
}
|
|
1617
2571
|
async workflowHistory(c) {
|
|
1618
2572
|
const config = c.get("config");
|
|
1619
2573
|
if (!config.db) return c.json({ message: "Database not configured" }, 500);
|
|
1620
|
-
if (!this.collection.workflow)
|
|
2574
|
+
if (!this.collection.workflow)
|
|
2575
|
+
return c.json(
|
|
2576
|
+
{ message: "Workflows are not enabled for this collection" },
|
|
2577
|
+
404
|
|
2578
|
+
);
|
|
1621
2579
|
const documentId = c.req.param("id");
|
|
1622
2580
|
if (!documentId) return c.json({ message: "Missing ID" }, 400);
|
|
1623
|
-
const document = await config.db.findOne({
|
|
2581
|
+
const document = await config.db.findOne({
|
|
2582
|
+
collection: this.collection.slug,
|
|
2583
|
+
id: documentId
|
|
2584
|
+
});
|
|
1624
2585
|
if (!document) return c.json({ message: "Not Found" }, 404);
|
|
1625
2586
|
const readAccess = this.collection.access?.read;
|
|
1626
2587
|
if (readAccess !== void 0 && readAccess !== null) {
|
|
@@ -1635,11 +2596,21 @@ var CollectionController = class {
|
|
|
1635
2596
|
});
|
|
1636
2597
|
allowed = match.total > 0;
|
|
1637
2598
|
}
|
|
1638
|
-
if (!allowed)
|
|
2599
|
+
if (!allowed)
|
|
2600
|
+
return c.json(
|
|
2601
|
+
{
|
|
2602
|
+
error: true,
|
|
2603
|
+
message: `Access denied: read on ${this.collection.slug}`
|
|
2604
|
+
},
|
|
2605
|
+
403
|
|
2606
|
+
);
|
|
1639
2607
|
}
|
|
1640
2608
|
const result = await config.db.find({
|
|
1641
2609
|
collection: WORKFLOW_HISTORY_COLLECTION,
|
|
1642
|
-
where: {
|
|
2610
|
+
where: {
|
|
2611
|
+
collection: { equals: this.collection.slug },
|
|
2612
|
+
documentId: { equals: documentId }
|
|
2613
|
+
},
|
|
1643
2614
|
sort: "-createdAt",
|
|
1644
2615
|
limit: Math.min(Number(c.req.query("limit")) || 50, 100)
|
|
1645
2616
|
});
|
|
@@ -1661,7 +2632,10 @@ var CollectionController = class {
|
|
|
1661
2632
|
const db = config.db;
|
|
1662
2633
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
1663
2634
|
if (!this.collection.auth) {
|
|
1664
|
-
return c.json(
|
|
2635
|
+
return c.json(
|
|
2636
|
+
{ message: "This collection does not support authentication" },
|
|
2637
|
+
400
|
|
2638
|
+
);
|
|
1665
2639
|
}
|
|
1666
2640
|
const id = c.req.param("id");
|
|
1667
2641
|
if (!id) return c.json({ message: "Missing ID" }, 400);
|
|
@@ -1681,29 +2655,44 @@ var CollectionController = class {
|
|
|
1681
2655
|
const isAdmin = Array.isArray(user.roles) && user.roles.includes("admin");
|
|
1682
2656
|
const isSelf = user.sub === id;
|
|
1683
2657
|
if (!isAdmin && !isSelf) {
|
|
1684
|
-
return c.json(
|
|
2658
|
+
return c.json(
|
|
2659
|
+
{ message: "You are not authorised to change this password" },
|
|
2660
|
+
403
|
|
2661
|
+
);
|
|
1685
2662
|
}
|
|
1686
2663
|
if (!isAdmin) {
|
|
1687
2664
|
if (!oldPassword) {
|
|
1688
2665
|
return c.json({ message: "Current password is required" }, 400);
|
|
1689
2666
|
}
|
|
1690
|
-
const existing = await db.findOne({
|
|
2667
|
+
const existing = await db.findOne({
|
|
2668
|
+
collection: this.collection.slug,
|
|
2669
|
+
id
|
|
2670
|
+
});
|
|
1691
2671
|
if (!existing) return c.json({ message: "User not found" }, 404);
|
|
1692
|
-
const valid = await verifyPassword(
|
|
2672
|
+
const valid = await verifyPassword(
|
|
2673
|
+
oldPassword,
|
|
2674
|
+
existing.password
|
|
2675
|
+
);
|
|
1693
2676
|
if (!valid) {
|
|
1694
2677
|
return c.json({ message: "Invalid current password" }, 400);
|
|
1695
2678
|
}
|
|
1696
2679
|
}
|
|
1697
2680
|
const hashed = await hashPassword(newPassword);
|
|
1698
|
-
|
|
2681
|
+
await db.update({
|
|
1699
2682
|
collection: this.collection.slug,
|
|
1700
2683
|
id,
|
|
1701
2684
|
data: {
|
|
1702
2685
|
password: hashed,
|
|
2686
|
+
loginAttempts: 0,
|
|
2687
|
+
lockedUntil: null,
|
|
1703
2688
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1704
2689
|
updatedBy: user.sub
|
|
1705
2690
|
}
|
|
1706
2691
|
});
|
|
2692
|
+
await revokeAllAuthSessions(config, {
|
|
2693
|
+
userId: id,
|
|
2694
|
+
collection: this.collection.slug
|
|
2695
|
+
});
|
|
1707
2696
|
if (this.collection.audit) {
|
|
1708
2697
|
AuditService.log(db, {
|
|
1709
2698
|
operation: "update",
|
|
@@ -1712,7 +2701,7 @@ var CollectionController = class {
|
|
|
1712
2701
|
user: { id: user.sub, collection: user.collection, email: user.email },
|
|
1713
2702
|
before: null,
|
|
1714
2703
|
after: { id }
|
|
1715
|
-
});
|
|
2704
|
+
}, config);
|
|
1716
2705
|
}
|
|
1717
2706
|
return c.json({ success: true, message: "Password updated successfully" });
|
|
1718
2707
|
}
|
|
@@ -1725,7 +2714,10 @@ var CollectionController = class {
|
|
|
1725
2714
|
const hookReq = this.toHookRequestContext(c);
|
|
1726
2715
|
const id2 = c.req.param("id");
|
|
1727
2716
|
if (!id2) return c.json({ message: "Missing ID" }, 400);
|
|
1728
|
-
const localDoc = await db.findOne({
|
|
2717
|
+
const localDoc = await db.findOne({
|
|
2718
|
+
collection: this.collection.slug,
|
|
2719
|
+
id: id2
|
|
2720
|
+
});
|
|
1729
2721
|
const externalSubject = localDoc?.externalSubject || id2;
|
|
1730
2722
|
await provider.members.delete({ externalSubject, req: hookReq });
|
|
1731
2723
|
return c.json({ message: "Deleted" });
|
|
@@ -1738,7 +2730,13 @@ var CollectionController = class {
|
|
|
1738
2730
|
if (!doc) return c.json({ message: "Not Found" }, 404);
|
|
1739
2731
|
const deleteAccess = await this.evaluateAccess(c, "delete", { id, doc });
|
|
1740
2732
|
if (!deleteAccess.allowed) {
|
|
1741
|
-
return c.json(
|
|
2733
|
+
return c.json(
|
|
2734
|
+
{
|
|
2735
|
+
error: true,
|
|
2736
|
+
message: `Access denied: delete on ${this.collection.slug}`
|
|
2737
|
+
},
|
|
2738
|
+
403
|
|
2739
|
+
);
|
|
1742
2740
|
}
|
|
1743
2741
|
let before = null;
|
|
1744
2742
|
if (this.collection.audit) {
|
|
@@ -1760,15 +2758,19 @@ var CollectionController = class {
|
|
|
1760
2758
|
user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
|
|
1761
2759
|
before,
|
|
1762
2760
|
after: null
|
|
1763
|
-
});
|
|
2761
|
+
}, config);
|
|
1764
2762
|
}
|
|
1765
|
-
await runCollectionHooks(
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
2763
|
+
await runCollectionHooks(
|
|
2764
|
+
this.collection.hooks?.afterDelete,
|
|
2765
|
+
{
|
|
2766
|
+
id,
|
|
2767
|
+
doc,
|
|
2768
|
+
user,
|
|
2769
|
+
req: c.req,
|
|
2770
|
+
db
|
|
2771
|
+
},
|
|
2772
|
+
{ isolated: true }
|
|
2773
|
+
);
|
|
1772
2774
|
return c.json({ message: "Deleted" });
|
|
1773
2775
|
}
|
|
1774
2776
|
async deleteMany(c) {
|
|
@@ -1799,7 +2801,10 @@ var CollectionController = class {
|
|
|
1799
2801
|
failed.push({ id, error: "Not Found" });
|
|
1800
2802
|
continue;
|
|
1801
2803
|
}
|
|
1802
|
-
const deleteAccess = await this.evaluateAccess(c, "delete", {
|
|
2804
|
+
const deleteAccess = await this.evaluateAccess(c, "delete", {
|
|
2805
|
+
id,
|
|
2806
|
+
doc
|
|
2807
|
+
});
|
|
1803
2808
|
if (!deleteAccess.allowed) {
|
|
1804
2809
|
failed.push({ id, error: "Access denied" });
|
|
1805
2810
|
continue;
|
|
@@ -1825,15 +2830,19 @@ var CollectionController = class {
|
|
|
1825
2830
|
user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
|
|
1826
2831
|
before,
|
|
1827
2832
|
after: null
|
|
1828
|
-
});
|
|
2833
|
+
}, config);
|
|
1829
2834
|
}
|
|
1830
|
-
await runCollectionHooks(
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
2835
|
+
await runCollectionHooks(
|
|
2836
|
+
this.collection.hooks?.afterDelete,
|
|
2837
|
+
{
|
|
2838
|
+
id,
|
|
2839
|
+
doc,
|
|
2840
|
+
user,
|
|
2841
|
+
req: c.req,
|
|
2842
|
+
db
|
|
2843
|
+
},
|
|
2844
|
+
{ isolated: true }
|
|
2845
|
+
);
|
|
1837
2846
|
} catch (err) {
|
|
1838
2847
|
failed.push({ id, error: err?.message ?? "Unknown error" });
|
|
1839
2848
|
}
|
|
@@ -1853,17 +2862,26 @@ var CollectionController = class {
|
|
|
1853
2862
|
if (!initialData || !Array.isArray(initialData)) {
|
|
1854
2863
|
return c.json({ message: "Invalid initial data" }, 400);
|
|
1855
2864
|
}
|
|
1856
|
-
const result = await db.find({
|
|
2865
|
+
const result = await db.find({
|
|
2866
|
+
collection: this.collection.slug,
|
|
2867
|
+
limit: 1
|
|
2868
|
+
});
|
|
1857
2869
|
if (result.total > 0) {
|
|
1858
2870
|
return c.json({ message: "Collection is not empty, skipping seed" });
|
|
1859
2871
|
}
|
|
1860
|
-
|
|
2872
|
+
getRequestLogger(c, "collection").info({
|
|
2873
|
+
msg: "Auto-seeding collection",
|
|
2874
|
+
collection: this.collection.slug
|
|
2875
|
+
});
|
|
1861
2876
|
const createdDocs = [];
|
|
1862
2877
|
for (const data of initialData) {
|
|
1863
2878
|
const doc = await db.create({ collection: this.collection.slug, data });
|
|
1864
2879
|
createdDocs.push(doc);
|
|
1865
2880
|
}
|
|
1866
|
-
return c.json(
|
|
2881
|
+
return c.json(
|
|
2882
|
+
{ message: "Seed successful", count: createdDocs.length },
|
|
2883
|
+
201
|
|
2884
|
+
);
|
|
1867
2885
|
}
|
|
1868
2886
|
};
|
|
1869
2887
|
|
|
@@ -1880,46 +2898,66 @@ var GlobalController = class {
|
|
|
1880
2898
|
const readonlyDb = createReadonlyDb(db);
|
|
1881
2899
|
const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
|
|
1882
2900
|
const user = c.get("user");
|
|
1883
|
-
|
|
1884
|
-
const beforeReadResult = await runCollectionHooks(this.global.hooks?.beforeRead, {
|
|
2901
|
+
await runCollectionHooks(this.global.hooks?.beforeRead, {
|
|
1885
2902
|
req: c.req,
|
|
1886
|
-
query,
|
|
2903
|
+
query: void 0,
|
|
1887
2904
|
user,
|
|
1888
2905
|
db: readonlyDb
|
|
1889
2906
|
});
|
|
1890
|
-
if (beforeReadResult !== void 0) {
|
|
1891
|
-
query = beforeReadResult;
|
|
1892
|
-
}
|
|
1893
2907
|
let data = await db.getGlobal({ slug: this.global.slug });
|
|
1894
2908
|
const isEmpty = !data || isFunctionallyEmpty(data);
|
|
1895
2909
|
if (isEmpty && this.global.initialData) {
|
|
1896
|
-
|
|
1897
|
-
|
|
2910
|
+
getRequestLogger(c, "global").info({
|
|
2911
|
+
msg: "Auto-seeding global from config.initialData",
|
|
2912
|
+
global: this.global.slug
|
|
2913
|
+
});
|
|
2914
|
+
await db.updateGlobal({
|
|
2915
|
+
slug: this.global.slug,
|
|
2916
|
+
data: this.global.initialData
|
|
2917
|
+
});
|
|
1898
2918
|
data = this.global.initialData;
|
|
1899
2919
|
}
|
|
1900
|
-
const canRead = await resolveBooleanAccess(
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2920
|
+
const canRead = await resolveBooleanAccess(
|
|
2921
|
+
config,
|
|
2922
|
+
this.global.access?.read,
|
|
2923
|
+
{
|
|
2924
|
+
user,
|
|
2925
|
+
req: toHookRequestContext(c.req),
|
|
2926
|
+
doc: data
|
|
2927
|
+
}
|
|
2928
|
+
);
|
|
1905
2929
|
if (!canRead) {
|
|
1906
|
-
return c.json(
|
|
2930
|
+
return c.json(
|
|
2931
|
+
{ error: true, message: `Access denied: read on ${this.global.slug}` },
|
|
2932
|
+
403
|
|
2933
|
+
);
|
|
1907
2934
|
}
|
|
1908
2935
|
const dataWithDefaults = DefaultsService.apply(this.global.fields, data);
|
|
1909
|
-
const docWithCollectionHooks = await runCollectionHooks(
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2936
|
+
const docWithCollectionHooks = await runCollectionHooks(
|
|
2937
|
+
this.global.hooks?.afterRead,
|
|
2938
|
+
{
|
|
2939
|
+
doc: dataWithDefaults,
|
|
2940
|
+
req: c.req,
|
|
2941
|
+
user,
|
|
2942
|
+
db: readonlyDb
|
|
2943
|
+
}
|
|
2944
|
+
);
|
|
2945
|
+
const docWithFieldHooks = await executeFieldAfterRead(
|
|
2946
|
+
this.global.fields,
|
|
2947
|
+
docWithCollectionHooks,
|
|
1919
2948
|
user,
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
2949
|
+
readonlyDb
|
|
2950
|
+
);
|
|
2951
|
+
const docWithFieldAccess = await applyFieldReadAccess(
|
|
2952
|
+
{
|
|
2953
|
+
config,
|
|
2954
|
+
fields: this.global.fields,
|
|
2955
|
+
user,
|
|
2956
|
+
req: toHookRequestContext(c.req),
|
|
2957
|
+
doc: docWithFieldHooks
|
|
2958
|
+
},
|
|
2959
|
+
docWithFieldHooks
|
|
2960
|
+
);
|
|
1923
2961
|
if (depth > 0 && docWithFieldAccess) {
|
|
1924
2962
|
const populationService = new PopulationService(db, config.collections);
|
|
1925
2963
|
const populatedData = await populationService.populate({
|
|
@@ -1940,24 +2978,43 @@ var GlobalController = class {
|
|
|
1940
2978
|
const body = await c.req.json();
|
|
1941
2979
|
const user = c.get("user");
|
|
1942
2980
|
const originalDoc = await db.getGlobal({ slug: this.global.slug }) || {};
|
|
1943
|
-
const canUpdate = await resolveBooleanAccess(
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2981
|
+
const canUpdate = await resolveBooleanAccess(
|
|
2982
|
+
config,
|
|
2983
|
+
this.global.access?.update,
|
|
2984
|
+
{
|
|
2985
|
+
user,
|
|
2986
|
+
req: toHookRequestContext(c.req),
|
|
2987
|
+
doc: originalDoc,
|
|
2988
|
+
data: body
|
|
2989
|
+
}
|
|
2990
|
+
);
|
|
1949
2991
|
if (!canUpdate) {
|
|
1950
|
-
return c.json(
|
|
2992
|
+
return c.json(
|
|
2993
|
+
{
|
|
2994
|
+
error: true,
|
|
2995
|
+
message: `Access denied: update on ${this.global.slug}`
|
|
2996
|
+
},
|
|
2997
|
+
403
|
|
2998
|
+
);
|
|
1951
2999
|
}
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
3000
|
+
const sanitizedBody = await applyFieldWriteAccess(
|
|
3001
|
+
{
|
|
3002
|
+
config,
|
|
3003
|
+
fields: this.global.fields,
|
|
3004
|
+
user,
|
|
3005
|
+
req: toHookRequestContext(c.req),
|
|
3006
|
+
doc: originalDoc,
|
|
3007
|
+
data: body
|
|
3008
|
+
},
|
|
3009
|
+
body
|
|
3010
|
+
);
|
|
3011
|
+
let data = await executeFieldBeforeChange(
|
|
3012
|
+
this.global.fields,
|
|
3013
|
+
sanitizedBody,
|
|
3014
|
+
originalDoc,
|
|
1955
3015
|
user,
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
data: body
|
|
1959
|
-
}, body);
|
|
1960
|
-
let data = await executeFieldBeforeChange(this.global.fields, sanitizedBody, originalDoc, user, readonlyDb);
|
|
3016
|
+
readonlyDb
|
|
3017
|
+
);
|
|
1961
3018
|
data = await runCollectionHooks(this.global.hooks?.beforeChange, {
|
|
1962
3019
|
data,
|
|
1963
3020
|
doc: originalDoc,
|
|
@@ -1967,28 +3024,40 @@ var GlobalController = class {
|
|
|
1967
3024
|
db: readonlyDb
|
|
1968
3025
|
});
|
|
1969
3026
|
const updated = await db.updateGlobal({ slug: this.global.slug, data });
|
|
1970
|
-
await runCollectionHooks(
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
3027
|
+
await runCollectionHooks(
|
|
3028
|
+
this.global.hooks?.afterChange,
|
|
3029
|
+
{
|
|
3030
|
+
doc: updated,
|
|
3031
|
+
previousDoc: originalDoc,
|
|
3032
|
+
user,
|
|
3033
|
+
req: c.req,
|
|
3034
|
+
operation: "update",
|
|
3035
|
+
db
|
|
3036
|
+
},
|
|
3037
|
+
{ isolated: true }
|
|
3038
|
+
);
|
|
1978
3039
|
const readDoc = await runCollectionHooks(this.global.hooks?.afterRead, {
|
|
1979
3040
|
doc: updated,
|
|
1980
3041
|
req: c.req,
|
|
1981
3042
|
user,
|
|
1982
3043
|
db: readonlyDb
|
|
1983
3044
|
});
|
|
1984
|
-
const finalDoc = await executeFieldAfterRead(
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
fields: this.global.fields,
|
|
3045
|
+
const finalDoc = await executeFieldAfterRead(
|
|
3046
|
+
this.global.fields,
|
|
3047
|
+
readDoc,
|
|
1988
3048
|
user,
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
3049
|
+
readonlyDb
|
|
3050
|
+
);
|
|
3051
|
+
const accessibleDoc = await applyFieldReadAccess(
|
|
3052
|
+
{
|
|
3053
|
+
config,
|
|
3054
|
+
fields: this.global.fields,
|
|
3055
|
+
user,
|
|
3056
|
+
req: toHookRequestContext(c.req),
|
|
3057
|
+
doc: finalDoc
|
|
3058
|
+
},
|
|
3059
|
+
finalDoc
|
|
3060
|
+
);
|
|
1992
3061
|
return c.json(accessibleDoc);
|
|
1993
3062
|
}
|
|
1994
3063
|
async seed(c) {
|
|
@@ -2004,7 +3073,10 @@ var GlobalController = class {
|
|
|
2004
3073
|
if (existing && !isFunctionallyEmpty(existing)) {
|
|
2005
3074
|
return c.json({ message: "Global is not empty, skipping seed" });
|
|
2006
3075
|
}
|
|
2007
|
-
|
|
3076
|
+
getRequestLogger(c, "global").info({
|
|
3077
|
+
msg: "Auto-seeding global",
|
|
3078
|
+
global: this.global.slug
|
|
3079
|
+
});
|
|
2008
3080
|
await db.updateGlobal({ slug: this.global.slug, data: initialData });
|
|
2009
3081
|
return c.json({ message: "Seed successful", data: initialData }, 201);
|
|
2010
3082
|
}
|
|
@@ -2023,9 +3095,116 @@ function isFunctionallyEmpty(obj) {
|
|
|
2023
3095
|
return false;
|
|
2024
3096
|
}
|
|
2025
3097
|
|
|
3098
|
+
// src/utils/block-references.ts
|
|
3099
|
+
function mergeUniqueBlocks(blocks) {
|
|
3100
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3101
|
+
for (const block of blocks) {
|
|
3102
|
+
const existing = seen.get(block.slug);
|
|
3103
|
+
if (existing && existing !== block) {
|
|
3104
|
+
throw new Error(
|
|
3105
|
+
`Duplicate block slug "${block.slug}" found in the reusable block registry. Block slugs must be unique.`
|
|
3106
|
+
);
|
|
3107
|
+
}
|
|
3108
|
+
if (!existing) seen.set(block.slug, block);
|
|
3109
|
+
}
|
|
3110
|
+
return Array.from(seen.values());
|
|
3111
|
+
}
|
|
3112
|
+
function buildRegistry(blocks) {
|
|
3113
|
+
return new Map(blocks.map((block) => [block.slug, block]));
|
|
3114
|
+
}
|
|
3115
|
+
function resolveField(field, registry, blockCache) {
|
|
3116
|
+
const next = { ...field };
|
|
3117
|
+
if (next.fields) {
|
|
3118
|
+
next.fields = next.fields.map(
|
|
3119
|
+
(child) => resolveField(child, registry, blockCache)
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
if (next.type !== "blocks") return next;
|
|
3123
|
+
const hasInlineBlocks = Array.isArray(next.blocks) && next.blocks.length > 0;
|
|
3124
|
+
const hasReferences = Array.isArray(next.blockReferences) && next.blockReferences.length > 0;
|
|
3125
|
+
if (hasInlineBlocks && hasReferences) {
|
|
3126
|
+
throw new Error(
|
|
3127
|
+
`Blocks field "${next.name ?? "(unnamed)"}" cannot define both "blocks" and "blockReferences". Use one or the other.`
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
if (hasReferences) {
|
|
3131
|
+
next.blocks = next.blockReferences.map((slug) => {
|
|
3132
|
+
const block = registry.get(slug);
|
|
3133
|
+
if (!block) {
|
|
3134
|
+
throw new Error(
|
|
3135
|
+
`Unknown block reference "${slug}" on blocks field "${next.name ?? "(unnamed)"}". Add it to defineConfig({ blocks: [...] }).`
|
|
3136
|
+
);
|
|
3137
|
+
}
|
|
3138
|
+
return resolveBlock(block, registry, blockCache);
|
|
3139
|
+
});
|
|
3140
|
+
return next;
|
|
3141
|
+
}
|
|
3142
|
+
if (hasInlineBlocks) {
|
|
3143
|
+
next.blocks = next.blocks.map(
|
|
3144
|
+
(block) => resolveBlock(block, registry, blockCache)
|
|
3145
|
+
);
|
|
3146
|
+
}
|
|
3147
|
+
return next;
|
|
3148
|
+
}
|
|
3149
|
+
function resolveBlock(block, registry, blockCache) {
|
|
3150
|
+
const cached = blockCache.get(block);
|
|
3151
|
+
if (cached) return cached;
|
|
3152
|
+
const resolved = {
|
|
3153
|
+
...block,
|
|
3154
|
+
fields: []
|
|
3155
|
+
};
|
|
3156
|
+
blockCache.set(block, resolved);
|
|
3157
|
+
resolved.fields = block.fields.map(
|
|
3158
|
+
(field) => resolveField(field, registry, blockCache)
|
|
3159
|
+
);
|
|
3160
|
+
return resolved;
|
|
3161
|
+
}
|
|
3162
|
+
function resolveFields(fields, registry, blockCache) {
|
|
3163
|
+
return fields.map((field) => resolveField(field, registry, blockCache));
|
|
3164
|
+
}
|
|
3165
|
+
function normalizeSchemaFragment(fragment) {
|
|
3166
|
+
const reusableBlocks = mergeUniqueBlocks(fragment.blocks ?? []);
|
|
3167
|
+
const registry = buildRegistry(reusableBlocks);
|
|
3168
|
+
const blockCache = /* @__PURE__ */ new WeakMap();
|
|
3169
|
+
const resolvedBlocks = reusableBlocks.map(
|
|
3170
|
+
(block) => resolveBlock(block, registry, blockCache)
|
|
3171
|
+
);
|
|
3172
|
+
const resolvedCollections = (fragment.collections ?? []).map(
|
|
3173
|
+
(collection) => ({
|
|
3174
|
+
...collection,
|
|
3175
|
+
fields: resolveFields(collection.fields ?? [], registry, blockCache)
|
|
3176
|
+
})
|
|
3177
|
+
);
|
|
3178
|
+
const resolvedGlobals = (fragment.globals ?? []).map((global) => ({
|
|
3179
|
+
...global,
|
|
3180
|
+
fields: resolveFields(global.fields ?? [], registry, blockCache)
|
|
3181
|
+
}));
|
|
3182
|
+
return {
|
|
3183
|
+
...fragment,
|
|
3184
|
+
...fragment.blocks ? { blocks: resolvedBlocks } : {},
|
|
3185
|
+
...fragment.collections ? { collections: resolvedCollections } : {},
|
|
3186
|
+
...fragment.globals ? { globals: resolvedGlobals } : {}
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
3189
|
+
function mergeDynamicConfig(baseConfig, dynamic) {
|
|
3190
|
+
const normalizedDynamic = normalizeSchemaFragment({
|
|
3191
|
+
...dynamic,
|
|
3192
|
+
blocks: [...baseConfig.blocks ?? [], ...dynamic.blocks ?? []]
|
|
3193
|
+
});
|
|
3194
|
+
return {
|
|
3195
|
+
...baseConfig,
|
|
3196
|
+
...normalizedDynamic.blocks ? { blocks: normalizedDynamic.blocks } : {},
|
|
3197
|
+
...dynamic.admin ? { admin: { ...baseConfig.admin, ...dynamic.admin } } : {},
|
|
3198
|
+
...dynamic.adminAuth ? { adminAuth: { ...baseConfig.adminAuth, ...dynamic.adminAuth } } : {},
|
|
3199
|
+
collections: normalizedDynamic.collections ? [...baseConfig.collections, ...normalizedDynamic.collections] : baseConfig.collections,
|
|
3200
|
+
globals: normalizedDynamic.globals ? [...baseConfig.globals, ...normalizedDynamic.globals] : baseConfig.globals
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
3203
|
+
|
|
2026
3204
|
// src/controllers/media.controller.ts
|
|
2027
3205
|
var MediaController = class {
|
|
2028
3206
|
collection;
|
|
3207
|
+
loggerComponent = "media";
|
|
2029
3208
|
constructor(collection = "media") {
|
|
2030
3209
|
this.collection = collection;
|
|
2031
3210
|
}
|
|
@@ -2044,15 +3223,25 @@ var MediaController = class {
|
|
|
2044
3223
|
return c.json({ message: "No file uploaded" }, 400);
|
|
2045
3224
|
}
|
|
2046
3225
|
const siteId = c.get("siteId");
|
|
2047
|
-
let colConfig = config.collections.find(
|
|
3226
|
+
let colConfig = config.collections.find(
|
|
3227
|
+
(col) => col.slug === this.collection
|
|
3228
|
+
);
|
|
2048
3229
|
if (!colConfig && config.onSchemaFetch && siteId) {
|
|
2049
|
-
const
|
|
2050
|
-
|
|
3230
|
+
const requestConfig = mergeDynamicConfig(
|
|
3231
|
+
config,
|
|
3232
|
+
await config.onSchemaFetch(siteId)
|
|
3233
|
+
);
|
|
3234
|
+
colConfig = requestConfig.collections.find(
|
|
3235
|
+
(col) => col.slug === this.collection
|
|
3236
|
+
);
|
|
2051
3237
|
}
|
|
2052
3238
|
const uploadConfig = typeof colConfig?.upload === "object" ? colConfig.upload : void 0;
|
|
2053
3239
|
const validationError = validateUpload(file, uploadConfig);
|
|
2054
3240
|
if (validationError) {
|
|
2055
|
-
return c.json(
|
|
3241
|
+
return c.json(
|
|
3242
|
+
{ message: validationError.message },
|
|
3243
|
+
validationError.status
|
|
3244
|
+
);
|
|
2056
3245
|
}
|
|
2057
3246
|
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
2058
3247
|
const workspaceId = c.get("workspaceId");
|
|
@@ -2070,7 +3259,12 @@ var MediaController = class {
|
|
|
2070
3259
|
imageMetadata = processed.metadata;
|
|
2071
3260
|
imageSizes = processed.sizes;
|
|
2072
3261
|
} catch (err) {
|
|
2073
|
-
|
|
3262
|
+
getRequestLogger(c, this.loggerComponent).error({
|
|
3263
|
+
err,
|
|
3264
|
+
msg: "Image processing failed",
|
|
3265
|
+
collection: this.collection,
|
|
3266
|
+
filename: file.name
|
|
3267
|
+
});
|
|
2074
3268
|
}
|
|
2075
3269
|
}
|
|
2076
3270
|
const fileData = await storage.upload({
|
|
@@ -2103,7 +3297,13 @@ var MediaController = class {
|
|
|
2103
3297
|
height: sizeData.height
|
|
2104
3298
|
};
|
|
2105
3299
|
} catch (err) {
|
|
2106
|
-
|
|
3300
|
+
getRequestLogger(c, this.loggerComponent).error({
|
|
3301
|
+
err,
|
|
3302
|
+
msg: "Failed to upload image size",
|
|
3303
|
+
collection: this.collection,
|
|
3304
|
+
filename: file.name,
|
|
3305
|
+
sizeName
|
|
3306
|
+
});
|
|
2107
3307
|
}
|
|
2108
3308
|
}
|
|
2109
3309
|
}
|
|
@@ -2167,25 +3367,37 @@ var MediaController = class {
|
|
|
2167
3367
|
}
|
|
2168
3368
|
};
|
|
2169
3369
|
|
|
2170
|
-
// src/auth/
|
|
2171
|
-
var
|
|
2172
|
-
var
|
|
2173
|
-
function
|
|
2174
|
-
const
|
|
2175
|
-
if (!
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
3370
|
+
// src/auth/lockout.ts
|
|
3371
|
+
var DEFAULT_MAX_LOGIN_ATTEMPTS = 5;
|
|
3372
|
+
var DEFAULT_LOCK_TIME_MS = 10 * 60 * 1e3;
|
|
3373
|
+
function resolveAuthLockoutConfig(collection) {
|
|
3374
|
+
const auth = collection.auth;
|
|
3375
|
+
if (!auth) {
|
|
3376
|
+
return {
|
|
3377
|
+
enabled: false,
|
|
3378
|
+
maxLoginAttempts: DEFAULT_MAX_LOGIN_ATTEMPTS,
|
|
3379
|
+
lockTime: DEFAULT_LOCK_TIME_MS
|
|
3380
|
+
};
|
|
2179
3381
|
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
3382
|
+
if (auth === true) {
|
|
3383
|
+
return {
|
|
3384
|
+
enabled: true,
|
|
3385
|
+
maxLoginAttempts: DEFAULT_MAX_LOGIN_ATTEMPTS,
|
|
3386
|
+
lockTime: DEFAULT_LOCK_TIME_MS
|
|
3387
|
+
};
|
|
3388
|
+
}
|
|
3389
|
+
const maxLoginAttempts = typeof auth.maxLoginAttempts === "number" ? auth.maxLoginAttempts : DEFAULT_MAX_LOGIN_ATTEMPTS;
|
|
3390
|
+
const lockTime = typeof auth.lockTime === "number" && auth.lockTime > 0 ? auth.lockTime : DEFAULT_LOCK_TIME_MS;
|
|
3391
|
+
return {
|
|
3392
|
+
enabled: maxLoginAttempts > 0,
|
|
3393
|
+
maxLoginAttempts,
|
|
3394
|
+
lockTime
|
|
3395
|
+
};
|
|
2185
3396
|
}
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
3397
|
+
function getLockedUntilMs(value) {
|
|
3398
|
+
if (typeof value !== "string") return null;
|
|
3399
|
+
const parsed = Date.parse(value);
|
|
3400
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2189
3401
|
}
|
|
2190
3402
|
|
|
2191
3403
|
// src/services/email-template.ts
|
|
@@ -2285,6 +3497,7 @@ async function getDevSend() {
|
|
|
2285
3497
|
if (_devSend) return _devSend;
|
|
2286
3498
|
if (_devSendPromise) return _devSendPromise;
|
|
2287
3499
|
_devSendPromise = (async () => {
|
|
3500
|
+
const logger = getConfigLogger(void 0, "email");
|
|
2288
3501
|
try {
|
|
2289
3502
|
const nodemailer = await import("nodemailer");
|
|
2290
3503
|
const account = await nodemailer.default.createTestAccount();
|
|
@@ -2293,23 +3506,50 @@ async function getDevSend() {
|
|
|
2293
3506
|
port: 587,
|
|
2294
3507
|
auth: { user: account.user, pass: account.pass }
|
|
2295
3508
|
});
|
|
2296
|
-
|
|
2297
|
-
|
|
3509
|
+
logger.info({
|
|
3510
|
+
msg: "No email config; using Ethereal for development email preview"
|
|
3511
|
+
});
|
|
3512
|
+
logger.info({
|
|
3513
|
+
msg: "Ethereal credentials",
|
|
3514
|
+
loginUrl: "https://ethereal.email",
|
|
3515
|
+
user: account.user,
|
|
3516
|
+
password: account.pass
|
|
3517
|
+
});
|
|
2298
3518
|
_devSend = async ({ to, subject, html }) => {
|
|
2299
3519
|
const info = await transport.sendMail({ from: '"Dyrected Dev" <dev@dyrected.local>', to, subject, html });
|
|
2300
|
-
|
|
3520
|
+
logger.info({
|
|
3521
|
+
msg: "Email preview URL",
|
|
3522
|
+
previewUrl: nodemailer.default.getTestMessageUrl(info)
|
|
3523
|
+
});
|
|
2301
3524
|
};
|
|
2302
3525
|
return _devSend;
|
|
2303
3526
|
} catch {
|
|
2304
|
-
|
|
3527
|
+
logger.warn({
|
|
3528
|
+
msg: "nodemailer not available; development emails will not be sent"
|
|
3529
|
+
});
|
|
2305
3530
|
return null;
|
|
2306
3531
|
}
|
|
2307
3532
|
})();
|
|
2308
3533
|
return _devSendPromise;
|
|
2309
3534
|
}
|
|
2310
3535
|
async function sendEmail(config, payload) {
|
|
3536
|
+
const logger = getConfigLogger(config, "email");
|
|
3537
|
+
const observability = getObservabilityRuntime(config);
|
|
2311
3538
|
if (config.email) {
|
|
2312
|
-
|
|
3539
|
+
try {
|
|
3540
|
+
await config.email.send(payload);
|
|
3541
|
+
} catch (err) {
|
|
3542
|
+
observability?.recordEmailSendFailure({
|
|
3543
|
+
to: payload.to
|
|
3544
|
+
});
|
|
3545
|
+
logger.error({
|
|
3546
|
+
err,
|
|
3547
|
+
msg: "Email provider send failed",
|
|
3548
|
+
to: payload.to,
|
|
3549
|
+
subject: payload.subject
|
|
3550
|
+
});
|
|
3551
|
+
throw err;
|
|
3552
|
+
}
|
|
2313
3553
|
return;
|
|
2314
3554
|
}
|
|
2315
3555
|
if (process.env.NODE_ENV !== "production") {
|
|
@@ -2373,6 +3613,55 @@ var AuthController = class {
|
|
|
2373
3613
|
constructor(collection) {
|
|
2374
3614
|
this.collection = collection;
|
|
2375
3615
|
}
|
|
3616
|
+
sanitizeUser(user) {
|
|
3617
|
+
const {
|
|
3618
|
+
password: _password,
|
|
3619
|
+
loginAttempts: _loginAttempts,
|
|
3620
|
+
lockedUntil: _lockedUntil,
|
|
3621
|
+
...safeUser
|
|
3622
|
+
} = user;
|
|
3623
|
+
return safeUser;
|
|
3624
|
+
}
|
|
3625
|
+
clearLockoutState(c, userId) {
|
|
3626
|
+
const db = c.get("config").db;
|
|
3627
|
+
if (!db) return Promise.resolve(null);
|
|
3628
|
+
return db.update({
|
|
3629
|
+
collection: this.collection.slug,
|
|
3630
|
+
id: userId,
|
|
3631
|
+
data: {
|
|
3632
|
+
loginAttempts: 0,
|
|
3633
|
+
lockedUntil: null
|
|
3634
|
+
}
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3637
|
+
async recordFailedLogin(c, user) {
|
|
3638
|
+
const db = c.get("config").db;
|
|
3639
|
+
if (!db)
|
|
3640
|
+
return { justLocked: false, retryAfterSeconds: null };
|
|
3641
|
+
const lockout = resolveAuthLockoutConfig(this.collection);
|
|
3642
|
+
if (!lockout.enabled) {
|
|
3643
|
+
return { justLocked: false, retryAfterSeconds: null };
|
|
3644
|
+
}
|
|
3645
|
+
const now = Date.now();
|
|
3646
|
+
const lockedUntilMs = getLockedUntilMs(user.lockedUntil);
|
|
3647
|
+
const lockExpired = lockedUntilMs !== null && lockedUntilMs <= now;
|
|
3648
|
+
const currentAttempts = !lockExpired && typeof user.loginAttempts === "number" && user.loginAttempts > 0 ? user.loginAttempts : 0;
|
|
3649
|
+
const loginAttempts = currentAttempts + 1;
|
|
3650
|
+
const justLocked = loginAttempts >= lockout.maxLoginAttempts;
|
|
3651
|
+
const nextLockedUntil = justLocked ? new Date(now + lockout.lockTime).toISOString() : null;
|
|
3652
|
+
await db.update({
|
|
3653
|
+
collection: this.collection.slug,
|
|
3654
|
+
id: String(user.id),
|
|
3655
|
+
data: {
|
|
3656
|
+
loginAttempts,
|
|
3657
|
+
lockedUntil: nextLockedUntil
|
|
3658
|
+
}
|
|
3659
|
+
});
|
|
3660
|
+
return {
|
|
3661
|
+
justLocked,
|
|
3662
|
+
retryAfterSeconds: justLocked ? Math.max(1, Math.ceil(lockout.lockTime / 1e3)) : null
|
|
3663
|
+
};
|
|
3664
|
+
}
|
|
2376
3665
|
// ---------------------------------------------------------------------------
|
|
2377
3666
|
// GET /init
|
|
2378
3667
|
// Checks if the first user needs to be created.
|
|
@@ -2401,11 +3690,17 @@ var AuthController = class {
|
|
|
2401
3690
|
limit: 1
|
|
2402
3691
|
});
|
|
2403
3692
|
if (check.total > 0) {
|
|
2404
|
-
return c.json(
|
|
3693
|
+
return c.json(
|
|
3694
|
+
{ error: true, message: "Initial user already exists." },
|
|
3695
|
+
403
|
|
3696
|
+
);
|
|
2405
3697
|
}
|
|
2406
3698
|
const body = await c.req.json().catch(() => null);
|
|
2407
3699
|
if (!body?.email || !body?.password) {
|
|
2408
|
-
return c.json(
|
|
3700
|
+
return c.json(
|
|
3701
|
+
{ error: true, message: "email and password are required." },
|
|
3702
|
+
400
|
|
3703
|
+
);
|
|
2409
3704
|
}
|
|
2410
3705
|
const hashedPassword = await hashPassword(body.password);
|
|
2411
3706
|
const user = await db.create({
|
|
@@ -2417,16 +3712,23 @@ var AuthController = class {
|
|
|
2417
3712
|
// Default first user to admin
|
|
2418
3713
|
}
|
|
2419
3714
|
});
|
|
2420
|
-
const token = await
|
|
2421
|
-
|
|
3715
|
+
const token = await issueAuthSessionToken({
|
|
3716
|
+
config,
|
|
3717
|
+
userId: user.id,
|
|
2422
3718
|
email: user.email,
|
|
2423
|
-
collection: this.collection.slug
|
|
3719
|
+
collection: this.collection.slug,
|
|
3720
|
+
ip: c.get("clientIp"),
|
|
3721
|
+
authSource: "local"
|
|
2424
3722
|
});
|
|
2425
3723
|
const { subject, html } = buildWelcomeEmail(config, { email: body.email });
|
|
2426
3724
|
sendEmail(config, { to: body.email, subject, html }).catch(
|
|
2427
|
-
(err) =>
|
|
3725
|
+
(err) => getRequestLogger(c, "auth").error({
|
|
3726
|
+
err,
|
|
3727
|
+
msg: "Failed to send welcome email",
|
|
3728
|
+
email: body.email
|
|
3729
|
+
})
|
|
2428
3730
|
);
|
|
2429
|
-
const
|
|
3731
|
+
const safeUser = this.sanitizeUser(user);
|
|
2430
3732
|
return c.json({ token, user: safeUser });
|
|
2431
3733
|
}
|
|
2432
3734
|
// ---------------------------------------------------------------------------
|
|
@@ -2437,7 +3739,10 @@ var AuthController = class {
|
|
|
2437
3739
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2438
3740
|
const body = await c.req.json().catch(() => null);
|
|
2439
3741
|
if (!body?.email || !body?.password) {
|
|
2440
|
-
return c.json(
|
|
3742
|
+
return c.json(
|
|
3743
|
+
{ error: true, message: "email and password are required." },
|
|
3744
|
+
400
|
|
3745
|
+
);
|
|
2441
3746
|
}
|
|
2442
3747
|
const result = await db.find({
|
|
2443
3748
|
collection: this.collection.slug,
|
|
@@ -2446,27 +3751,107 @@ var AuthController = class {
|
|
|
2446
3751
|
});
|
|
2447
3752
|
const user = result.docs[0];
|
|
2448
3753
|
if (!user) {
|
|
2449
|
-
return c.json(
|
|
3754
|
+
return c.json(
|
|
3755
|
+
{ error: true, message: "Invalid email or password." },
|
|
3756
|
+
401
|
|
3757
|
+
);
|
|
3758
|
+
}
|
|
3759
|
+
const lockedUntilMs = getLockedUntilMs(user.lockedUntil);
|
|
3760
|
+
if (lockedUntilMs !== null && lockedUntilMs > Date.now()) {
|
|
3761
|
+
const retryAfterSeconds = Math.max(
|
|
3762
|
+
1,
|
|
3763
|
+
Math.ceil((lockedUntilMs - Date.now()) / 1e3)
|
|
3764
|
+
);
|
|
3765
|
+
c.header("Retry-After", String(retryAfterSeconds));
|
|
3766
|
+
return c.json(
|
|
3767
|
+
{
|
|
3768
|
+
error: true,
|
|
3769
|
+
message: "Too many login attempts. Try again later.",
|
|
3770
|
+
retryAfterSeconds
|
|
3771
|
+
},
|
|
3772
|
+
429
|
|
3773
|
+
);
|
|
2450
3774
|
}
|
|
2451
3775
|
const valid = await verifyPassword(body.password, user.password);
|
|
2452
3776
|
if (!valid) {
|
|
2453
|
-
|
|
3777
|
+
const { justLocked, retryAfterSeconds } = await this.recordFailedLogin(
|
|
3778
|
+
c,
|
|
3779
|
+
user
|
|
3780
|
+
);
|
|
3781
|
+
if (justLocked && retryAfterSeconds) {
|
|
3782
|
+
c.header("Retry-After", String(retryAfterSeconds));
|
|
3783
|
+
return c.json(
|
|
3784
|
+
{
|
|
3785
|
+
error: true,
|
|
3786
|
+
message: "Too many login attempts. Try again later.",
|
|
3787
|
+
retryAfterSeconds
|
|
3788
|
+
},
|
|
3789
|
+
429
|
|
3790
|
+
);
|
|
3791
|
+
}
|
|
3792
|
+
return c.json(
|
|
3793
|
+
{ error: true, message: "Invalid email or password." },
|
|
3794
|
+
401
|
|
3795
|
+
);
|
|
3796
|
+
}
|
|
3797
|
+
if (typeof user.loginAttempts === "number" && user.loginAttempts > 0 || user.lockedUntil != null) {
|
|
3798
|
+
await this.clearLockoutState(c, String(user.id));
|
|
2454
3799
|
}
|
|
2455
|
-
const token = await
|
|
2456
|
-
|
|
3800
|
+
const token = await issueAuthSessionToken({
|
|
3801
|
+
config: c.get("config"),
|
|
3802
|
+
userId: user.id,
|
|
2457
3803
|
email: user.email,
|
|
2458
|
-
collection: this.collection.slug
|
|
3804
|
+
collection: this.collection.slug,
|
|
3805
|
+
ip: c.get("clientIp"),
|
|
3806
|
+
authSource: "local"
|
|
2459
3807
|
});
|
|
2460
|
-
const
|
|
3808
|
+
const safeUser = this.sanitizeUser(user);
|
|
2461
3809
|
return c.json({ token, user: safeUser });
|
|
2462
3810
|
}
|
|
2463
3811
|
// ---------------------------------------------------------------------------
|
|
2464
3812
|
// POST /logout
|
|
2465
|
-
//
|
|
2466
|
-
//
|
|
3813
|
+
// Revoke the current session by default. Pass `?allSessions=true` to revoke
|
|
3814
|
+
// every active session for the current account.
|
|
2467
3815
|
// ---------------------------------------------------------------------------
|
|
2468
3816
|
async logout(c) {
|
|
2469
|
-
|
|
3817
|
+
const requestUser = c.get("user");
|
|
3818
|
+
const tokenPayload = c.get("authTokenPayload");
|
|
3819
|
+
const allSessions = ["1", "true", "yes"].includes(
|
|
3820
|
+
(c.req.query("allSessions") || "").toLowerCase()
|
|
3821
|
+
);
|
|
3822
|
+
if (!requestUser) {
|
|
3823
|
+
if (allSessions) {
|
|
3824
|
+
return c.json(
|
|
3825
|
+
{ error: true, message: "Authentication required." },
|
|
3826
|
+
401
|
|
3827
|
+
);
|
|
3828
|
+
}
|
|
3829
|
+
return c.json({
|
|
3830
|
+
success: true,
|
|
3831
|
+
message: "Logged out. Discard your token."
|
|
3832
|
+
});
|
|
3833
|
+
}
|
|
3834
|
+
if (allSessions) {
|
|
3835
|
+
await revokeAllAuthSessions(c.get("config"), {
|
|
3836
|
+
userId: requestUser.sub,
|
|
3837
|
+
collection: this.collection.slug
|
|
3838
|
+
});
|
|
3839
|
+
return c.json({
|
|
3840
|
+
success: true,
|
|
3841
|
+
message: "All sessions have been logged out."
|
|
3842
|
+
});
|
|
3843
|
+
}
|
|
3844
|
+
if (tokenPayload?.sid) {
|
|
3845
|
+
await revokeAuthSession(c.get("config"), tokenPayload.sid);
|
|
3846
|
+
return c.json({
|
|
3847
|
+
success: true,
|
|
3848
|
+
message: "Logged out."
|
|
3849
|
+
});
|
|
3850
|
+
}
|
|
3851
|
+
return c.json({
|
|
3852
|
+
success: true,
|
|
3853
|
+
message: "Logged out. Discard your token."
|
|
3854
|
+
});
|
|
2470
3855
|
}
|
|
2471
3856
|
// ---------------------------------------------------------------------------
|
|
2472
3857
|
// GET /me
|
|
@@ -2478,11 +3863,14 @@ var AuthController = class {
|
|
|
2478
3863
|
if (!requestUser) {
|
|
2479
3864
|
return c.json({ error: true, message: "Authentication required." }, 401);
|
|
2480
3865
|
}
|
|
2481
|
-
const user = await db.findOne({
|
|
3866
|
+
const user = await db.findOne({
|
|
3867
|
+
collection: this.collection.slug,
|
|
3868
|
+
id: requestUser.sub
|
|
3869
|
+
});
|
|
2482
3870
|
if (!user) {
|
|
2483
3871
|
return c.json({ error: true, message: "User not found." }, 404);
|
|
2484
3872
|
}
|
|
2485
|
-
const
|
|
3873
|
+
const safeUser = this.sanitizeUser(user);
|
|
2486
3874
|
return c.json(safeUser);
|
|
2487
3875
|
}
|
|
2488
3876
|
// ---------------------------------------------------------------------------
|
|
@@ -2493,10 +3881,29 @@ var AuthController = class {
|
|
|
2493
3881
|
if (!requestUser) {
|
|
2494
3882
|
return c.json({ error: true, message: "Authentication required." }, 401);
|
|
2495
3883
|
}
|
|
2496
|
-
|
|
2497
|
-
|
|
3884
|
+
if (!requestUser.email) {
|
|
3885
|
+
return c.json(
|
|
3886
|
+
{ error: true, message: "Authenticated user is missing an email." },
|
|
3887
|
+
400
|
|
3888
|
+
);
|
|
3889
|
+
}
|
|
3890
|
+
const tokenPayload = c.get("authTokenPayload");
|
|
3891
|
+
if (tokenPayload?.sid) {
|
|
3892
|
+
const token2 = await signCollectionToken({
|
|
3893
|
+
sub: requestUser.sub,
|
|
3894
|
+
email: requestUser.email,
|
|
3895
|
+
collection: this.collection.slug,
|
|
3896
|
+
sid: tokenPayload.sid
|
|
3897
|
+
});
|
|
3898
|
+
return c.json({ token: token2 });
|
|
3899
|
+
}
|
|
3900
|
+
const token = await issueAuthSessionToken({
|
|
3901
|
+
config: c.get("config"),
|
|
3902
|
+
userId: requestUser.sub,
|
|
2498
3903
|
email: requestUser.email,
|
|
2499
|
-
collection: this.collection.slug
|
|
3904
|
+
collection: this.collection.slug,
|
|
3905
|
+
ip: c.get("clientIp"),
|
|
3906
|
+
authSource: "local"
|
|
2500
3907
|
});
|
|
2501
3908
|
return c.json({ token });
|
|
2502
3909
|
}
|
|
@@ -2521,16 +3928,28 @@ var AuthController = class {
|
|
|
2521
3928
|
const user = result.docs[0];
|
|
2522
3929
|
if (user) {
|
|
2523
3930
|
const resetToken = await signCollectionToken(
|
|
2524
|
-
{
|
|
3931
|
+
{
|
|
3932
|
+
sub: user.id,
|
|
3933
|
+
email: user.email,
|
|
3934
|
+
collection: this.collection.slug,
|
|
3935
|
+
purpose: "reset"
|
|
3936
|
+
},
|
|
2525
3937
|
"1h"
|
|
2526
3938
|
);
|
|
2527
3939
|
const resetUrl = body?.resetUrl;
|
|
2528
3940
|
const url = resetUrl ? `${resetUrl}${resetUrl.includes("?") ? "&" : "?"}token=${encodeURIComponent(resetToken)}` : void 0;
|
|
2529
3941
|
try {
|
|
2530
|
-
const { subject, html } = buildResetPasswordEmail(config, {
|
|
3942
|
+
const { subject, html } = buildResetPasswordEmail(config, {
|
|
3943
|
+
token: resetToken,
|
|
3944
|
+
url
|
|
3945
|
+
});
|
|
2531
3946
|
await sendEmail(config, { to: user.email, subject, html });
|
|
2532
3947
|
} catch (err) {
|
|
2533
|
-
|
|
3948
|
+
getRequestLogger(c, "auth").error({
|
|
3949
|
+
err,
|
|
3950
|
+
msg: "Failed to send password reset email",
|
|
3951
|
+
email: user.email
|
|
3952
|
+
});
|
|
2534
3953
|
}
|
|
2535
3954
|
}
|
|
2536
3955
|
return c.json({
|
|
@@ -2549,28 +3968,54 @@ var AuthController = class {
|
|
|
2549
3968
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2550
3969
|
const body = await c.req.json().catch(() => null);
|
|
2551
3970
|
if (!body?.token || !body?.password) {
|
|
2552
|
-
return c.json(
|
|
3971
|
+
return c.json(
|
|
3972
|
+
{ error: true, message: "token and password are required." },
|
|
3973
|
+
400
|
|
3974
|
+
);
|
|
2553
3975
|
}
|
|
2554
3976
|
let payload;
|
|
2555
3977
|
try {
|
|
2556
3978
|
payload = await verifyCollectionToken(body.token);
|
|
2557
3979
|
} catch {
|
|
2558
|
-
return c.json(
|
|
3980
|
+
return c.json(
|
|
3981
|
+
{ error: true, message: "Reset token is invalid or has expired." },
|
|
3982
|
+
400
|
|
3983
|
+
);
|
|
2559
3984
|
}
|
|
2560
3985
|
if (payload.collection !== this.collection.slug || payload.purpose !== "reset") {
|
|
2561
|
-
return c.json(
|
|
3986
|
+
return c.json(
|
|
3987
|
+
{ error: true, message: "Reset token is invalid or has expired." },
|
|
3988
|
+
400
|
|
3989
|
+
);
|
|
2562
3990
|
}
|
|
2563
3991
|
const hashedPassword = await hashPassword(body.password);
|
|
2564
3992
|
await db.update({
|
|
2565
3993
|
collection: this.collection.slug,
|
|
2566
3994
|
id: payload.sub,
|
|
2567
|
-
data: {
|
|
3995
|
+
data: {
|
|
3996
|
+
password: hashedPassword,
|
|
3997
|
+
loginAttempts: 0,
|
|
3998
|
+
lockedUntil: null
|
|
3999
|
+
}
|
|
4000
|
+
});
|
|
4001
|
+
await revokeAllAuthSessions(config, {
|
|
4002
|
+
userId: payload.sub,
|
|
4003
|
+
collection: this.collection.slug
|
|
4004
|
+
});
|
|
4005
|
+
const { subject, html } = buildPasswordChangedEmail(config, {
|
|
4006
|
+
email: payload.email
|
|
2568
4007
|
});
|
|
2569
|
-
const { subject, html } = buildPasswordChangedEmail(config, { email: payload.email });
|
|
2570
4008
|
sendEmail(config, { to: payload.email, subject, html }).catch(
|
|
2571
|
-
(err) =>
|
|
4009
|
+
(err) => getRequestLogger(c, "auth").error({
|
|
4010
|
+
err,
|
|
4011
|
+
msg: "Failed to send password-changed email",
|
|
4012
|
+
email: payload.email
|
|
4013
|
+
})
|
|
2572
4014
|
);
|
|
2573
|
-
return c.json({
|
|
4015
|
+
return c.json({
|
|
4016
|
+
success: true,
|
|
4017
|
+
message: "Password has been reset. You can now log in."
|
|
4018
|
+
});
|
|
2574
4019
|
}
|
|
2575
4020
|
// ---------------------------------------------------------------------------
|
|
2576
4021
|
// POST /invite
|
|
@@ -2594,10 +4039,18 @@ var AuthController = class {
|
|
|
2594
4039
|
limit: 1
|
|
2595
4040
|
});
|
|
2596
4041
|
if (existing.total > 0) {
|
|
2597
|
-
return c.json(
|
|
4042
|
+
return c.json(
|
|
4043
|
+
{ error: true, message: "An account with that email already exists." },
|
|
4044
|
+
409
|
|
4045
|
+
);
|
|
2598
4046
|
}
|
|
2599
4047
|
const inviteToken = await signCollectionToken(
|
|
2600
|
-
{
|
|
4048
|
+
{
|
|
4049
|
+
sub: body.email,
|
|
4050
|
+
email: body.email,
|
|
4051
|
+
collection: this.collection.slug,
|
|
4052
|
+
purpose: "invite"
|
|
4053
|
+
},
|
|
2601
4054
|
"7d"
|
|
2602
4055
|
);
|
|
2603
4056
|
try {
|
|
@@ -2607,7 +4060,11 @@ var AuthController = class {
|
|
|
2607
4060
|
});
|
|
2608
4061
|
await sendEmail(config, { to: body.email, subject, html });
|
|
2609
4062
|
} catch (err) {
|
|
2610
|
-
|
|
4063
|
+
getRequestLogger(c, "auth").error({
|
|
4064
|
+
err,
|
|
4065
|
+
msg: "Failed to send invite email",
|
|
4066
|
+
email: body.email
|
|
4067
|
+
});
|
|
2611
4068
|
}
|
|
2612
4069
|
return c.json({ success: true, message: `Invite sent to ${body.email}.` });
|
|
2613
4070
|
}
|
|
@@ -2622,16 +4079,25 @@ var AuthController = class {
|
|
|
2622
4079
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2623
4080
|
const body = await c.req.json().catch(() => null);
|
|
2624
4081
|
if (!body?.token || !body?.password) {
|
|
2625
|
-
return c.json(
|
|
4082
|
+
return c.json(
|
|
4083
|
+
{ error: true, message: "token and password are required." },
|
|
4084
|
+
400
|
|
4085
|
+
);
|
|
2626
4086
|
}
|
|
2627
4087
|
let payload;
|
|
2628
4088
|
try {
|
|
2629
4089
|
payload = await verifyCollectionToken(body.token);
|
|
2630
4090
|
} catch {
|
|
2631
|
-
return c.json(
|
|
4091
|
+
return c.json(
|
|
4092
|
+
{ error: true, message: "Invite token is invalid or has expired." },
|
|
4093
|
+
400
|
|
4094
|
+
);
|
|
2632
4095
|
}
|
|
2633
4096
|
if (payload.collection !== this.collection.slug || payload.purpose !== "invite") {
|
|
2634
|
-
return c.json(
|
|
4097
|
+
return c.json(
|
|
4098
|
+
{ error: true, message: "Invite token is invalid or has expired." },
|
|
4099
|
+
400
|
|
4100
|
+
);
|
|
2635
4101
|
}
|
|
2636
4102
|
const inviteeEmail = payload.sub;
|
|
2637
4103
|
const existing = await db.find({
|
|
@@ -2640,7 +4106,10 @@ var AuthController = class {
|
|
|
2640
4106
|
limit: 1
|
|
2641
4107
|
});
|
|
2642
4108
|
if (existing.total > 0) {
|
|
2643
|
-
return c.json(
|
|
4109
|
+
return c.json(
|
|
4110
|
+
{ error: true, message: "An account with that email already exists." },
|
|
4111
|
+
409
|
|
4112
|
+
);
|
|
2644
4113
|
}
|
|
2645
4114
|
const { token: _t, password: _p, ...extraFields } = body;
|
|
2646
4115
|
const hashedPassword = await hashPassword(body.password);
|
|
@@ -2648,16 +4117,25 @@ var AuthController = class {
|
|
|
2648
4117
|
collection: this.collection.slug,
|
|
2649
4118
|
data: { ...extraFields, email: inviteeEmail, password: hashedPassword }
|
|
2650
4119
|
});
|
|
2651
|
-
const sessionToken = await
|
|
2652
|
-
|
|
4120
|
+
const sessionToken = await issueAuthSessionToken({
|
|
4121
|
+
config,
|
|
4122
|
+
userId: user.id,
|
|
2653
4123
|
email: inviteeEmail,
|
|
2654
|
-
collection: this.collection.slug
|
|
4124
|
+
collection: this.collection.slug,
|
|
4125
|
+
ip: c.get("clientIp"),
|
|
4126
|
+
authSource: "local"
|
|
4127
|
+
});
|
|
4128
|
+
const { subject, html } = buildWelcomeEmail(config, {
|
|
4129
|
+
email: inviteeEmail
|
|
2655
4130
|
});
|
|
2656
|
-
const { subject, html } = buildWelcomeEmail(config, { email: inviteeEmail });
|
|
2657
4131
|
sendEmail(config, { to: inviteeEmail, subject, html }).catch(
|
|
2658
|
-
(err) =>
|
|
4132
|
+
(err) => getRequestLogger(c, "auth").error({
|
|
4133
|
+
err,
|
|
4134
|
+
msg: "Failed to send welcome email",
|
|
4135
|
+
email: inviteeEmail
|
|
4136
|
+
})
|
|
2659
4137
|
);
|
|
2660
|
-
const
|
|
4138
|
+
const safeUser = this.sanitizeUser(user);
|
|
2661
4139
|
return c.json({ token: sessionToken, user: safeUser }, 201);
|
|
2662
4140
|
}
|
|
2663
4141
|
};
|
|
@@ -2673,13 +4151,21 @@ var AdminAuthController = class {
|
|
|
2673
4151
|
config;
|
|
2674
4152
|
async providers(c) {
|
|
2675
4153
|
const requestConfig = await this.getRequestConfig(c);
|
|
2676
|
-
return c.json(
|
|
4154
|
+
return c.json(
|
|
4155
|
+
getPublicAdminAuthConfig(
|
|
4156
|
+
requestConfig.adminAuth,
|
|
4157
|
+
requestConfig.collections
|
|
4158
|
+
)
|
|
4159
|
+
);
|
|
2677
4160
|
}
|
|
2678
4161
|
async start(c) {
|
|
2679
4162
|
const requestConfig = await this.getRequestConfig(c);
|
|
2680
4163
|
const provider = this.getProvider(requestConfig, c.req.param("provider"));
|
|
2681
4164
|
if (!provider) {
|
|
2682
|
-
return c.json(
|
|
4165
|
+
return c.json(
|
|
4166
|
+
{ error: true, message: "Admin auth provider not found." },
|
|
4167
|
+
404
|
|
4168
|
+
);
|
|
2683
4169
|
}
|
|
2684
4170
|
const siteId = this.getSiteId(c);
|
|
2685
4171
|
const returnTo = this.normalizeReturnTo(c.req.query("returnTo"), c);
|
|
@@ -2693,9 +4179,15 @@ var AdminAuthController = class {
|
|
|
2693
4179
|
return c.redirect(redirectUrl, 302);
|
|
2694
4180
|
}
|
|
2695
4181
|
if (!provider.startUrl) {
|
|
2696
|
-
return c.json(
|
|
4182
|
+
return c.json(
|
|
4183
|
+
{ error: true, message: "This provider does not expose a start URL." },
|
|
4184
|
+
400
|
|
4185
|
+
);
|
|
2697
4186
|
}
|
|
2698
|
-
const url = this.buildCustomProviderStartUrl(
|
|
4187
|
+
const url = this.buildCustomProviderStartUrl(
|
|
4188
|
+
provider.startUrl,
|
|
4189
|
+
new URL(c.req.url).origin
|
|
4190
|
+
);
|
|
2699
4191
|
if (!url) {
|
|
2700
4192
|
return c.json(
|
|
2701
4193
|
{
|
|
@@ -2714,13 +4206,23 @@ var AdminAuthController = class {
|
|
|
2714
4206
|
const requestConfig = await this.getRequestConfig(c);
|
|
2715
4207
|
const provider = this.getProvider(requestConfig, c.req.param("provider"));
|
|
2716
4208
|
if (!provider) {
|
|
2717
|
-
return c.json(
|
|
4209
|
+
return c.json(
|
|
4210
|
+
{ error: true, message: "Admin auth provider not found." },
|
|
4211
|
+
404
|
|
4212
|
+
);
|
|
2718
4213
|
}
|
|
2719
4214
|
try {
|
|
2720
|
-
const exchange = await this.completeProviderAuth(
|
|
4215
|
+
const exchange = await this.completeProviderAuth(
|
|
4216
|
+
requestConfig,
|
|
4217
|
+
provider,
|
|
4218
|
+
c
|
|
4219
|
+
);
|
|
2721
4220
|
const redirectUrl = new URL(exchange.returnTo);
|
|
2722
4221
|
redirectUrl.searchParams.set("dyrectedExternalToken", exchange.token);
|
|
2723
|
-
redirectUrl.searchParams.set(
|
|
4222
|
+
redirectUrl.searchParams.set(
|
|
4223
|
+
"dyrectedAdminCollection",
|
|
4224
|
+
exchange.collectionSlug
|
|
4225
|
+
);
|
|
2724
4226
|
return c.redirect(redirectUrl.toString(), 302);
|
|
2725
4227
|
} catch (error) {
|
|
2726
4228
|
const message = error instanceof Error ? error.message : "External authentication failed.";
|
|
@@ -2734,10 +4236,17 @@ var AdminAuthController = class {
|
|
|
2734
4236
|
const requestConfig = await this.getRequestConfig(c);
|
|
2735
4237
|
const provider = this.getProvider(requestConfig, c.req.param("provider"));
|
|
2736
4238
|
if (!provider) {
|
|
2737
|
-
return c.json(
|
|
4239
|
+
return c.json(
|
|
4240
|
+
{ error: true, message: "Admin auth provider not found." },
|
|
4241
|
+
404
|
|
4242
|
+
);
|
|
2738
4243
|
}
|
|
2739
4244
|
try {
|
|
2740
|
-
const exchange = await this.completeProviderAuth(
|
|
4245
|
+
const exchange = await this.completeProviderAuth(
|
|
4246
|
+
requestConfig,
|
|
4247
|
+
provider,
|
|
4248
|
+
c
|
|
4249
|
+
);
|
|
2741
4250
|
return c.json({
|
|
2742
4251
|
token: exchange.token,
|
|
2743
4252
|
collectionSlug: exchange.collectionSlug,
|
|
@@ -2750,7 +4259,44 @@ var AdminAuthController = class {
|
|
|
2750
4259
|
}
|
|
2751
4260
|
}
|
|
2752
4261
|
async logout(c) {
|
|
2753
|
-
|
|
4262
|
+
const requestUser = c.get("user");
|
|
4263
|
+
const tokenPayload = c.get("authTokenPayload");
|
|
4264
|
+
const allSessions = ["1", "true", "yes"].includes(
|
|
4265
|
+
(c.req.query("allSessions") || "").toLowerCase()
|
|
4266
|
+
);
|
|
4267
|
+
if (!requestUser) {
|
|
4268
|
+
if (allSessions) {
|
|
4269
|
+
return c.json(
|
|
4270
|
+
{ error: true, message: "Authentication required." },
|
|
4271
|
+
401
|
|
4272
|
+
);
|
|
4273
|
+
}
|
|
4274
|
+
return c.json({
|
|
4275
|
+
success: true,
|
|
4276
|
+
message: "Logged out. Discard your token."
|
|
4277
|
+
});
|
|
4278
|
+
}
|
|
4279
|
+
if (allSessions) {
|
|
4280
|
+
await revokeAllAuthSessions(c.get("config"), {
|
|
4281
|
+
userId: requestUser.sub,
|
|
4282
|
+
collection: requestUser.collection
|
|
4283
|
+
});
|
|
4284
|
+
return c.json({
|
|
4285
|
+
success: true,
|
|
4286
|
+
message: "All sessions have been logged out."
|
|
4287
|
+
});
|
|
4288
|
+
}
|
|
4289
|
+
if (tokenPayload?.sid) {
|
|
4290
|
+
await revokeAuthSession(c.get("config"), tokenPayload.sid);
|
|
4291
|
+
return c.json({
|
|
4292
|
+
success: true,
|
|
4293
|
+
message: "Logged out."
|
|
4294
|
+
});
|
|
4295
|
+
}
|
|
4296
|
+
return c.json({
|
|
4297
|
+
success: true,
|
|
4298
|
+
message: "Logged out. Discard your token."
|
|
4299
|
+
});
|
|
2754
4300
|
}
|
|
2755
4301
|
getProvider(config, id) {
|
|
2756
4302
|
if (config.adminAuth?.mode !== "external") return void 0;
|
|
@@ -2766,7 +4312,12 @@ var AdminAuthController = class {
|
|
|
2766
4312
|
}
|
|
2767
4313
|
const db = c.get("config").db;
|
|
2768
4314
|
if (!db) throw new Error("Database not configured.");
|
|
2769
|
-
let user = await this.findUserByExternalIdentity(
|
|
4315
|
+
let user = await this.findUserByExternalIdentity(
|
|
4316
|
+
db,
|
|
4317
|
+
adminCollection.slug,
|
|
4318
|
+
provider.id,
|
|
4319
|
+
identity.sub
|
|
4320
|
+
) ?? (identity.email ? (await db.find({
|
|
2770
4321
|
collection: adminCollection.slug,
|
|
2771
4322
|
where: { email: identity.email },
|
|
2772
4323
|
limit: 1
|
|
@@ -2774,9 +4325,18 @@ var AdminAuthController = class {
|
|
|
2774
4325
|
const provisioningMode = requestConfig.adminAuth?.provisioningMode ?? "jit_plus_membership_management";
|
|
2775
4326
|
const allowJitProvisioning = provisioningMode !== "preprovisioned_only" && provider.allowJitProvisioning !== false;
|
|
2776
4327
|
if (!user && !allowJitProvisioning) {
|
|
2777
|
-
throw new Error(
|
|
4328
|
+
throw new Error(
|
|
4329
|
+
"This account has not been provisioned for admin access."
|
|
4330
|
+
);
|
|
2778
4331
|
}
|
|
2779
|
-
const access = await this.resolveAccess(
|
|
4332
|
+
const access = await this.resolveAccess(
|
|
4333
|
+
requestConfig,
|
|
4334
|
+
identity,
|
|
4335
|
+
provider.id,
|
|
4336
|
+
siteId,
|
|
4337
|
+
c,
|
|
4338
|
+
user
|
|
4339
|
+
);
|
|
2780
4340
|
if (!access.allowed) {
|
|
2781
4341
|
throw new Error("Access denied for this site.");
|
|
2782
4342
|
}
|
|
@@ -2784,7 +4344,12 @@ var AdminAuthController = class {
|
|
|
2784
4344
|
user = await db.create({
|
|
2785
4345
|
collection: adminCollection.slug,
|
|
2786
4346
|
data: {
|
|
2787
|
-
...await this.buildUserData(
|
|
4347
|
+
...await this.buildUserData(
|
|
4348
|
+
identity,
|
|
4349
|
+
provider.id,
|
|
4350
|
+
access.roles,
|
|
4351
|
+
access.data
|
|
4352
|
+
),
|
|
2788
4353
|
password: await hashPassword((0, import_node_crypto2.randomBytes)(32).toString("hex"))
|
|
2789
4354
|
}
|
|
2790
4355
|
});
|
|
@@ -2792,18 +4357,25 @@ var AdminAuthController = class {
|
|
|
2792
4357
|
user = await db.update({
|
|
2793
4358
|
collection: adminCollection.slug,
|
|
2794
4359
|
id: user.id,
|
|
2795
|
-
data: await this.buildUserData(
|
|
4360
|
+
data: await this.buildUserData(
|
|
4361
|
+
identity,
|
|
4362
|
+
provider.id,
|
|
4363
|
+
access.roles,
|
|
4364
|
+
access.data
|
|
4365
|
+
)
|
|
2796
4366
|
});
|
|
2797
4367
|
}
|
|
2798
4368
|
if (!user?.email) {
|
|
2799
4369
|
throw new Error("Authenticated admin user is missing an email address.");
|
|
2800
4370
|
}
|
|
2801
|
-
const token = await
|
|
2802
|
-
|
|
4371
|
+
const token = await issueAuthSessionToken({
|
|
4372
|
+
config: c.get("config"),
|
|
4373
|
+
userId: user.id,
|
|
2803
4374
|
email: user.email,
|
|
2804
4375
|
collection: adminCollection.slug,
|
|
2805
4376
|
providerId: provider.id,
|
|
2806
|
-
authSource: "external"
|
|
4377
|
+
authSource: "external",
|
|
4378
|
+
ip: c.get("clientIp")
|
|
2807
4379
|
});
|
|
2808
4380
|
return {
|
|
2809
4381
|
token,
|
|
@@ -2857,14 +4429,10 @@ var AdminAuthController = class {
|
|
|
2857
4429
|
async getRequestConfig(c) {
|
|
2858
4430
|
const siteId = this.getSiteId(c);
|
|
2859
4431
|
if (!siteId || !this.config.onSchemaFetch) return this.config;
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
...dynamic.adminAuth ? { adminAuth: { ...this.config.adminAuth, ...dynamic.adminAuth } } : {},
|
|
2865
|
-
collections: dynamic.collections ? [...this.config.collections, ...dynamic.collections] : this.config.collections,
|
|
2866
|
-
globals: dynamic.globals ? [...this.config.globals, ...dynamic.globals] : this.config.globals
|
|
2867
|
-
};
|
|
4432
|
+
return mergeDynamicConfig(
|
|
4433
|
+
this.config,
|
|
4434
|
+
await this.config.onSchemaFetch(siteId)
|
|
4435
|
+
);
|
|
2868
4436
|
}
|
|
2869
4437
|
async buildOidcStartUrl(provider, returnTo, siteId, origin) {
|
|
2870
4438
|
const discovery = await this.getOidcDiscovery(provider);
|
|
@@ -2875,11 +4443,19 @@ var AdminAuthController = class {
|
|
|
2875
4443
|
siteId,
|
|
2876
4444
|
nonce
|
|
2877
4445
|
});
|
|
2878
|
-
const url = new URL(
|
|
4446
|
+
const url = new URL(
|
|
4447
|
+
provider.authorizationEndpoint || discovery.authorization_endpoint
|
|
4448
|
+
);
|
|
2879
4449
|
url.searchParams.set("response_type", "code");
|
|
2880
4450
|
url.searchParams.set("client_id", provider.clientId);
|
|
2881
|
-
url.searchParams.set(
|
|
2882
|
-
|
|
4451
|
+
url.searchParams.set(
|
|
4452
|
+
"redirect_uri",
|
|
4453
|
+
provider.redirectUri || this.defaultCallbackPath(provider.id, origin)
|
|
4454
|
+
);
|
|
4455
|
+
url.searchParams.set(
|
|
4456
|
+
"scope",
|
|
4457
|
+
(provider.scopes ?? ["openid", "profile", "email"]).join(" ")
|
|
4458
|
+
);
|
|
2883
4459
|
url.searchParams.set("state", state);
|
|
2884
4460
|
url.searchParams.set("nonce", nonce);
|
|
2885
4461
|
return url.toString();
|
|
@@ -2891,23 +4467,26 @@ var AdminAuthController = class {
|
|
|
2891
4467
|
const parsedState = await this.verifyState(state, provider.id);
|
|
2892
4468
|
const discovery = await this.getOidcDiscovery(provider);
|
|
2893
4469
|
const redirectUri = provider.redirectUri || this.defaultCallbackPath(provider.id, new URL(c.req.url).origin);
|
|
2894
|
-
const tokenResponse = await fetch(
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
4470
|
+
const tokenResponse = await fetch(
|
|
4471
|
+
provider.tokenEndpoint || discovery.token_endpoint,
|
|
4472
|
+
{
|
|
4473
|
+
method: "POST",
|
|
4474
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
4475
|
+
body: new URLSearchParams({
|
|
4476
|
+
grant_type: "authorization_code",
|
|
4477
|
+
code,
|
|
4478
|
+
redirect_uri: redirectUri,
|
|
4479
|
+
client_id: provider.clientId,
|
|
4480
|
+
client_secret: provider.clientSecret
|
|
4481
|
+
})
|
|
4482
|
+
}
|
|
4483
|
+
);
|
|
2905
4484
|
if (!tokenResponse.ok) {
|
|
2906
4485
|
throw new Error("OIDC token exchange failed.");
|
|
2907
4486
|
}
|
|
2908
4487
|
const tokenBody = await tokenResponse.json();
|
|
2909
4488
|
const idToken = tokenBody.id_token;
|
|
2910
|
-
let claims
|
|
4489
|
+
let claims;
|
|
2911
4490
|
if (idToken) {
|
|
2912
4491
|
const jwks = (0, import_jose2.createRemoteJWKSet)(new URL(discovery.jwks_uri));
|
|
2913
4492
|
const { payload } = await (0, import_jose2.jwtVerify)(idToken, jwks, {
|
|
@@ -2919,9 +4498,12 @@ var AdminAuthController = class {
|
|
|
2919
4498
|
}
|
|
2920
4499
|
claims = payload;
|
|
2921
4500
|
} else if (tokenBody.access_token && (provider.userInfoEndpoint || discovery.userinfo_endpoint)) {
|
|
2922
|
-
const userInfo = await fetch(
|
|
2923
|
-
|
|
2924
|
-
|
|
4501
|
+
const userInfo = await fetch(
|
|
4502
|
+
provider.userInfoEndpoint || discovery.userinfo_endpoint,
|
|
4503
|
+
{
|
|
4504
|
+
headers: { Authorization: `Bearer ${tokenBody.access_token}` }
|
|
4505
|
+
}
|
|
4506
|
+
);
|
|
2925
4507
|
if (!userInfo.ok) throw new Error("OIDC userinfo request failed.");
|
|
2926
4508
|
claims = await userInfo.json();
|
|
2927
4509
|
} else {
|
|
@@ -2966,7 +4548,10 @@ var AdminAuthController = class {
|
|
|
2966
4548
|
}
|
|
2967
4549
|
}
|
|
2968
4550
|
return {
|
|
2969
|
-
identity: this.mapClaims(
|
|
4551
|
+
identity: this.mapClaims(
|
|
4552
|
+
claims,
|
|
4553
|
+
provider.claimMapping
|
|
4554
|
+
),
|
|
2970
4555
|
returnTo: this.normalizeReturnTo(
|
|
2971
4556
|
body?.returnTo || c.req.query("returnTo"),
|
|
2972
4557
|
c
|
|
@@ -2990,9 +4575,15 @@ var AdminAuthController = class {
|
|
|
2990
4575
|
email: this.readStringClaim(claims[emailKey]),
|
|
2991
4576
|
name: this.readStringClaim(claims[nameKey]),
|
|
2992
4577
|
roles: this.readStringArrayClaim(rolesKey ? claims[rolesKey] : void 0),
|
|
2993
|
-
groups: this.readStringArrayClaim(
|
|
2994
|
-
|
|
2995
|
-
|
|
4578
|
+
groups: this.readStringArrayClaim(
|
|
4579
|
+
groupsKey ? claims[groupsKey] : void 0
|
|
4580
|
+
),
|
|
4581
|
+
siteIds: this.readStringArrayClaim(
|
|
4582
|
+
siteIdsKey ? claims[siteIdsKey] : void 0
|
|
4583
|
+
),
|
|
4584
|
+
workspaceIds: this.readStringArrayClaim(
|
|
4585
|
+
workspaceIdsKey ? claims[workspaceIdsKey] : void 0
|
|
4586
|
+
),
|
|
2996
4587
|
rawClaims: claims
|
|
2997
4588
|
};
|
|
2998
4589
|
}
|
|
@@ -3002,7 +4593,9 @@ var AdminAuthController = class {
|
|
|
3002
4593
|
readStringArrayClaim(value) {
|
|
3003
4594
|
if (!value) return void 0;
|
|
3004
4595
|
if (Array.isArray(value)) {
|
|
3005
|
-
return value.filter(
|
|
4596
|
+
return value.filter(
|
|
4597
|
+
(entry) => typeof entry === "string"
|
|
4598
|
+
);
|
|
3006
4599
|
}
|
|
3007
4600
|
if (typeof value === "string") return [value];
|
|
3008
4601
|
return void 0;
|
|
@@ -3012,7 +4605,8 @@ var AdminAuthController = class {
|
|
|
3012
4605
|
provider.issuer.replace(/\/$/, "") + "/.well-known/openid-configuration"
|
|
3013
4606
|
);
|
|
3014
4607
|
const response = await fetch(discoveryUrl.toString());
|
|
3015
|
-
if (!response.ok)
|
|
4608
|
+
if (!response.ok)
|
|
4609
|
+
throw new Error("Failed to load OIDC discovery document.");
|
|
3016
4610
|
return response.json();
|
|
3017
4611
|
}
|
|
3018
4612
|
defaultCallbackPath(providerId, origin = "") {
|
|
@@ -3069,7 +4663,10 @@ var PreviewController = class {
|
|
|
3069
4663
|
async createToken(c) {
|
|
3070
4664
|
const body = await c.req.json().catch(() => null);
|
|
3071
4665
|
if (!body?.collectionSlug || !body?.data) {
|
|
3072
|
-
return c.json(
|
|
4666
|
+
return c.json(
|
|
4667
|
+
{ error: true, message: "collectionSlug and data are required." },
|
|
4668
|
+
400
|
|
4669
|
+
);
|
|
3073
4670
|
}
|
|
3074
4671
|
const token = await new import_jose3.SignJWT({
|
|
3075
4672
|
collectionSlug: body.collectionSlug,
|
|
@@ -3086,13 +4683,19 @@ var PreviewController = class {
|
|
|
3086
4683
|
async getData(c) {
|
|
3087
4684
|
const token = c.req.query("token");
|
|
3088
4685
|
if (!token) {
|
|
3089
|
-
return c.json(
|
|
4686
|
+
return c.json(
|
|
4687
|
+
{ error: true, message: "token query parameter is required." },
|
|
4688
|
+
400
|
|
4689
|
+
);
|
|
3090
4690
|
}
|
|
3091
4691
|
try {
|
|
3092
4692
|
const { payload } = await (0, import_jose3.jwtVerify)(token, this.getSecret());
|
|
3093
4693
|
return c.json(payload);
|
|
3094
|
-
} catch (
|
|
3095
|
-
return c.json(
|
|
4694
|
+
} catch (_err) {
|
|
4695
|
+
return c.json(
|
|
4696
|
+
{ error: true, message: "Invalid or expired preview token." },
|
|
4697
|
+
401
|
|
4698
|
+
);
|
|
3096
4699
|
}
|
|
3097
4700
|
}
|
|
3098
4701
|
};
|
|
@@ -3168,9 +4771,15 @@ var AuditController = class {
|
|
|
3168
4771
|
if (!access.allowed) {
|
|
3169
4772
|
return { denied: true, where: {} };
|
|
3170
4773
|
}
|
|
3171
|
-
let where = {
|
|
4774
|
+
let where = {
|
|
4775
|
+
collection: { equals: collection.slug }
|
|
4776
|
+
};
|
|
3172
4777
|
if (access.constraint) {
|
|
3173
|
-
const ids = await this.collectAccessibleDocumentIds(
|
|
4778
|
+
const ids = await this.collectAccessibleDocumentIds(
|
|
4779
|
+
config.db,
|
|
4780
|
+
collection,
|
|
4781
|
+
access.constraint
|
|
4782
|
+
);
|
|
3174
4783
|
where = mergeWhereConstraint(where, { documentId: { in: ids } });
|
|
3175
4784
|
}
|
|
3176
4785
|
return { denied: false, where };
|
|
@@ -3183,8 +4792,11 @@ var AuditController = class {
|
|
|
3183
4792
|
}
|
|
3184
4793
|
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
3185
4794
|
if (config.onSchemaFetch && siteId) {
|
|
3186
|
-
const
|
|
3187
|
-
|
|
4795
|
+
const requestConfig = mergeDynamicConfig(
|
|
4796
|
+
config,
|
|
4797
|
+
await config.onSchemaFetch(siteId)
|
|
4798
|
+
);
|
|
4799
|
+
for (const collection of requestConfig.collections) {
|
|
3188
4800
|
if (!collections.has(collection.slug)) {
|
|
3189
4801
|
collections.set(collection.slug, collection);
|
|
3190
4802
|
}
|
|
@@ -3195,11 +4807,18 @@ var AuditController = class {
|
|
|
3195
4807
|
async findForCollection(c, collection) {
|
|
3196
4808
|
const config = c.get("config");
|
|
3197
4809
|
if (!config.db) return c.json({ message: "Database not configured" }, 500);
|
|
3198
|
-
if (!collection.audit)
|
|
4810
|
+
if (!collection.audit)
|
|
4811
|
+
return c.json(
|
|
4812
|
+
{ message: "Audit is not enabled for this collection" },
|
|
4813
|
+
404
|
|
4814
|
+
);
|
|
3199
4815
|
const query = this.parseQuery(c);
|
|
3200
4816
|
const scope = await this.buildCollectionAuditScope(c, collection);
|
|
3201
4817
|
if (scope.denied) {
|
|
3202
|
-
return c.json(
|
|
4818
|
+
return c.json(
|
|
4819
|
+
{ error: true, message: `Access denied: read on ${collection.slug}` },
|
|
4820
|
+
403
|
|
4821
|
+
);
|
|
3203
4822
|
}
|
|
3204
4823
|
const where = query.where ? mergeWhereConstraint(scope.where, query.where) : scope.where;
|
|
3205
4824
|
const result = await config.db.find({
|
|
@@ -3215,7 +4834,9 @@ var AuditController = class {
|
|
|
3215
4834
|
const config = c.get("config");
|
|
3216
4835
|
if (!config.db) return c.json({ message: "Database not configured" }, 500);
|
|
3217
4836
|
const query = this.parseQuery(c);
|
|
3218
|
-
const collections = (await this.getVisibleCollections(c)).filter(
|
|
4837
|
+
const collections = (await this.getVisibleCollections(c)).filter(
|
|
4838
|
+
(collection) => collection.audit
|
|
4839
|
+
);
|
|
3219
4840
|
const scopes = [];
|
|
3220
4841
|
for (const collection of collections) {
|
|
3221
4842
|
const scope = await this.buildCollectionAuditScope(c, collection);
|
|
@@ -3246,8 +4867,7 @@ function getBearerToken(c) {
|
|
|
3246
4867
|
const authHeader = c.req.header("Authorization");
|
|
3247
4868
|
return authHeader?.replace(/^Bearer\s+/i, "") || void 0;
|
|
3248
4869
|
}
|
|
3249
|
-
async function resolveUser(
|
|
3250
|
-
const payload = await verifyCollectionToken(token);
|
|
4870
|
+
async function resolveUser(payload, config) {
|
|
3251
4871
|
if (payload.purpose) {
|
|
3252
4872
|
return payload;
|
|
3253
4873
|
}
|
|
@@ -3262,7 +4882,12 @@ async function resolveUser(token, config) {
|
|
|
3262
4882
|
id: payload.sub
|
|
3263
4883
|
});
|
|
3264
4884
|
} catch (err) {
|
|
3265
|
-
|
|
4885
|
+
getConfigLogger(config, "auth").error({
|
|
4886
|
+
err,
|
|
4887
|
+
msg: "Failed to hydrate user from token",
|
|
4888
|
+
collection: payload.collection,
|
|
4889
|
+
userId: payload.sub
|
|
4890
|
+
});
|
|
3266
4891
|
return payload;
|
|
3267
4892
|
}
|
|
3268
4893
|
if (!doc) {
|
|
@@ -3276,6 +4901,18 @@ async function resolveUser(token, config) {
|
|
|
3276
4901
|
collection: payload.collection
|
|
3277
4902
|
};
|
|
3278
4903
|
}
|
|
4904
|
+
async function resolveAuthenticatedRequest(token, config, clientIp) {
|
|
4905
|
+
const payload = await verifyCollectionToken(token);
|
|
4906
|
+
if (payload.sid && config?.db) {
|
|
4907
|
+
const session = await getAuthSession(config, payload.sid);
|
|
4908
|
+
if (!isAuthSessionActive(session) || session.userId !== payload.sub || session.collection !== payload.collection) {
|
|
4909
|
+
throw new Error("Invalid or expired session.");
|
|
4910
|
+
}
|
|
4911
|
+
await touchAuthSession(config, payload.sid, { ip: clientIp });
|
|
4912
|
+
}
|
|
4913
|
+
const user = await resolveUser(payload, config);
|
|
4914
|
+
return { user, payload };
|
|
4915
|
+
}
|
|
3279
4916
|
function requireAuth(config) {
|
|
3280
4917
|
return async (c, next) => {
|
|
3281
4918
|
if (c.get("user")) {
|
|
@@ -3283,18 +4920,42 @@ function requireAuth(config) {
|
|
|
3283
4920
|
}
|
|
3284
4921
|
const token = getBearerToken(c);
|
|
3285
4922
|
if (!token) {
|
|
4923
|
+
c.get("observability")?.recordAuthFailure({
|
|
4924
|
+
reason: "missing_token",
|
|
4925
|
+
path: c.req.path
|
|
4926
|
+
});
|
|
3286
4927
|
return c.json({ error: true, message: "Authentication required." }, 401);
|
|
3287
4928
|
}
|
|
3288
4929
|
let user;
|
|
4930
|
+
let payload;
|
|
3289
4931
|
try {
|
|
3290
|
-
|
|
3291
|
-
|
|
4932
|
+
const resolved = await resolveAuthenticatedRequest(
|
|
4933
|
+
token,
|
|
4934
|
+
config ?? c.get("config"),
|
|
4935
|
+
c.get("clientIp")
|
|
4936
|
+
);
|
|
4937
|
+
user = resolved.user;
|
|
4938
|
+
payload = resolved.payload;
|
|
4939
|
+
} catch (err) {
|
|
4940
|
+
c.get("observability")?.recordAuthFailure({
|
|
4941
|
+
reason: "invalid_token",
|
|
4942
|
+
path: c.req.path
|
|
4943
|
+
});
|
|
4944
|
+
getRequestLogger(c, "auth").warn({
|
|
4945
|
+
err,
|
|
4946
|
+
msg: "Rejected invalid or expired token"
|
|
4947
|
+
});
|
|
3292
4948
|
return c.json({ error: true, message: "Invalid or expired token." }, 401);
|
|
3293
4949
|
}
|
|
3294
4950
|
if (!user) {
|
|
4951
|
+
c.get("observability")?.recordAuthFailure({
|
|
4952
|
+
reason: "missing_user",
|
|
4953
|
+
path: c.req.path
|
|
4954
|
+
});
|
|
3295
4955
|
return c.json({ error: true, message: "Invalid or expired token." }, 401);
|
|
3296
4956
|
}
|
|
3297
4957
|
c.set("user", user);
|
|
4958
|
+
c.set("authTokenPayload", payload);
|
|
3298
4959
|
await next();
|
|
3299
4960
|
};
|
|
3300
4961
|
}
|
|
@@ -3306,10 +4967,16 @@ function optionalAuth(config) {
|
|
|
3306
4967
|
const token = getBearerToken(c);
|
|
3307
4968
|
if (token) {
|
|
3308
4969
|
try {
|
|
3309
|
-
const
|
|
4970
|
+
const resolved = await resolveAuthenticatedRequest(
|
|
4971
|
+
token,
|
|
4972
|
+
config ?? c.get("config"),
|
|
4973
|
+
c.get("clientIp")
|
|
4974
|
+
);
|
|
4975
|
+
const user = resolved.user;
|
|
3310
4976
|
if (user) {
|
|
3311
4977
|
c.set("user", user);
|
|
3312
4978
|
}
|
|
4979
|
+
c.set("authTokenPayload", resolved.payload);
|
|
3313
4980
|
} catch {
|
|
3314
4981
|
}
|
|
3315
4982
|
}
|
|
@@ -3358,7 +5025,15 @@ function generateOpenApi(config) {
|
|
|
3358
5025
|
},
|
|
3359
5026
|
WorkflowHistoryEntry: {
|
|
3360
5027
|
type: "object",
|
|
3361
|
-
required: [
|
|
5028
|
+
required: [
|
|
5029
|
+
"collection",
|
|
5030
|
+
"documentId",
|
|
5031
|
+
"transition",
|
|
5032
|
+
"from",
|
|
5033
|
+
"to",
|
|
5034
|
+
"revision",
|
|
5035
|
+
"createdAt"
|
|
5036
|
+
],
|
|
3362
5037
|
properties: {
|
|
3363
5038
|
id: { type: "string" },
|
|
3364
5039
|
collection: { type: "string" },
|
|
@@ -3383,7 +5058,11 @@ function generateOpenApi(config) {
|
|
|
3383
5058
|
user: { type: "string", nullable: true },
|
|
3384
5059
|
timestamp: { type: "string", format: "date-time" },
|
|
3385
5060
|
changes: {
|
|
3386
|
-
oneOf: [
|
|
5061
|
+
oneOf: [
|
|
5062
|
+
{ type: "string" },
|
|
5063
|
+
{ type: "object", additionalProperties: true },
|
|
5064
|
+
{ type: "null" }
|
|
5065
|
+
]
|
|
3387
5066
|
}
|
|
3388
5067
|
}
|
|
3389
5068
|
},
|
|
@@ -3475,13 +5154,17 @@ function generateOpenApi(config) {
|
|
|
3475
5154
|
get: {
|
|
3476
5155
|
tags: ["Preferences"],
|
|
3477
5156
|
summary: "Get an authenticated user preference",
|
|
3478
|
-
parameters: [
|
|
5157
|
+
parameters: [
|
|
5158
|
+
{ name: "key", in: "path", required: true, schema: { type: "string" } }
|
|
5159
|
+
],
|
|
3479
5160
|
responses: { 200: { description: "Preference value" } }
|
|
3480
5161
|
},
|
|
3481
5162
|
put: {
|
|
3482
5163
|
tags: ["Preferences"],
|
|
3483
5164
|
summary: "Set an authenticated user preference",
|
|
3484
|
-
parameters: [
|
|
5165
|
+
parameters: [
|
|
5166
|
+
{ name: "key", in: "path", required: true, schema: { type: "string" } }
|
|
5167
|
+
],
|
|
3485
5168
|
requestBody: {
|
|
3486
5169
|
required: true,
|
|
3487
5170
|
content: { "application/json": { schema: { type: "object" } } }
|
|
@@ -3517,10 +5200,23 @@ function generateOpenApi(config) {
|
|
|
3517
5200
|
tags: ["Audit"],
|
|
3518
5201
|
summary: "Get audit entries across all readable audited collections",
|
|
3519
5202
|
parameters: [
|
|
3520
|
-
{
|
|
5203
|
+
{
|
|
5204
|
+
name: "limit",
|
|
5205
|
+
in: "query",
|
|
5206
|
+
schema: { type: "integer", default: 50, maximum: 100 }
|
|
5207
|
+
},
|
|
3521
5208
|
{ name: "page", in: "query", schema: { type: "integer", default: 1 } },
|
|
3522
|
-
{
|
|
3523
|
-
|
|
5209
|
+
{
|
|
5210
|
+
name: "where",
|
|
5211
|
+
in: "query",
|
|
5212
|
+
schema: { type: "string" },
|
|
5213
|
+
description: "JSON filter"
|
|
5214
|
+
},
|
|
5215
|
+
{
|
|
5216
|
+
name: "sort",
|
|
5217
|
+
in: "query",
|
|
5218
|
+
schema: { type: "string", default: "-timestamp" }
|
|
5219
|
+
}
|
|
3524
5220
|
],
|
|
3525
5221
|
responses: {
|
|
3526
5222
|
200: {
|
|
@@ -3725,10 +5421,27 @@ function generateOpenApi(config) {
|
|
|
3725
5421
|
tags: [collectionTag],
|
|
3726
5422
|
summary: `Get ${labels.singular} audit entries`,
|
|
3727
5423
|
parameters: [
|
|
3728
|
-
{
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
5424
|
+
{
|
|
5425
|
+
name: "limit",
|
|
5426
|
+
in: "query",
|
|
5427
|
+
schema: { type: "integer", default: 50, maximum: 100 }
|
|
5428
|
+
},
|
|
5429
|
+
{
|
|
5430
|
+
name: "page",
|
|
5431
|
+
in: "query",
|
|
5432
|
+
schema: { type: "integer", default: 1 }
|
|
5433
|
+
},
|
|
5434
|
+
{
|
|
5435
|
+
name: "where",
|
|
5436
|
+
in: "query",
|
|
5437
|
+
schema: { type: "string" },
|
|
5438
|
+
description: "JSON filter"
|
|
5439
|
+
},
|
|
5440
|
+
{
|
|
5441
|
+
name: "sort",
|
|
5442
|
+
in: "query",
|
|
5443
|
+
schema: { type: "string", default: "-timestamp" }
|
|
5444
|
+
}
|
|
3732
5445
|
],
|
|
3733
5446
|
responses: {
|
|
3734
5447
|
200: {
|
|
@@ -3854,6 +5567,15 @@ function generateOpenApi(config) {
|
|
|
3854
5567
|
post: {
|
|
3855
5568
|
tags: [collectionTag],
|
|
3856
5569
|
summary: `Log out of ${labels.plural}`,
|
|
5570
|
+
parameters: [
|
|
5571
|
+
{
|
|
5572
|
+
name: "allSessions",
|
|
5573
|
+
in: "query",
|
|
5574
|
+
required: false,
|
|
5575
|
+
schema: { type: "boolean" },
|
|
5576
|
+
description: "Revoke every active session for this account instead of only the current one."
|
|
5577
|
+
}
|
|
5578
|
+
],
|
|
3857
5579
|
responses: { 200: { description: "Logged out" } }
|
|
3858
5580
|
}
|
|
3859
5581
|
};
|
|
@@ -3879,7 +5601,11 @@ function generateOpenApi(config) {
|
|
|
3879
5601
|
post: {
|
|
3880
5602
|
tags: [collectionTag],
|
|
3881
5603
|
summary: "Refresh an authentication token",
|
|
3882
|
-
responses: {
|
|
5604
|
+
responses: {
|
|
5605
|
+
200: {
|
|
5606
|
+
description: "Refreshed token for the current active session"
|
|
5607
|
+
}
|
|
5608
|
+
}
|
|
3883
5609
|
}
|
|
3884
5610
|
};
|
|
3885
5611
|
spec.paths[`${path}/forgot-password`] = {
|
|
@@ -3910,7 +5636,11 @@ function generateOpenApi(config) {
|
|
|
3910
5636
|
schema: { type: "string" }
|
|
3911
5637
|
}
|
|
3912
5638
|
],
|
|
3913
|
-
responses: {
|
|
5639
|
+
responses: {
|
|
5640
|
+
200: {
|
|
5641
|
+
description: "Password changed and active sessions revoked"
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
3914
5644
|
}
|
|
3915
5645
|
};
|
|
3916
5646
|
}
|
|
@@ -4119,7 +5849,7 @@ function fieldsToProperties(fields) {
|
|
|
4119
5849
|
return { properties: props, required };
|
|
4120
5850
|
}
|
|
4121
5851
|
function fieldToSchema(field) {
|
|
4122
|
-
let schema
|
|
5852
|
+
let schema;
|
|
4123
5853
|
switch (field.type) {
|
|
4124
5854
|
case "text":
|
|
4125
5855
|
case "textarea":
|
|
@@ -4128,7 +5858,10 @@ function fieldToSchema(field) {
|
|
|
4128
5858
|
break;
|
|
4129
5859
|
case "url":
|
|
4130
5860
|
schema = {
|
|
4131
|
-
oneOf: [
|
|
5861
|
+
oneOf: [
|
|
5862
|
+
{ type: "string" },
|
|
5863
|
+
{ type: "object", additionalProperties: true }
|
|
5864
|
+
]
|
|
4132
5865
|
};
|
|
4133
5866
|
break;
|
|
4134
5867
|
case "icon":
|
|
@@ -4290,7 +6023,10 @@ function accessGate(config, target, action) {
|
|
|
4290
6023
|
req: toHookRequestContext(c.req)
|
|
4291
6024
|
});
|
|
4292
6025
|
if (!allowed) {
|
|
4293
|
-
return c.json(
|
|
6026
|
+
return c.json(
|
|
6027
|
+
{ error: true, message: `Access denied: ${action} on ${target.slug}` },
|
|
6028
|
+
403
|
|
6029
|
+
);
|
|
4294
6030
|
}
|
|
4295
6031
|
await next();
|
|
4296
6032
|
};
|
|
@@ -4314,228 +6050,276 @@ function serializeFieldForApi(f) {
|
|
|
4314
6050
|
if (serialized.fields) {
|
|
4315
6051
|
serialized.fields = serialized.fields.map(serializeFieldForApi);
|
|
4316
6052
|
}
|
|
4317
|
-
if (serialized.blocks) {
|
|
6053
|
+
if (serialized.blocks && !serialized.blockReferences?.length) {
|
|
4318
6054
|
serialized.blocks = serialized.blocks.map((b) => ({
|
|
4319
6055
|
...b,
|
|
4320
6056
|
fields: b.fields?.map(serializeFieldForApi)
|
|
4321
6057
|
}));
|
|
6058
|
+
} else if (serialized.blockReferences?.length) {
|
|
6059
|
+
delete serialized.blocks;
|
|
4322
6060
|
}
|
|
4323
6061
|
return serialized;
|
|
4324
6062
|
}
|
|
6063
|
+
function serializeBlockForApi(block) {
|
|
6064
|
+
return {
|
|
6065
|
+
...block,
|
|
6066
|
+
fields: block.fields?.map(serializeFieldForApi)
|
|
6067
|
+
};
|
|
6068
|
+
}
|
|
4325
6069
|
function registerRoutes(app, config) {
|
|
4326
6070
|
const optionsCache = /* @__PURE__ */ new Map();
|
|
4327
6071
|
app.get("/api/schemas", optionalAuth(config), async (c) => {
|
|
4328
6072
|
const siteId = c.req.header("X-Site-Id");
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
const dynamic = await config.onSchemaFetch(siteId);
|
|
4333
|
-
if (dynamic.collections) collections = [...collections, ...dynamic.collections];
|
|
4334
|
-
if (dynamic.globals) globals = [...globals, ...dynamic.globals];
|
|
4335
|
-
if (dynamic.admin) {
|
|
4336
|
-
config.admin = { ...config.admin, ...dynamic.admin };
|
|
4337
|
-
}
|
|
4338
|
-
if (dynamic.adminAuth) {
|
|
4339
|
-
config.adminAuth = { ...config.adminAuth, ...dynamic.adminAuth };
|
|
4340
|
-
}
|
|
4341
|
-
}
|
|
6073
|
+
const requestConfig = siteId && config.onSchemaFetch ? mergeDynamicConfig(config, await config.onSchemaFetch(siteId)) : config;
|
|
6074
|
+
const collections = [...requestConfig.collections];
|
|
6075
|
+
const globals = [...requestConfig.globals];
|
|
4342
6076
|
const user = c.get("user");
|
|
4343
6077
|
const accessArgs = { user, req: toHookRequestContext(c.req) };
|
|
4344
6078
|
const serializeAccess = async (access) => {
|
|
4345
6079
|
if (typeof access === "string") return access;
|
|
4346
6080
|
if (typeof access === "boolean") return access;
|
|
4347
6081
|
if (access && typeof access === "object" && typeof access.policy === "string") {
|
|
4348
|
-
const policy =
|
|
4349
|
-
if (typeof policy === "string" || typeof policy === "boolean")
|
|
6082
|
+
const policy = requestConfig.accessPolicies?.[access.policy];
|
|
6083
|
+
if (typeof policy === "string" || typeof policy === "boolean")
|
|
6084
|
+
return policy;
|
|
4350
6085
|
}
|
|
4351
|
-
return resolveBooleanAccess(
|
|
6086
|
+
return resolveBooleanAccess(requestConfig, access, accessArgs);
|
|
4352
6087
|
};
|
|
4353
|
-
const filteredCollections = await Promise.all(
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
update: await serializeAccess(col.access?.update),
|
|
4360
|
-
delete: await serializeAccess(col.access?.delete)
|
|
4361
|
-
},
|
|
4362
|
-
fields: await Promise.all(col.fields.map(serializeFieldForApi).map(async (f) => ({
|
|
4363
|
-
name: f.name,
|
|
4364
|
-
type: f.type,
|
|
4365
|
-
label: f.label,
|
|
4366
|
-
required: f.required,
|
|
4367
|
-
defaultValue: f.defaultValue,
|
|
4368
|
-
options: f.options,
|
|
4369
|
-
relationTo: f.relationTo,
|
|
4370
|
-
hasMany: f.hasMany,
|
|
4371
|
-
fields: f.fields,
|
|
4372
|
-
blocks: f.blocks,
|
|
4373
|
-
collection: f.collection,
|
|
4374
|
-
on: f.on,
|
|
4375
|
-
limit: f.limit,
|
|
4376
|
-
admin: f.admin,
|
|
6088
|
+
const filteredCollections = await Promise.all(
|
|
6089
|
+
collections.filter(
|
|
6090
|
+
(col) => !siteId || col.shared || !col.siteId || col.siteId === siteId
|
|
6091
|
+
).map(async (col) => ({
|
|
6092
|
+
slug: col.slug,
|
|
6093
|
+
labels: col.labels,
|
|
4377
6094
|
access: {
|
|
4378
|
-
read: await serializeAccess(
|
|
4379
|
-
create: await serializeAccess(
|
|
4380
|
-
update: await serializeAccess(
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
6095
|
+
read: await serializeAccess(col.access?.read),
|
|
6096
|
+
create: await serializeAccess(col.access?.create),
|
|
6097
|
+
update: await serializeAccess(col.access?.update),
|
|
6098
|
+
delete: await serializeAccess(col.access?.delete)
|
|
6099
|
+
},
|
|
6100
|
+
fields: await Promise.all(
|
|
6101
|
+
col.fields.map(serializeFieldForApi).map(async (f) => ({
|
|
6102
|
+
name: f.name,
|
|
6103
|
+
type: f.type,
|
|
6104
|
+
label: f.label,
|
|
6105
|
+
required: f.required,
|
|
6106
|
+
defaultValue: f.defaultValue,
|
|
6107
|
+
options: f.options,
|
|
6108
|
+
relationTo: f.relationTo,
|
|
6109
|
+
hasMany: f.hasMany,
|
|
6110
|
+
fields: f.fields,
|
|
6111
|
+
blocks: f.blocks,
|
|
6112
|
+
blockReferences: f.blockReferences,
|
|
6113
|
+
collection: f.collection,
|
|
6114
|
+
on: f.on,
|
|
6115
|
+
limit: f.limit,
|
|
6116
|
+
admin: f.admin,
|
|
6117
|
+
access: {
|
|
6118
|
+
read: await serializeAccess(f.access?.read),
|
|
6119
|
+
create: await serializeAccess(f.access?.create),
|
|
6120
|
+
update: await serializeAccess(f.access?.update)
|
|
6121
|
+
}
|
|
6122
|
+
}))
|
|
6123
|
+
),
|
|
6124
|
+
upload: !!col.upload,
|
|
6125
|
+
auth: !!col.auth,
|
|
6126
|
+
audit: !!col.audit,
|
|
6127
|
+
drafts: !!col.drafts,
|
|
6128
|
+
admin: col.admin,
|
|
6129
|
+
workflow: col.workflow ? {
|
|
6130
|
+
initialState: col.workflow.initialState,
|
|
6131
|
+
draftState: col.workflow.draftState,
|
|
6132
|
+
states: col.workflow.states,
|
|
6133
|
+
transitions: col.workflow.transitions.map((t) => ({
|
|
6134
|
+
name: t.name,
|
|
6135
|
+
label: t.label,
|
|
6136
|
+
from: t.from,
|
|
6137
|
+
to: t.to,
|
|
6138
|
+
requiredCapabilities: t.requiredCapabilities,
|
|
6139
|
+
requireComment: t.requireComment,
|
|
6140
|
+
unpublish: t.unpublish
|
|
6141
|
+
})),
|
|
6142
|
+
roles: col.workflow.roles
|
|
6143
|
+
} : void 0
|
|
6144
|
+
}))
|
|
6145
|
+
);
|
|
6146
|
+
const filteredGlobals = await Promise.all(
|
|
6147
|
+
globals.filter(
|
|
6148
|
+
(glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId
|
|
6149
|
+
).map(async (glb) => ({
|
|
6150
|
+
slug: glb.slug,
|
|
6151
|
+
label: glb.label,
|
|
4426
6152
|
access: {
|
|
4427
|
-
read: await serializeAccess(
|
|
4428
|
-
update: await serializeAccess(
|
|
4429
|
-
}
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
6153
|
+
read: await serializeAccess(glb.access?.read),
|
|
6154
|
+
update: await serializeAccess(glb.access?.update)
|
|
6155
|
+
},
|
|
6156
|
+
fields: await Promise.all(
|
|
6157
|
+
glb.fields.map(serializeFieldForApi).map(async (f) => ({
|
|
6158
|
+
name: f.name,
|
|
6159
|
+
type: f.type,
|
|
6160
|
+
label: f.label,
|
|
6161
|
+
required: f.required,
|
|
6162
|
+
defaultValue: f.defaultValue,
|
|
6163
|
+
options: f.options,
|
|
6164
|
+
relationTo: f.relationTo,
|
|
6165
|
+
hasMany: f.hasMany,
|
|
6166
|
+
fields: f.fields,
|
|
6167
|
+
blocks: f.blocks,
|
|
6168
|
+
blockReferences: f.blockReferences,
|
|
6169
|
+
collection: f.collection,
|
|
6170
|
+
on: f.on,
|
|
6171
|
+
limit: f.limit,
|
|
6172
|
+
admin: f.admin,
|
|
6173
|
+
access: {
|
|
6174
|
+
read: await serializeAccess(f.access?.read),
|
|
6175
|
+
update: await serializeAccess(f.access?.update)
|
|
6176
|
+
}
|
|
6177
|
+
}))
|
|
6178
|
+
),
|
|
6179
|
+
admin: glb.admin
|
|
6180
|
+
}))
|
|
6181
|
+
);
|
|
4433
6182
|
return c.json({
|
|
6183
|
+
blocks: requestConfig.blocks?.map(serializeBlockForApi),
|
|
4434
6184
|
collections: filteredCollections,
|
|
4435
6185
|
globals: filteredGlobals,
|
|
4436
|
-
admin:
|
|
4437
|
-
adminAuth: getPublicAdminAuthConfig(
|
|
6186
|
+
admin: requestConfig.admin || {},
|
|
6187
|
+
adminAuth: getPublicAdminAuthConfig(requestConfig.adminAuth, collections)
|
|
4438
6188
|
});
|
|
4439
6189
|
});
|
|
4440
|
-
app.get(
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
const
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
6190
|
+
app.get(
|
|
6191
|
+
"/api/dyrected/options/:collection/:field",
|
|
6192
|
+
optionalAuth(config),
|
|
6193
|
+
async (c) => {
|
|
6194
|
+
const { collection: colSlug, field: fieldName } = c.req.param();
|
|
6195
|
+
const siteId = c.req.header("X-Site-Id");
|
|
6196
|
+
const requestConfig = siteId && config.onSchemaFetch ? mergeDynamicConfig(config, await config.onSchemaFetch(siteId)) : config;
|
|
6197
|
+
let collections = [...requestConfig.collections];
|
|
6198
|
+
const user = c.get("user");
|
|
6199
|
+
let collection = collections.find((col) => col.slug === colSlug);
|
|
6200
|
+
let field;
|
|
6201
|
+
if (collection) {
|
|
6202
|
+
const accessExpr = collection.access?.read;
|
|
6203
|
+
if (accessExpr !== void 0 && accessExpr !== null) {
|
|
6204
|
+
const allowed = await resolveBooleanAccess(config, accessExpr, {
|
|
6205
|
+
user,
|
|
6206
|
+
req: toHookRequestContext(c.req)
|
|
6207
|
+
});
|
|
6208
|
+
if (!allowed) {
|
|
6209
|
+
return c.json(
|
|
6210
|
+
{ error: true, message: `Access denied: read on ${colSlug}` },
|
|
6211
|
+
403
|
|
6212
|
+
);
|
|
6213
|
+
}
|
|
6214
|
+
}
|
|
6215
|
+
field = collection.fields.find((f) => f.name === fieldName);
|
|
6216
|
+
} else {
|
|
6217
|
+
const globals = [...requestConfig.globals];
|
|
6218
|
+
const glb = globals.find((g) => g.slug === colSlug);
|
|
6219
|
+
if (!glb) {
|
|
6220
|
+
return c.json(
|
|
6221
|
+
{
|
|
6222
|
+
error: true,
|
|
6223
|
+
message: `${colSlug} not found as collection or global`
|
|
6224
|
+
},
|
|
6225
|
+
404
|
|
6226
|
+
);
|
|
6227
|
+
}
|
|
6228
|
+
const accessExpr = glb.access?.read;
|
|
6229
|
+
if (accessExpr !== void 0 && accessExpr !== null) {
|
|
6230
|
+
const allowed = await resolveBooleanAccess(config, accessExpr, {
|
|
6231
|
+
user,
|
|
6232
|
+
req: toHookRequestContext(c.req)
|
|
6233
|
+
});
|
|
6234
|
+
if (!allowed) {
|
|
6235
|
+
return c.json(
|
|
6236
|
+
{
|
|
6237
|
+
error: true,
|
|
6238
|
+
message: `Access denied: read on global ${colSlug}`
|
|
6239
|
+
},
|
|
6240
|
+
403
|
|
6241
|
+
);
|
|
6242
|
+
}
|
|
4460
6243
|
}
|
|
6244
|
+
field = glb.fields.find((f) => f.name === fieldName);
|
|
4461
6245
|
}
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
6246
|
+
if (!field) {
|
|
6247
|
+
return c.json(
|
|
6248
|
+
{
|
|
6249
|
+
error: true,
|
|
6250
|
+
message: `Field ${fieldName} not found in ${colSlug}`
|
|
6251
|
+
},
|
|
6252
|
+
404
|
|
6253
|
+
);
|
|
6254
|
+
}
|
|
6255
|
+
let resolver;
|
|
6256
|
+
let cacheTTL;
|
|
6257
|
+
if (typeof field.options === "function") {
|
|
6258
|
+
resolver = field.options;
|
|
6259
|
+
} else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
|
|
6260
|
+
resolver = field.options.resolve;
|
|
6261
|
+
cacheTTL = field.options.cacheTTL;
|
|
4468
6262
|
}
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
6263
|
+
if (!resolver) {
|
|
6264
|
+
return c.json(
|
|
6265
|
+
{
|
|
6266
|
+
error: true,
|
|
6267
|
+
message: `Field ${fieldName} in ${colSlug} is not dynamic`
|
|
6268
|
+
},
|
|
6269
|
+
400
|
|
6270
|
+
);
|
|
4472
6271
|
}
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
const
|
|
6272
|
+
try {
|
|
6273
|
+
const db = c.get("db") || config.db;
|
|
6274
|
+
const queryParams = c.req.query();
|
|
6275
|
+
const reqContext = {
|
|
6276
|
+
query: queryParams,
|
|
6277
|
+
headers: c.req.header(),
|
|
6278
|
+
raw: c.req.raw
|
|
6279
|
+
};
|
|
6280
|
+
const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
|
|
6281
|
+
let cacheKey = "";
|
|
6282
|
+
if (shouldCache) {
|
|
6283
|
+
cacheKey = JSON.stringify([
|
|
6284
|
+
siteId ?? "",
|
|
6285
|
+
colSlug,
|
|
6286
|
+
fieldName,
|
|
6287
|
+
user?.id ?? null,
|
|
6288
|
+
queryParams
|
|
6289
|
+
]);
|
|
6290
|
+
const hit = optionsCache.get(cacheKey);
|
|
6291
|
+
if (hit && hit.expires > Date.now()) {
|
|
6292
|
+
return c.json(hit.value);
|
|
6293
|
+
}
|
|
6294
|
+
}
|
|
6295
|
+
const result = await resolver({
|
|
6296
|
+
db,
|
|
4476
6297
|
user,
|
|
4477
|
-
req:
|
|
6298
|
+
req: reqContext
|
|
4478
6299
|
});
|
|
4479
|
-
if (
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
}
|
|
4485
|
-
if (!field) {
|
|
4486
|
-
return c.json({ error: true, message: `Field ${fieldName} not found in ${colSlug}` }, 404);
|
|
4487
|
-
}
|
|
4488
|
-
let resolver;
|
|
4489
|
-
let cacheTTL;
|
|
4490
|
-
if (typeof field.options === "function") {
|
|
4491
|
-
resolver = field.options;
|
|
4492
|
-
} else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
|
|
4493
|
-
resolver = field.options.resolve;
|
|
4494
|
-
cacheTTL = field.options.cacheTTL;
|
|
4495
|
-
}
|
|
4496
|
-
if (!resolver) {
|
|
4497
|
-
return c.json({ error: true, message: `Field ${fieldName} in ${colSlug} is not dynamic` }, 400);
|
|
4498
|
-
}
|
|
4499
|
-
try {
|
|
4500
|
-
const db = c.get("db") || config.db;
|
|
4501
|
-
const queryParams = c.req.query();
|
|
4502
|
-
const reqContext = {
|
|
4503
|
-
query: queryParams,
|
|
4504
|
-
headers: c.req.header(),
|
|
4505
|
-
raw: c.req.raw
|
|
4506
|
-
};
|
|
4507
|
-
const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
|
|
4508
|
-
let cacheKey = "";
|
|
4509
|
-
if (shouldCache) {
|
|
4510
|
-
cacheKey = JSON.stringify([
|
|
4511
|
-
siteId ?? "",
|
|
4512
|
-
colSlug,
|
|
4513
|
-
fieldName,
|
|
4514
|
-
user?.id ?? null,
|
|
4515
|
-
queryParams
|
|
4516
|
-
]);
|
|
4517
|
-
const hit = optionsCache.get(cacheKey);
|
|
4518
|
-
if (hit && hit.expires > Date.now()) {
|
|
4519
|
-
return c.json(hit.value);
|
|
6300
|
+
if (shouldCache) {
|
|
6301
|
+
optionsCache.set(cacheKey, {
|
|
6302
|
+
expires: Date.now() + cacheTTL * 1e3,
|
|
6303
|
+
value: result
|
|
6304
|
+
});
|
|
4520
6305
|
}
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
if (shouldCache) {
|
|
4528
|
-
optionsCache.set(cacheKey, {
|
|
4529
|
-
expires: Date.now() + cacheTTL * 1e3,
|
|
4530
|
-
value: result
|
|
6306
|
+
return c.json(result);
|
|
6307
|
+
} catch (err) {
|
|
6308
|
+
getRequestLogger(c, "router").error({
|
|
6309
|
+
err,
|
|
6310
|
+
msg: "Failed to resolve dynamic field options",
|
|
6311
|
+
fieldName
|
|
4531
6312
|
});
|
|
6313
|
+
return c.json(
|
|
6314
|
+
{
|
|
6315
|
+
error: true,
|
|
6316
|
+
message: err.message || "Failed to resolve dynamic options"
|
|
6317
|
+
},
|
|
6318
|
+
500
|
|
6319
|
+
);
|
|
4532
6320
|
}
|
|
4533
|
-
return c.json(result);
|
|
4534
|
-
} catch (err) {
|
|
4535
|
-
console.error(`[dyrected/core] Failed to resolve dynamic options for field ${fieldName}:`, err);
|
|
4536
|
-
return c.json({ error: true, message: err.message || "Failed to resolve dynamic options" }, 500);
|
|
4537
6321
|
}
|
|
4538
|
-
|
|
6322
|
+
);
|
|
4539
6323
|
app.get("/api/openapi.json", (c) => {
|
|
4540
6324
|
return c.json(generateOpenApi(config));
|
|
4541
6325
|
});
|
|
@@ -4548,10 +6332,18 @@ function registerRoutes(app, config) {
|
|
|
4548
6332
|
const key = c.req.param("key");
|
|
4549
6333
|
const scope = c.req.query("scope");
|
|
4550
6334
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
4551
|
-
if (!user?.collection || !user.sub)
|
|
4552
|
-
|
|
6335
|
+
if (!user?.collection || !user.sub)
|
|
6336
|
+
return c.json({ error: true, message: "Authentication required." }, 401);
|
|
6337
|
+
if (!key)
|
|
6338
|
+
return c.json(
|
|
6339
|
+
{ error: true, message: "Preference key is required." },
|
|
6340
|
+
400
|
|
6341
|
+
);
|
|
4553
6342
|
const getGlobalPreference = async () => {
|
|
4554
|
-
const globalDoc = await db.findOne({
|
|
6343
|
+
const globalDoc = await db.findOne({
|
|
6344
|
+
collection: "__global_preferences",
|
|
6345
|
+
id: key
|
|
6346
|
+
});
|
|
4555
6347
|
return globalDoc ? globalDoc.value : null;
|
|
4556
6348
|
};
|
|
4557
6349
|
if (scope === "global") {
|
|
@@ -4573,15 +6365,29 @@ function registerRoutes(app, config) {
|
|
|
4573
6365
|
const key = c.req.param("key");
|
|
4574
6366
|
const scope = c.req.query("scope");
|
|
4575
6367
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
4576
|
-
if (!user?.collection || !user.sub)
|
|
4577
|
-
|
|
6368
|
+
if (!user?.collection || !user.sub)
|
|
6369
|
+
return c.json({ error: true, message: "Authentication required." }, 401);
|
|
6370
|
+
if (!key)
|
|
6371
|
+
return c.json(
|
|
6372
|
+
{ error: true, message: "Preference key is required." },
|
|
6373
|
+
400
|
|
6374
|
+
);
|
|
4578
6375
|
const body = await c.req.json().catch(() => ({}));
|
|
4579
6376
|
if (scope === "global") {
|
|
4580
6377
|
const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
|
|
4581
6378
|
if (!isAdminUser) {
|
|
4582
|
-
return c.json(
|
|
6379
|
+
return c.json(
|
|
6380
|
+
{
|
|
6381
|
+
error: true,
|
|
6382
|
+
message: "Only administrators can save global preferences."
|
|
6383
|
+
},
|
|
6384
|
+
403
|
|
6385
|
+
);
|
|
4583
6386
|
}
|
|
4584
|
-
const existing = await db.findOne({
|
|
6387
|
+
const existing = await db.findOne({
|
|
6388
|
+
collection: "__global_preferences",
|
|
6389
|
+
id: key
|
|
6390
|
+
});
|
|
4585
6391
|
if (existing) {
|
|
4586
6392
|
await db.update({
|
|
4587
6393
|
collection: "__global_preferences",
|
|
@@ -4613,12 +6419,23 @@ function registerRoutes(app, config) {
|
|
|
4613
6419
|
const key = c.req.param("key");
|
|
4614
6420
|
const scope = c.req.query("scope");
|
|
4615
6421
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
4616
|
-
if (!user?.collection || !user.sub)
|
|
4617
|
-
|
|
6422
|
+
if (!user?.collection || !user.sub)
|
|
6423
|
+
return c.json({ error: true, message: "Authentication required." }, 401);
|
|
6424
|
+
if (!key)
|
|
6425
|
+
return c.json(
|
|
6426
|
+
{ error: true, message: "Preference key is required." },
|
|
6427
|
+
400
|
|
6428
|
+
);
|
|
4618
6429
|
if (scope === "global") {
|
|
4619
6430
|
const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
|
|
4620
6431
|
if (!isAdminUser) {
|
|
4621
|
-
return c.json(
|
|
6432
|
+
return c.json(
|
|
6433
|
+
{
|
|
6434
|
+
error: true,
|
|
6435
|
+
message: "Only administrators can delete global preferences."
|
|
6436
|
+
},
|
|
6437
|
+
403
|
|
6438
|
+
);
|
|
4622
6439
|
}
|
|
4623
6440
|
await db.delete({ collection: "__global_preferences", id: key });
|
|
4624
6441
|
return c.json({ success: true });
|
|
@@ -4647,17 +6464,41 @@ function registerRoutes(app, config) {
|
|
|
4647
6464
|
for (const col of uploadCollections) {
|
|
4648
6465
|
const mediaController = new MediaController(col.slug);
|
|
4649
6466
|
const prefix = `/api/collections/${col.slug}`;
|
|
4650
|
-
app.get(
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
6467
|
+
app.get(
|
|
6468
|
+
`${prefix}/media`,
|
|
6469
|
+
accessGate(config, col, "read"),
|
|
6470
|
+
(c) => mediaController.find(c)
|
|
6471
|
+
);
|
|
6472
|
+
app.get(
|
|
6473
|
+
`${prefix}/media/:filename{.+$}`,
|
|
6474
|
+
(c) => mediaController.serve(c)
|
|
6475
|
+
);
|
|
6476
|
+
app.post(
|
|
6477
|
+
`${prefix}/media`,
|
|
6478
|
+
accessGate(config, col, "create"),
|
|
6479
|
+
(c) => mediaController.upload(c)
|
|
6480
|
+
);
|
|
6481
|
+
app.delete(
|
|
6482
|
+
`${prefix}/media/:id`,
|
|
6483
|
+
accessGate(config, col, "delete"),
|
|
6484
|
+
(c) => mediaController.delete(c)
|
|
6485
|
+
);
|
|
4654
6486
|
}
|
|
4655
6487
|
}
|
|
4656
6488
|
const adminAuthController = new AdminAuthController(config);
|
|
4657
6489
|
app.get("/api/admin/auth/providers", (c) => adminAuthController.providers(c));
|
|
4658
|
-
app.get(
|
|
4659
|
-
|
|
4660
|
-
|
|
6490
|
+
app.get(
|
|
6491
|
+
"/api/admin/auth/:provider/start",
|
|
6492
|
+
(c) => adminAuthController.start(c)
|
|
6493
|
+
);
|
|
6494
|
+
app.get(
|
|
6495
|
+
"/api/admin/auth/:provider/callback",
|
|
6496
|
+
(c) => adminAuthController.callback(c)
|
|
6497
|
+
);
|
|
6498
|
+
app.post(
|
|
6499
|
+
"/api/admin/auth/:provider/exchange",
|
|
6500
|
+
(c) => adminAuthController.exchange(c)
|
|
6501
|
+
);
|
|
4661
6502
|
app.post("/api/admin/logout", (c) => adminAuthController.logout(c));
|
|
4662
6503
|
for (const collection of config.collections) {
|
|
4663
6504
|
if (!collection.auth) continue;
|
|
@@ -4668,10 +6509,21 @@ function registerRoutes(app, config) {
|
|
|
4668
6509
|
app.get(`${path}/init`, (c) => authController.init(c));
|
|
4669
6510
|
app.post(`${path}/first-user`, (c) => authController.registerFirstUser(c));
|
|
4670
6511
|
app.get(`${path}/me`, requireAuth(config), (c) => authController.me(c));
|
|
4671
|
-
app.post(
|
|
4672
|
-
|
|
6512
|
+
app.post(
|
|
6513
|
+
`${path}/refresh-token`,
|
|
6514
|
+
requireAuth(config),
|
|
6515
|
+
(c) => authController.refreshToken(c)
|
|
6516
|
+
);
|
|
6517
|
+
app.post(
|
|
6518
|
+
`${path}/forgot-password`,
|
|
6519
|
+
(c) => authController.forgotPassword(c)
|
|
6520
|
+
);
|
|
4673
6521
|
app.post(`${path}/reset-password`, (c) => authController.resetPassword(c));
|
|
4674
|
-
app.post(
|
|
6522
|
+
app.post(
|
|
6523
|
+
`${path}/invite`,
|
|
6524
|
+
requireAuth(config),
|
|
6525
|
+
(c) => authController.invite(c)
|
|
6526
|
+
);
|
|
4675
6527
|
app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
|
|
4676
6528
|
}
|
|
4677
6529
|
const auditController = new AuditController();
|
|
@@ -4683,18 +6535,33 @@ function registerRoutes(app, config) {
|
|
|
4683
6535
|
app.post(`${path}/media`, (c) => controller.create(c));
|
|
4684
6536
|
app.delete(`${path}/delete-many`, (c) => controller.deleteMany(c));
|
|
4685
6537
|
if (collection.audit) {
|
|
4686
|
-
app.get(
|
|
6538
|
+
app.get(
|
|
6539
|
+
`${path}/__audit`,
|
|
6540
|
+
(c) => auditController.findForCollection(c, collection)
|
|
6541
|
+
);
|
|
4687
6542
|
}
|
|
4688
6543
|
app.get(`${path}/:id`, (c) => controller.findOne(c));
|
|
4689
6544
|
app.patch(`${path}/:id`, (c) => controller.update(c));
|
|
4690
6545
|
app.delete(`${path}/:id`, (c) => controller.delete(c));
|
|
4691
6546
|
app.post(`${path}/seed`, (c) => controller.seed(c));
|
|
4692
6547
|
if (collection.auth) {
|
|
4693
|
-
app.post(
|
|
6548
|
+
app.post(
|
|
6549
|
+
`${path}/:id/change-password`,
|
|
6550
|
+
requireAuth(config),
|
|
6551
|
+
(c) => controller.changePassword(c)
|
|
6552
|
+
);
|
|
4694
6553
|
}
|
|
4695
6554
|
if (collection.workflow) {
|
|
4696
|
-
app.post(
|
|
4697
|
-
|
|
6555
|
+
app.post(
|
|
6556
|
+
`${path}/:id/transitions/:transition`,
|
|
6557
|
+
requireAuth(config),
|
|
6558
|
+
(c) => controller.transition(c)
|
|
6559
|
+
);
|
|
6560
|
+
app.get(
|
|
6561
|
+
`${path}/:id/workflow-history`,
|
|
6562
|
+
requireAuth(config),
|
|
6563
|
+
(c) => controller.workflowHistory(c)
|
|
6564
|
+
);
|
|
4698
6565
|
}
|
|
4699
6566
|
}
|
|
4700
6567
|
for (const global of config.globals) {
|
|
@@ -4705,12 +6572,16 @@ function registerRoutes(app, config) {
|
|
|
4705
6572
|
app.post(`${path}/seed`, (c) => controller.seed(c));
|
|
4706
6573
|
}
|
|
4707
6574
|
if (!process.env.DYRECTED_JWT_SECRET) {
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
);
|
|
6575
|
+
getConfigLogger(config, "router").warn({
|
|
6576
|
+
msg: "DYRECTED_JWT_SECRET is not set; token-mode live preview is signing with an insecure default"
|
|
6577
|
+
});
|
|
4711
6578
|
}
|
|
4712
6579
|
const previewController = new PreviewController();
|
|
4713
|
-
app.post(
|
|
6580
|
+
app.post(
|
|
6581
|
+
"/api/preview-token",
|
|
6582
|
+
requireAuth(config),
|
|
6583
|
+
(c) => previewController.createToken(c)
|
|
6584
|
+
);
|
|
4714
6585
|
app.get("/api/preview-data", (c) => previewController.getData(c));
|
|
4715
6586
|
app.get("/api/audit", (c) => auditController.findAll(c));
|
|
4716
6587
|
app.get("/api/collections/:slug/__audit", async (c) => {
|
|
@@ -4723,49 +6594,81 @@ function registerRoutes(app, config) {
|
|
|
4723
6594
|
if (!config2.onSchemaFetch || !siteId) {
|
|
4724
6595
|
return c.json({ message: `Collection "${slug}" not found` }, 404);
|
|
4725
6596
|
}
|
|
4726
|
-
const
|
|
4727
|
-
|
|
6597
|
+
const requestConfig = mergeDynamicConfig(
|
|
6598
|
+
config2,
|
|
6599
|
+
await config2.onSchemaFetch(siteId)
|
|
6600
|
+
);
|
|
6601
|
+
const collection = requestConfig.collections.find(
|
|
6602
|
+
(col) => col.slug === slug
|
|
6603
|
+
);
|
|
4728
6604
|
if (!collection?.audit) {
|
|
4729
|
-
return c.json(
|
|
6605
|
+
return c.json(
|
|
6606
|
+
{ message: `Collection "${slug}" not found or has no audit log` },
|
|
6607
|
+
404
|
|
6608
|
+
);
|
|
4730
6609
|
}
|
|
4731
6610
|
return auditController.findForCollection(c, collection);
|
|
4732
6611
|
});
|
|
4733
|
-
app.post(
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
6612
|
+
app.post(
|
|
6613
|
+
"/api/collections/:slug/:id/transitions/:transition",
|
|
6614
|
+
requireAuth(config),
|
|
6615
|
+
async (c) => {
|
|
6616
|
+
const slug = c.req.param("slug");
|
|
6617
|
+
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
6618
|
+
const config2 = c.get("config");
|
|
6619
|
+
if (config2.collections.some((col) => col.slug === slug)) {
|
|
6620
|
+
return c.json({ message: "Not Found" }, 404);
|
|
6621
|
+
}
|
|
6622
|
+
if (!config2.onSchemaFetch || !siteId) {
|
|
6623
|
+
return c.json({ message: `Collection "${slug}" not found` }, 404);
|
|
6624
|
+
}
|
|
6625
|
+
const requestConfig = mergeDynamicConfig(
|
|
6626
|
+
config2,
|
|
6627
|
+
await config2.onSchemaFetch(siteId)
|
|
6628
|
+
);
|
|
6629
|
+
const collection = requestConfig.collections.find(
|
|
6630
|
+
(col) => col.slug === slug
|
|
6631
|
+
);
|
|
6632
|
+
if (!collection?.workflow) {
|
|
6633
|
+
return c.json(
|
|
6634
|
+
{ message: `Collection "${slug}" not found or has no workflow` },
|
|
6635
|
+
404
|
|
6636
|
+
);
|
|
6637
|
+
}
|
|
6638
|
+
const controller = new CollectionController(collection);
|
|
6639
|
+
return controller.transition(c);
|
|
4760
6640
|
}
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
6641
|
+
);
|
|
6642
|
+
app.get(
|
|
6643
|
+
"/api/collections/:slug/:id/workflow-history",
|
|
6644
|
+
requireAuth(config),
|
|
6645
|
+
async (c) => {
|
|
6646
|
+
const slug = c.req.param("slug");
|
|
6647
|
+
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
6648
|
+
const config2 = c.get("config");
|
|
6649
|
+
if (config2.collections.some((col) => col.slug === slug)) {
|
|
6650
|
+
return c.json({ message: "Not Found" }, 404);
|
|
6651
|
+
}
|
|
6652
|
+
if (!config2.onSchemaFetch || !siteId) {
|
|
6653
|
+
return c.json({ message: `Collection "${slug}" not found` }, 404);
|
|
6654
|
+
}
|
|
6655
|
+
const requestConfig = mergeDynamicConfig(
|
|
6656
|
+
config2,
|
|
6657
|
+
await config2.onSchemaFetch(siteId)
|
|
6658
|
+
);
|
|
6659
|
+
const collection = requestConfig.collections.find(
|
|
6660
|
+
(col) => col.slug === slug
|
|
6661
|
+
);
|
|
6662
|
+
if (!collection?.workflow) {
|
|
6663
|
+
return c.json(
|
|
6664
|
+
{ message: `Collection "${slug}" not found or has no workflow` },
|
|
6665
|
+
404
|
|
6666
|
+
);
|
|
6667
|
+
}
|
|
6668
|
+
const controller = new CollectionController(collection);
|
|
6669
|
+
return controller.workflowHistory(c);
|
|
4765
6670
|
}
|
|
4766
|
-
|
|
4767
|
-
return controller.workflowHistory(c);
|
|
4768
|
-
});
|
|
6671
|
+
);
|
|
4769
6672
|
app.all("/api/collections/:slug/:id?", async (c) => {
|
|
4770
6673
|
const slug = c.req.param("slug");
|
|
4771
6674
|
const id = c.req.param("id");
|
|
@@ -4775,8 +6678,13 @@ function registerRoutes(app, config) {
|
|
|
4775
6678
|
return c.json({ message: "Method Not Allowed" }, 405);
|
|
4776
6679
|
}
|
|
4777
6680
|
if (config2.onSchemaFetch && siteId) {
|
|
4778
|
-
const
|
|
4779
|
-
|
|
6681
|
+
const requestConfig = mergeDynamicConfig(
|
|
6682
|
+
config2,
|
|
6683
|
+
await config2.onSchemaFetch(siteId)
|
|
6684
|
+
);
|
|
6685
|
+
let collection = requestConfig.collections.find(
|
|
6686
|
+
(col) => col.slug === slug
|
|
6687
|
+
);
|
|
4780
6688
|
if (!collection && slug === "media") {
|
|
4781
6689
|
collection = {
|
|
4782
6690
|
slug: "media",
|
|
@@ -4789,19 +6697,25 @@ function registerRoutes(app, config) {
|
|
|
4789
6697
|
if (collection.auth && id) {
|
|
4790
6698
|
const authController = new AuthController(collection);
|
|
4791
6699
|
const method2 = c.req.method;
|
|
4792
|
-
if (method2 === "POST" && id === "login")
|
|
4793
|
-
|
|
6700
|
+
if (method2 === "POST" && id === "login")
|
|
6701
|
+
return authController.login(c);
|
|
6702
|
+
if (method2 === "POST" && id === "logout")
|
|
6703
|
+
return authController.logout(c);
|
|
4794
6704
|
if (method2 === "GET" && id === "me") return authController.me(c);
|
|
4795
|
-
if (method2 === "POST" && id === "refresh-token")
|
|
4796
|
-
|
|
4797
|
-
if (method2 === "POST" && id === "
|
|
6705
|
+
if (method2 === "POST" && id === "refresh-token")
|
|
6706
|
+
return authController.refreshToken(c);
|
|
6707
|
+
if (method2 === "POST" && id === "forgot-password")
|
|
6708
|
+
return authController.forgotPassword(c);
|
|
6709
|
+
if (method2 === "POST" && id === "reset-password")
|
|
6710
|
+
return authController.resetPassword(c);
|
|
4798
6711
|
}
|
|
4799
6712
|
const controller = new CollectionController(collection);
|
|
4800
6713
|
const method = c.req.method;
|
|
4801
6714
|
if (id) {
|
|
4802
6715
|
if (method === "GET") return controller.findOne(c);
|
|
4803
6716
|
if (method === "PATCH") return controller.update(c);
|
|
4804
|
-
if (method === "DELETE" && id === "delete-many")
|
|
6717
|
+
if (method === "DELETE" && id === "delete-many")
|
|
6718
|
+
return controller.deleteMany(c);
|
|
4805
6719
|
if (method === "DELETE") return controller.delete(c);
|
|
4806
6720
|
if (method === "POST" && id === "media") return controller.create(c);
|
|
4807
6721
|
if (method === "POST" && id === "seed") return controller.seed(c);
|
|
@@ -4822,8 +6736,11 @@ function registerRoutes(app, config) {
|
|
|
4822
6736
|
return c.json({ message: "Method Not Allowed" }, 405);
|
|
4823
6737
|
}
|
|
4824
6738
|
if (config2.onSchemaFetch && siteId) {
|
|
4825
|
-
const
|
|
4826
|
-
|
|
6739
|
+
const requestConfig = mergeDynamicConfig(
|
|
6740
|
+
config2,
|
|
6741
|
+
await config2.onSchemaFetch(siteId)
|
|
6742
|
+
);
|
|
6743
|
+
const global = requestConfig.globals.find((glb) => glb.slug === slug);
|
|
4827
6744
|
if (global) {
|
|
4828
6745
|
const controller = new GlobalController(global);
|
|
4829
6746
|
const method = c.req.method;
|
|
@@ -4873,12 +6790,23 @@ var AUDIT_COLLECTION = {
|
|
|
4873
6790
|
fields: [
|
|
4874
6791
|
{ name: "collection", type: "text", label: "Collection", required: true },
|
|
4875
6792
|
{ name: "documentId", type: "text", label: "Document ID" },
|
|
4876
|
-
{
|
|
6793
|
+
{
|
|
6794
|
+
name: "operation",
|
|
6795
|
+
type: "select",
|
|
6796
|
+
label: "Operation",
|
|
6797
|
+
options: ["create", "update", "delete"],
|
|
6798
|
+
required: true
|
|
6799
|
+
},
|
|
4877
6800
|
{ name: "user", type: "text", label: "User ID" },
|
|
4878
6801
|
{ name: "timestamp", type: "date", label: "Timestamp", required: true },
|
|
4879
6802
|
{ name: "changes", type: "json", label: "Changes" }
|
|
4880
6803
|
],
|
|
4881
|
-
access: {
|
|
6804
|
+
access: {
|
|
6805
|
+
read: () => false,
|
|
6806
|
+
create: () => false,
|
|
6807
|
+
update: () => false,
|
|
6808
|
+
delete: () => false
|
|
6809
|
+
},
|
|
4882
6810
|
admin: { hidden: true }
|
|
4883
6811
|
};
|
|
4884
6812
|
var WORKFLOW_HISTORY_COLLECTION_CONFIG = {
|
|
@@ -4895,7 +6823,12 @@ var WORKFLOW_HISTORY_COLLECTION_CONFIG = {
|
|
|
4895
6823
|
{ name: "actorId", type: "text" },
|
|
4896
6824
|
{ name: "createdAt", type: "date", required: true }
|
|
4897
6825
|
],
|
|
4898
|
-
access: {
|
|
6826
|
+
access: {
|
|
6827
|
+
read: ({ user }) => !!user,
|
|
6828
|
+
create: () => false,
|
|
6829
|
+
update: () => false,
|
|
6830
|
+
delete: () => false
|
|
6831
|
+
},
|
|
4899
6832
|
admin: { hidden: true }
|
|
4900
6833
|
};
|
|
4901
6834
|
var LIFECYCLE_EVENTS_COLLECTION_CONFIG = {
|
|
@@ -4909,22 +6842,56 @@ var LIFECYCLE_EVENTS_COLLECTION_CONFIG = {
|
|
|
4909
6842
|
{ name: "actorId", type: "text" },
|
|
4910
6843
|
{ name: "payload", type: "json", required: true },
|
|
4911
6844
|
{ name: "attempts", type: "number", required: true },
|
|
4912
|
-
{
|
|
6845
|
+
{
|
|
6846
|
+
name: "status",
|
|
6847
|
+
type: "select",
|
|
6848
|
+
options: ["pending", "processing", "delivered", "failed"],
|
|
6849
|
+
required: true
|
|
6850
|
+
},
|
|
4913
6851
|
{ name: "nextAttemptAt", type: "date" },
|
|
4914
6852
|
{ name: "deliveredAt", type: "date" },
|
|
4915
6853
|
{ name: "lastError", type: "textarea" }
|
|
4916
6854
|
],
|
|
4917
|
-
access: {
|
|
6855
|
+
access: {
|
|
6856
|
+
read: ({ user }) => !!user?.roles?.includes("admin"),
|
|
6857
|
+
create: () => false,
|
|
6858
|
+
update: () => false,
|
|
6859
|
+
delete: () => false
|
|
6860
|
+
},
|
|
6861
|
+
admin: { hidden: true }
|
|
6862
|
+
};
|
|
6863
|
+
var AUTH_SESSIONS_COLLECTION_CONFIG = {
|
|
6864
|
+
slug: AUTH_SESSIONS_COLLECTION,
|
|
6865
|
+
labels: { singular: "Auth session", plural: "Auth sessions" },
|
|
6866
|
+
fields: [
|
|
6867
|
+
{ name: "userId", type: "text", required: true },
|
|
6868
|
+
{ name: "email", type: "email", required: true },
|
|
6869
|
+
{ name: "collection", type: "text", required: true },
|
|
6870
|
+
{ name: "authSource", type: "text" },
|
|
6871
|
+
{ name: "providerId", type: "text" },
|
|
6872
|
+
{ name: "lastIp", type: "text" },
|
|
6873
|
+
{ name: "lastSeenAt", type: "date" },
|
|
6874
|
+
{ name: "expiresAt", type: "date" },
|
|
6875
|
+
{ name: "revokedAt", type: "date" }
|
|
6876
|
+
],
|
|
6877
|
+
access: {
|
|
6878
|
+
read: () => false,
|
|
6879
|
+
create: () => false,
|
|
6880
|
+
update: () => false,
|
|
6881
|
+
delete: () => false
|
|
6882
|
+
},
|
|
4918
6883
|
admin: { hidden: true }
|
|
4919
6884
|
};
|
|
4920
6885
|
function normalizeConfig(config) {
|
|
4921
|
-
const
|
|
4922
|
-
const
|
|
6886
|
+
const schemaAwareConfig = normalizeSchemaFragment(config);
|
|
6887
|
+
const collections = schemaAwareConfig?.collections || [];
|
|
6888
|
+
const globals = schemaAwareConfig?.globals || [];
|
|
4923
6889
|
const needsAudit = collections.some((col) => col.audit);
|
|
4924
6890
|
const needsWorkflow = collections.some((col) => col.workflow || col.drafts);
|
|
6891
|
+
const needsAuthSessions = collections.some((col) => !!col.auth);
|
|
4925
6892
|
const adminAuthCollectionSlug = getAdminAuthCollection({
|
|
4926
6893
|
collections,
|
|
4927
|
-
adminAuth:
|
|
6894
|
+
adminAuth: schemaAwareConfig.adminAuth
|
|
4928
6895
|
})?.slug;
|
|
4929
6896
|
const normalizedCollections = collections.map((col) => {
|
|
4930
6897
|
let fields = col.fields || [];
|
|
@@ -5021,7 +6988,7 @@ function normalizeConfig(config) {
|
|
|
5021
6988
|
return field;
|
|
5022
6989
|
});
|
|
5023
6990
|
}
|
|
5024
|
-
if (adminAuthCollectionSlug && col.slug === adminAuthCollectionSlug &&
|
|
6991
|
+
if (adminAuthCollectionSlug && col.slug === adminAuthCollectionSlug && schemaAwareConfig.adminAuth?.mode === "external") {
|
|
5025
6992
|
const externalAdminFields = [
|
|
5026
6993
|
{
|
|
5027
6994
|
name: "authProvider",
|
|
@@ -5055,7 +7022,9 @@ function normalizeConfig(config) {
|
|
|
5055
7022
|
}
|
|
5056
7023
|
}
|
|
5057
7024
|
const updatedFieldNames = new Set(fields.map((f) => f.name));
|
|
5058
|
-
const fieldsToInject = SYSTEM_FIELDS.filter(
|
|
7025
|
+
const fieldsToInject = SYSTEM_FIELDS.filter(
|
|
7026
|
+
(f) => !updatedFieldNames.has(f.name)
|
|
7027
|
+
);
|
|
5059
7028
|
const workflow = col.workflow || (col.drafts ? simplePublishingWorkflow() : void 0);
|
|
5060
7029
|
return {
|
|
5061
7030
|
...col,
|
|
@@ -5063,52 +7032,309 @@ function normalizeConfig(config) {
|
|
|
5063
7032
|
fields: [...fields, ...fieldsToInject]
|
|
5064
7033
|
};
|
|
5065
7034
|
});
|
|
5066
|
-
const hasAuditCollection = normalizedCollections.some(
|
|
7035
|
+
const hasAuditCollection = normalizedCollections.some(
|
|
7036
|
+
(col) => col.slug === AUDIT_COLLECTION_SLUG
|
|
7037
|
+
);
|
|
5067
7038
|
const systemCollections = [];
|
|
5068
|
-
if (needsAudit && !hasAuditCollection)
|
|
5069
|
-
|
|
7039
|
+
if (needsAudit && !hasAuditCollection)
|
|
7040
|
+
systemCollections.push(AUDIT_COLLECTION);
|
|
7041
|
+
if (needsWorkflow && !normalizedCollections.some(
|
|
7042
|
+
(col) => col.slug === WORKFLOW_HISTORY_COLLECTION
|
|
7043
|
+
)) {
|
|
5070
7044
|
systemCollections.push(WORKFLOW_HISTORY_COLLECTION_CONFIG);
|
|
5071
7045
|
}
|
|
5072
|
-
if (needsWorkflow && !normalizedCollections.some(
|
|
7046
|
+
if (needsWorkflow && !normalizedCollections.some(
|
|
7047
|
+
(col) => col.slug === LIFECYCLE_EVENTS_COLLECTION
|
|
7048
|
+
)) {
|
|
5073
7049
|
systemCollections.push(LIFECYCLE_EVENTS_COLLECTION_CONFIG);
|
|
5074
7050
|
}
|
|
7051
|
+
if (needsAuthSessions && !normalizedCollections.some((col) => col.slug === AUTH_SESSIONS_COLLECTION)) {
|
|
7052
|
+
systemCollections.push(AUTH_SESSIONS_COLLECTION_CONFIG);
|
|
7053
|
+
}
|
|
5075
7054
|
return {
|
|
5076
|
-
...
|
|
7055
|
+
...schemaAwareConfig,
|
|
5077
7056
|
collections: [...normalizedCollections, ...systemCollections],
|
|
5078
7057
|
globals
|
|
5079
7058
|
};
|
|
5080
7059
|
}
|
|
5081
7060
|
|
|
7061
|
+
// src/network.ts
|
|
7062
|
+
var DIRECT_IP_HEADERS = [
|
|
7063
|
+
"cf-connecting-ip",
|
|
7064
|
+
"fly-client-ip",
|
|
7065
|
+
"true-client-ip",
|
|
7066
|
+
"x-real-ip",
|
|
7067
|
+
"x-client-ip",
|
|
7068
|
+
"fastly-client-ip"
|
|
7069
|
+
];
|
|
7070
|
+
function toHookRequestContext2(c) {
|
|
7071
|
+
const headers = Object.fromEntries(
|
|
7072
|
+
Object.entries(c.req.header()).map(([key, value]) => [key.toLowerCase(), value])
|
|
7073
|
+
);
|
|
7074
|
+
return {
|
|
7075
|
+
query: c.req.query(),
|
|
7076
|
+
headers,
|
|
7077
|
+
raw: c.req.raw
|
|
7078
|
+
};
|
|
7079
|
+
}
|
|
7080
|
+
function resolveClientIp(req, trustProxy = false) {
|
|
7081
|
+
for (const header of DIRECT_IP_HEADERS) {
|
|
7082
|
+
const value = normalizeIp(req.headers[header]);
|
|
7083
|
+
if (value) return value;
|
|
7084
|
+
}
|
|
7085
|
+
if (trustProxy) {
|
|
7086
|
+
const forwardedFor = pickForwardedFor(req.headers, trustProxy);
|
|
7087
|
+
if (forwardedFor) return forwardedFor;
|
|
7088
|
+
}
|
|
7089
|
+
return "127.0.0.1";
|
|
7090
|
+
}
|
|
7091
|
+
function pickForwardedFor(headers, trustProxy) {
|
|
7092
|
+
const forwardedHeader = headers["x-forwarded-for"];
|
|
7093
|
+
if (forwardedHeader) {
|
|
7094
|
+
const chain2 = forwardedHeader.split(",").map((entry) => normalizeIp(entry)).filter((entry) => !!entry);
|
|
7095
|
+
if (chain2.length > 0) {
|
|
7096
|
+
if (trustProxy === true) return chain2[0];
|
|
7097
|
+
const trustedHops2 = Math.max(1, Math.floor(trustProxy));
|
|
7098
|
+
const candidateIndex2 = Math.max(0, chain2.length - trustedHops2);
|
|
7099
|
+
return chain2[candidateIndex2] ?? chain2[0];
|
|
7100
|
+
}
|
|
7101
|
+
}
|
|
7102
|
+
const forwarded = headers.forwarded;
|
|
7103
|
+
if (!forwarded) return null;
|
|
7104
|
+
const matches = [...forwarded.matchAll(/for=(?:"?\[?([^;\],"]+)\]?"?)/gi)];
|
|
7105
|
+
if (matches.length === 0) return null;
|
|
7106
|
+
const chain = matches.map((match) => normalizeIp(match[1])).filter((entry) => !!entry);
|
|
7107
|
+
if (chain.length === 0) return null;
|
|
7108
|
+
if (trustProxy === true) return chain[0];
|
|
7109
|
+
const trustedHops = Math.max(1, Math.floor(trustProxy));
|
|
7110
|
+
const candidateIndex = Math.max(0, chain.length - trustedHops);
|
|
7111
|
+
return chain[candidateIndex] ?? chain[0];
|
|
7112
|
+
}
|
|
7113
|
+
function normalizeIp(value) {
|
|
7114
|
+
if (!value) return null;
|
|
7115
|
+
const trimmed = value.trim();
|
|
7116
|
+
if (!trimmed || trimmed.toLowerCase() === "unknown") return null;
|
|
7117
|
+
return trimmed.replace(/^\[(.*)\]$/, "$1");
|
|
7118
|
+
}
|
|
7119
|
+
|
|
7120
|
+
// src/middleware/rate-limit.ts
|
|
7121
|
+
var DEFAULT_WINDOW_MS = 15 * 60 * 1e3;
|
|
7122
|
+
var DEFAULT_MAX_REQUESTS = 500;
|
|
7123
|
+
var DEFAULT_PATHS = ["/api"];
|
|
7124
|
+
function createRateLimitMiddleware(config) {
|
|
7125
|
+
const settings = normalizeRateLimitConfig(config.rateLimit);
|
|
7126
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
7127
|
+
return async (c, next) => {
|
|
7128
|
+
const req = toHookRequestContext2(c);
|
|
7129
|
+
const ip = resolveClientIp(req, settings.trustProxy);
|
|
7130
|
+
c.set("clientIp", ip);
|
|
7131
|
+
if (!settings.enabled) {
|
|
7132
|
+
return next();
|
|
7133
|
+
}
|
|
7134
|
+
const path = c.req.path;
|
|
7135
|
+
if (!matchesLimitedPath(path, settings.paths)) {
|
|
7136
|
+
return next();
|
|
7137
|
+
}
|
|
7138
|
+
if (settings.skip && await settings.skip({
|
|
7139
|
+
ip,
|
|
7140
|
+
path,
|
|
7141
|
+
method: c.req.method,
|
|
7142
|
+
req
|
|
7143
|
+
})) {
|
|
7144
|
+
return next();
|
|
7145
|
+
}
|
|
7146
|
+
const now = Date.now();
|
|
7147
|
+
const key = ip;
|
|
7148
|
+
const existing = buckets.get(key);
|
|
7149
|
+
const bucket = existing && existing.resetAt > now ? existing : { count: 0, resetAt: now + settings.window };
|
|
7150
|
+
bucket.count += 1;
|
|
7151
|
+
buckets.set(key, bucket);
|
|
7152
|
+
pruneExpiredBuckets(buckets, now);
|
|
7153
|
+
const remaining = Math.max(0, settings.max - bucket.count);
|
|
7154
|
+
const resetSeconds = Math.max(1, Math.ceil((bucket.resetAt - now) / 1e3));
|
|
7155
|
+
c.res.headers.set("X-RateLimit-Limit", String(settings.max));
|
|
7156
|
+
c.res.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
7157
|
+
c.res.headers.set("X-RateLimit-Reset", String(Math.ceil(bucket.resetAt / 1e3)));
|
|
7158
|
+
if (bucket.count > settings.max) {
|
|
7159
|
+
c.res.headers.set("Retry-After", String(resetSeconds));
|
|
7160
|
+
return c.json(
|
|
7161
|
+
{
|
|
7162
|
+
error: true,
|
|
7163
|
+
message: "Too many requests. Try again later.",
|
|
7164
|
+
retryAfterSeconds: resetSeconds
|
|
7165
|
+
},
|
|
7166
|
+
429
|
|
7167
|
+
);
|
|
7168
|
+
}
|
|
7169
|
+
await next();
|
|
7170
|
+
};
|
|
7171
|
+
}
|
|
7172
|
+
function normalizeRateLimitConfig(config) {
|
|
7173
|
+
return {
|
|
7174
|
+
enabled: config?.enabled ?? true,
|
|
7175
|
+
window: config?.window ?? DEFAULT_WINDOW_MS,
|
|
7176
|
+
max: config?.max ?? DEFAULT_MAX_REQUESTS,
|
|
7177
|
+
paths: config?.paths?.length ? config.paths : DEFAULT_PATHS,
|
|
7178
|
+
skip: config?.skip,
|
|
7179
|
+
trustProxy: config?.trustProxy ?? false
|
|
7180
|
+
};
|
|
7181
|
+
}
|
|
7182
|
+
function matchesLimitedPath(path, prefixes) {
|
|
7183
|
+
return prefixes.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
|
|
7184
|
+
}
|
|
7185
|
+
function pruneExpiredBuckets(buckets, now) {
|
|
7186
|
+
if (buckets.size < 1e3) return;
|
|
7187
|
+
for (const [key, bucket] of buckets) {
|
|
7188
|
+
if (bucket.resetAt <= now) {
|
|
7189
|
+
buckets.delete(key);
|
|
7190
|
+
}
|
|
7191
|
+
}
|
|
7192
|
+
}
|
|
7193
|
+
|
|
5082
7194
|
// src/app.ts
|
|
5083
7195
|
async function createDyrectedApp(rawConfig) {
|
|
5084
7196
|
const config = normalizeConfig(rawConfig);
|
|
7197
|
+
const observability = createObservabilityRuntime(config);
|
|
7198
|
+
bindObservabilityRuntime(config, observability);
|
|
7199
|
+
config.logger = observability.logger;
|
|
5085
7200
|
const app = new import_hono.Hono();
|
|
5086
7201
|
if (config.db?.sync) {
|
|
5087
7202
|
await config.db.sync(config.collections, config.globals);
|
|
5088
7203
|
}
|
|
5089
7204
|
app.use("*", (0, import_request_id.requestId)());
|
|
5090
|
-
app.use("*", optionalAuth(config));
|
|
5091
|
-
app.use("*", async (c, next) => {
|
|
5092
|
-
const start = Date.now();
|
|
5093
|
-
await next();
|
|
5094
|
-
const ms = Date.now() - start;
|
|
5095
|
-
console.log(`[dyrected/api] ${c.req.method} ${c.req.path} ${c.res.status} - ${ms}ms`);
|
|
5096
|
-
});
|
|
5097
|
-
app.use("*", (0, import_cors.cors)());
|
|
5098
7205
|
app.use("*", async (c, next) => {
|
|
5099
7206
|
c.set("config", config);
|
|
7207
|
+
c.set("observability", observability);
|
|
5100
7208
|
if (!c.get("siteId")) {
|
|
5101
7209
|
c.set("siteId", "default");
|
|
5102
7210
|
}
|
|
7211
|
+
c.set("logger", buildRequestLogger(observability, c.var, c.get("requestId")));
|
|
5103
7212
|
await next();
|
|
5104
7213
|
});
|
|
7214
|
+
app.use("*", async (c, next) => {
|
|
7215
|
+
const start = Date.now();
|
|
7216
|
+
const method = c.req.method;
|
|
7217
|
+
const path = c.req.path;
|
|
7218
|
+
const requestId2 = c.get("requestId");
|
|
7219
|
+
const observabilityConfig = observability.config;
|
|
7220
|
+
const traceSampled = shouldSampleRequest(
|
|
7221
|
+
requestId2,
|
|
7222
|
+
200,
|
|
7223
|
+
observabilityConfig.sampling,
|
|
7224
|
+
"trace"
|
|
7225
|
+
);
|
|
7226
|
+
const requestTrace = traceSampled ? createRequestSpan(observability, `${method} ${path}`, {
|
|
7227
|
+
"http.method": method,
|
|
7228
|
+
"http.route": path,
|
|
7229
|
+
requestId: requestId2,
|
|
7230
|
+
siteId: c.get("siteId"),
|
|
7231
|
+
workspaceId: c.get("workspaceId")
|
|
7232
|
+
}) : void 0;
|
|
7233
|
+
if (requestTrace) {
|
|
7234
|
+
c.set("requestTrace", requestTrace);
|
|
7235
|
+
c.set("logger", buildRequestLogger(observability, c.var, requestId2));
|
|
7236
|
+
}
|
|
7237
|
+
let bodyCapturePromise;
|
|
7238
|
+
if (observabilityConfig.requestLogging.logBodies) {
|
|
7239
|
+
const bodySampled = shouldSampleRequest(
|
|
7240
|
+
requestId2,
|
|
7241
|
+
200,
|
|
7242
|
+
observabilityConfig.sampling,
|
|
7243
|
+
"body"
|
|
7244
|
+
);
|
|
7245
|
+
if (bodySampled) {
|
|
7246
|
+
bodyCapturePromise = captureRequestBody(c.req.raw, observabilityConfig);
|
|
7247
|
+
}
|
|
7248
|
+
}
|
|
7249
|
+
let caughtError;
|
|
7250
|
+
try {
|
|
7251
|
+
await next();
|
|
7252
|
+
} catch (error) {
|
|
7253
|
+
caughtError = error;
|
|
7254
|
+
throw error;
|
|
7255
|
+
} finally {
|
|
7256
|
+
const durationMs = Date.now() - start;
|
|
7257
|
+
const statusCode = c.res.status || (caughtError ? 500 : 200);
|
|
7258
|
+
const sampled = shouldSampleRequest(
|
|
7259
|
+
requestId2,
|
|
7260
|
+
statusCode,
|
|
7261
|
+
observabilityConfig.sampling,
|
|
7262
|
+
"log"
|
|
7263
|
+
);
|
|
7264
|
+
const requestLogger = getRequestLogger(c, "api");
|
|
7265
|
+
const requestHeaders = redactHeaders(
|
|
7266
|
+
Object.fromEntries(c.req.raw.headers.entries()),
|
|
7267
|
+
observabilityConfig
|
|
7268
|
+
);
|
|
7269
|
+
const requestBodyCapture = bodyCapturePromise ? await bodyCapturePromise : void 0;
|
|
7270
|
+
if (requestBodyCapture) {
|
|
7271
|
+
c.set("requestBodyCapture", requestBodyCapture);
|
|
7272
|
+
}
|
|
7273
|
+
attachRequestMetrics(observability, {
|
|
7274
|
+
method,
|
|
7275
|
+
route: path,
|
|
7276
|
+
statusCode,
|
|
7277
|
+
durationMs
|
|
7278
|
+
});
|
|
7279
|
+
if (observabilityConfig.requestLogging.enabled && sampled) {
|
|
7280
|
+
const level = statusCode >= 500 ? "error" : statusCode >= 400 ? "warn" : "info";
|
|
7281
|
+
requestLogger[level]({
|
|
7282
|
+
msg: "HTTP request completed",
|
|
7283
|
+
method,
|
|
7284
|
+
path,
|
|
7285
|
+
statusCode,
|
|
7286
|
+
durationMs,
|
|
7287
|
+
requestId: requestId2,
|
|
7288
|
+
siteId: c.get("siteId"),
|
|
7289
|
+
workspaceId: c.get("workspaceId"),
|
|
7290
|
+
contentType: c.req.header("content-type") ?? void 0,
|
|
7291
|
+
contentLength: c.req.header("content-length") ? Number(c.req.header("content-length")) : void 0,
|
|
7292
|
+
sampled,
|
|
7293
|
+
headers: Object.keys(requestHeaders).length > 0 ? requestHeaders : void 0,
|
|
7294
|
+
body: requestBodyCapture?.body,
|
|
7295
|
+
bodyCapture: requestBodyCapture && requestBodyCapture.body === void 0 ? {
|
|
7296
|
+
attempted: requestBodyCapture.attempted,
|
|
7297
|
+
contentType: requestBodyCapture.contentType,
|
|
7298
|
+
contentLength: requestBodyCapture.contentLength,
|
|
7299
|
+
truncated: requestBodyCapture.truncated,
|
|
7300
|
+
parseFailed: requestBodyCapture.parseFailed
|
|
7301
|
+
} : void 0
|
|
7302
|
+
});
|
|
7303
|
+
}
|
|
7304
|
+
endRequestSpan(requestTrace, statusCode, caughtError);
|
|
7305
|
+
}
|
|
7306
|
+
});
|
|
7307
|
+
app.use("*", createRateLimitMiddleware(config));
|
|
7308
|
+
app.use("*", optionalAuth(config));
|
|
7309
|
+
app.use("*", (0, import_cors.cors)(config.cors ? { origin: config.cors.origins } : {}));
|
|
5105
7310
|
app.get("/health", (c) => c.json({ status: "ok", version: "0.0.1" }));
|
|
7311
|
+
if (observability.config.metrics.enabled && observability.config.metrics.exporter === "prometheus" && observability.prometheusExporter) {
|
|
7312
|
+
app.get(observability.config.metrics.path, async (c) => {
|
|
7313
|
+
const metrics = await renderPrometheusMetrics(
|
|
7314
|
+
observability.prometheusExporter
|
|
7315
|
+
);
|
|
7316
|
+
return new Response(metrics.body, {
|
|
7317
|
+
status: metrics.statusCode,
|
|
7318
|
+
headers: metrics.headers
|
|
7319
|
+
});
|
|
7320
|
+
});
|
|
7321
|
+
}
|
|
5106
7322
|
app.get("/routes", (c) => {
|
|
5107
7323
|
const routes = app.routes.map((r) => ({ method: r.method, path: r.path }));
|
|
5108
7324
|
return c.json({ routes });
|
|
5109
7325
|
});
|
|
5110
7326
|
app.onError((err, c) => {
|
|
5111
|
-
|
|
7327
|
+
const logger = getRequestLogger(c, "core");
|
|
7328
|
+
logger.error({
|
|
7329
|
+
err,
|
|
7330
|
+
msg: "Uncaught error",
|
|
7331
|
+
path: c.req.path,
|
|
7332
|
+
method: c.req.method
|
|
7333
|
+
});
|
|
7334
|
+
observability.recordUncaughtError({
|
|
7335
|
+
path: c.req.path,
|
|
7336
|
+
method: c.req.method
|
|
7337
|
+
});
|
|
5112
7338
|
return c.json({
|
|
5113
7339
|
message: err.message || "Internal Server Error",
|
|
5114
7340
|
stack: process.env.NODE_ENV === "development" ? err.stack : void 0
|