@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.js
CHANGED
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
import {
|
|
2
2
|
WORKFLOW_HISTORY_COLLECTION,
|
|
3
|
+
attachRequestMetrics,
|
|
4
|
+
bindObservabilityRuntime,
|
|
5
|
+
buildRequestLogger,
|
|
3
6
|
canViewWorkflowDraft,
|
|
7
|
+
captureRequestBody,
|
|
8
|
+
createObservabilityRuntime,
|
|
9
|
+
createRequestSpan,
|
|
4
10
|
createWorkflowDocument,
|
|
11
|
+
endRequestSpan,
|
|
5
12
|
executeFieldAfterRead,
|
|
6
13
|
executeFieldBeforeChange,
|
|
7
14
|
generateOpenApi,
|
|
8
15
|
getAdminAuthCollection,
|
|
16
|
+
getAuthSession,
|
|
17
|
+
getConfigLogger,
|
|
18
|
+
getObservabilityRuntime,
|
|
9
19
|
getPublicAdminAuthConfig,
|
|
20
|
+
getRequestLogger,
|
|
10
21
|
initializeWorkflowDocument,
|
|
22
|
+
isAuthSessionActive,
|
|
23
|
+
issueAuthSessionToken,
|
|
11
24
|
materializeWorkflowDocument,
|
|
25
|
+
mergeDynamicConfig,
|
|
12
26
|
normalizeConfig,
|
|
27
|
+
redactHeaders,
|
|
28
|
+
renderPrometheusMetrics,
|
|
29
|
+
revokeAllAuthSessions,
|
|
30
|
+
revokeAuthSession,
|
|
13
31
|
runCollectionHooks,
|
|
14
32
|
saveWorkflowDraft,
|
|
15
|
-
|
|
16
|
-
|
|
33
|
+
shouldSampleRequest,
|
|
34
|
+
signCollectionToken,
|
|
35
|
+
touchAuthSession,
|
|
36
|
+
transitionWorkflow,
|
|
37
|
+
verifyCollectionToken
|
|
38
|
+
} from "./chunk-BQV3QW3Y.js";
|
|
17
39
|
|
|
18
40
|
// src/app.ts
|
|
19
41
|
import { Hono } from "hono";
|
|
@@ -81,12 +103,16 @@ var PopulationService = class {
|
|
|
81
103
|
return data;
|
|
82
104
|
}
|
|
83
105
|
if (Array.isArray(data)) {
|
|
84
|
-
return Promise.all(
|
|
106
|
+
return Promise.all(
|
|
107
|
+
data.map((item) => this.populate({ ...args, data: item }))
|
|
108
|
+
);
|
|
85
109
|
}
|
|
86
110
|
const populatedDoc = { ...data };
|
|
87
111
|
for (const field of fields) {
|
|
88
112
|
if (field.type === "join" && field.collection && field.on && currentDepth === 0) {
|
|
89
|
-
const targetCollection = this.collections.find(
|
|
113
|
+
const targetCollection = this.collections.find(
|
|
114
|
+
(c) => c.slug === field.collection
|
|
115
|
+
);
|
|
90
116
|
if (targetCollection) {
|
|
91
117
|
const docId = populatedDoc.id;
|
|
92
118
|
if (docId) {
|
|
@@ -98,7 +124,9 @@ var PopulationService = class {
|
|
|
98
124
|
limit: joinLimit
|
|
99
125
|
});
|
|
100
126
|
const populatedDocs = await this.populate({
|
|
101
|
-
data: result.docs.map(
|
|
127
|
+
data: result.docs.map(
|
|
128
|
+
(doc) => DefaultsService.apply(targetCollection.fields, doc)
|
|
129
|
+
),
|
|
102
130
|
fields: targetCollection.fields,
|
|
103
131
|
currentDepth: 1,
|
|
104
132
|
maxDepth
|
|
@@ -113,14 +141,21 @@ var PopulationService = class {
|
|
|
113
141
|
continue;
|
|
114
142
|
}
|
|
115
143
|
if (field.type === "row" && field.fields) {
|
|
116
|
-
const rowPopulated = await this.populate({
|
|
144
|
+
const rowPopulated = await this.populate({
|
|
145
|
+
data,
|
|
146
|
+
fields: field.fields,
|
|
147
|
+
currentDepth,
|
|
148
|
+
maxDepth
|
|
149
|
+
});
|
|
117
150
|
Object.assign(populatedDoc, rowPopulated);
|
|
118
151
|
continue;
|
|
119
152
|
}
|
|
120
153
|
if (!field.name) continue;
|
|
121
154
|
const value = populatedDoc[field.name];
|
|
122
155
|
if (field.type === "relationship" && field.relationTo && value) {
|
|
123
|
-
const relatedCollection = this.collections.find(
|
|
156
|
+
const relatedCollection = this.collections.find(
|
|
157
|
+
(c) => c.slug === field.relationTo
|
|
158
|
+
);
|
|
124
159
|
if (!relatedCollection) continue;
|
|
125
160
|
if (Array.isArray(value)) {
|
|
126
161
|
populatedDoc[field.name] = await Promise.all(
|
|
@@ -128,10 +163,16 @@ var PopulationService = class {
|
|
|
128
163
|
if (!id) return id;
|
|
129
164
|
let doc = id;
|
|
130
165
|
if (typeof id === "string") {
|
|
131
|
-
doc = await this.db.findOne({
|
|
166
|
+
doc = await this.db.findOne({
|
|
167
|
+
collection: field.relationTo,
|
|
168
|
+
id
|
|
169
|
+
});
|
|
132
170
|
}
|
|
133
171
|
if (!doc || typeof doc !== "object") return id;
|
|
134
|
-
const docWithDefaults = DefaultsService.apply(
|
|
172
|
+
const docWithDefaults = DefaultsService.apply(
|
|
173
|
+
relatedCollection.fields,
|
|
174
|
+
doc
|
|
175
|
+
);
|
|
135
176
|
return this.populate({
|
|
136
177
|
data: docWithDefaults,
|
|
137
178
|
fields: relatedCollection.fields,
|
|
@@ -143,10 +184,16 @@ var PopulationService = class {
|
|
|
143
184
|
} else if (value) {
|
|
144
185
|
let doc = value;
|
|
145
186
|
if (typeof value === "string") {
|
|
146
|
-
doc = await this.db.findOne({
|
|
187
|
+
doc = await this.db.findOne({
|
|
188
|
+
collection: field.relationTo,
|
|
189
|
+
id: value
|
|
190
|
+
});
|
|
147
191
|
}
|
|
148
192
|
if (doc && typeof doc === "object") {
|
|
149
|
-
const docWithDefaults = DefaultsService.apply(
|
|
193
|
+
const docWithDefaults = DefaultsService.apply(
|
|
194
|
+
relatedCollection.fields,
|
|
195
|
+
doc
|
|
196
|
+
);
|
|
150
197
|
populatedDoc[field.name] = await this.populate({
|
|
151
198
|
data: docWithDefaults,
|
|
152
199
|
fields: relatedCollection.fields,
|
|
@@ -157,11 +204,19 @@ var PopulationService = class {
|
|
|
157
204
|
}
|
|
158
205
|
}
|
|
159
206
|
if (field.type === "url" && value && typeof value === "object" && value.type === "internal" && value.relationTo && value.value) {
|
|
160
|
-
const relatedCollection = this.collections.find(
|
|
207
|
+
const relatedCollection = this.collections.find(
|
|
208
|
+
(c) => c.slug === value.relationTo
|
|
209
|
+
);
|
|
161
210
|
if (relatedCollection) {
|
|
162
|
-
const doc = await this.db.findOne({
|
|
211
|
+
const doc = await this.db.findOne({
|
|
212
|
+
collection: value.relationTo,
|
|
213
|
+
id: value.value
|
|
214
|
+
});
|
|
163
215
|
if (doc && typeof doc === "object") {
|
|
164
|
-
const docWithDefaults = DefaultsService.apply(
|
|
216
|
+
const docWithDefaults = DefaultsService.apply(
|
|
217
|
+
relatedCollection.fields,
|
|
218
|
+
doc
|
|
219
|
+
);
|
|
165
220
|
const populatedDocValue = await this.populate({
|
|
166
221
|
data: docWithDefaults,
|
|
167
222
|
fields: relatedCollection.fields,
|
|
@@ -179,15 +234,27 @@ var PopulationService = class {
|
|
|
179
234
|
}
|
|
180
235
|
} else if (typeof previewUrlPattern === "string") {
|
|
181
236
|
if (previewUrlPattern.includes("{{")) {
|
|
182
|
-
resolvedUrl = previewUrlPattern.replace(
|
|
237
|
+
resolvedUrl = previewUrlPattern.replace(
|
|
238
|
+
/{{(.*?)}}/g,
|
|
239
|
+
(_, key) => String(docWithDefaults[key.trim()] || "")
|
|
240
|
+
);
|
|
183
241
|
} else if (previewUrlPattern.includes("+") || previewUrlPattern.includes("?") || previewUrlPattern.includes("==") || previewUrlPattern.includes("siteUrl")) {
|
|
184
242
|
try {
|
|
185
|
-
resolvedUrl = jexl.evalSync(
|
|
186
|
-
|
|
187
|
-
|
|
243
|
+
resolvedUrl = jexl.evalSync(
|
|
244
|
+
previewUrlPattern,
|
|
245
|
+
docWithDefaults
|
|
246
|
+
);
|
|
247
|
+
} catch (_err) {
|
|
248
|
+
resolvedUrl = previewUrlPattern.replace(
|
|
249
|
+
/:([a-zA-Z0-9_]+)/g,
|
|
250
|
+
(_, key) => String(docWithDefaults[key] || "")
|
|
251
|
+
);
|
|
188
252
|
}
|
|
189
253
|
} else {
|
|
190
|
-
resolvedUrl = previewUrlPattern.replace(
|
|
254
|
+
resolvedUrl = previewUrlPattern.replace(
|
|
255
|
+
/:([a-zA-Z0-9_]+)/g,
|
|
256
|
+
(_, key) => String(docWithDefaults[key] || "")
|
|
257
|
+
);
|
|
191
258
|
}
|
|
192
259
|
}
|
|
193
260
|
}
|
|
@@ -211,7 +278,9 @@ var PopulationService = class {
|
|
|
211
278
|
if (field.type === "blocks" && field.blocks && Array.isArray(value)) {
|
|
212
279
|
populatedDoc[field.name] = await Promise.all(
|
|
213
280
|
value.map(async (blockData) => {
|
|
214
|
-
const blockConfig = field.blocks.find(
|
|
281
|
+
const blockConfig = field.blocks.find(
|
|
282
|
+
(b) => b.slug === blockData.blockType
|
|
283
|
+
);
|
|
215
284
|
if (!blockConfig) return blockData;
|
|
216
285
|
return this.populate({
|
|
217
286
|
data: blockData,
|
|
@@ -249,7 +318,7 @@ var AuditService = class {
|
|
|
249
318
|
* Writes a single entry to the __audit collection.
|
|
250
319
|
* Called without await — runs asynchronously and never blocks the primary operation.
|
|
251
320
|
*/
|
|
252
|
-
static async log(db, args) {
|
|
321
|
+
static async log(db, args, config) {
|
|
253
322
|
try {
|
|
254
323
|
await db.create({
|
|
255
324
|
collection: "__audit",
|
|
@@ -266,7 +335,17 @@ var AuditService = class {
|
|
|
266
335
|
}
|
|
267
336
|
});
|
|
268
337
|
} catch (err) {
|
|
269
|
-
|
|
338
|
+
getObservabilityRuntime(config)?.recordAuditWriteFailure({
|
|
339
|
+
collection: args.collection,
|
|
340
|
+
operation: args.operation
|
|
341
|
+
});
|
|
342
|
+
getConfigLogger(config, "audit").error({
|
|
343
|
+
err,
|
|
344
|
+
msg: "Failed to write audit log",
|
|
345
|
+
collection: args.collection,
|
|
346
|
+
operation: args.operation,
|
|
347
|
+
documentId: args.documentId
|
|
348
|
+
});
|
|
270
349
|
}
|
|
271
350
|
}
|
|
272
351
|
};
|
|
@@ -351,7 +430,11 @@ async function evaluateAccess(expression, context) {
|
|
|
351
430
|
}
|
|
352
431
|
return !!result;
|
|
353
432
|
} catch (err) {
|
|
354
|
-
|
|
433
|
+
getConfigLogger(context.config, "access").error({
|
|
434
|
+
err,
|
|
435
|
+
msg: "Jexl evaluation failed",
|
|
436
|
+
expression
|
|
437
|
+
});
|
|
355
438
|
return false;
|
|
356
439
|
}
|
|
357
440
|
}
|
|
@@ -363,30 +446,40 @@ function isNamedAccessPolicy(value) {
|
|
|
363
446
|
async function resolveAccess(config, access, args) {
|
|
364
447
|
if (access === void 0 || access === null) return void 0;
|
|
365
448
|
if (typeof access === "boolean" || typeof access === "string") {
|
|
366
|
-
return evaluateAccess(access, args);
|
|
449
|
+
return evaluateAccess(access, { ...args, config });
|
|
367
450
|
}
|
|
368
451
|
if (typeof access === "function") {
|
|
369
452
|
try {
|
|
370
453
|
return await access(args);
|
|
371
454
|
} catch (err) {
|
|
372
|
-
|
|
455
|
+
getConfigLogger(config, "access").error({
|
|
456
|
+
err,
|
|
457
|
+
msg: "Functional access check failed"
|
|
458
|
+
});
|
|
373
459
|
return false;
|
|
374
460
|
}
|
|
375
461
|
}
|
|
376
462
|
if (isNamedAccessPolicy(access)) {
|
|
377
463
|
const policy = config.accessPolicies?.[access.policy];
|
|
378
464
|
if (policy === void 0) {
|
|
379
|
-
|
|
465
|
+
getConfigLogger(config, "access").error({
|
|
466
|
+
msg: "Unknown access policy",
|
|
467
|
+
policy: access.policy
|
|
468
|
+
});
|
|
380
469
|
return false;
|
|
381
470
|
}
|
|
382
471
|
if (typeof policy === "string" || typeof policy === "boolean") {
|
|
383
|
-
return evaluateAccess(policy, args);
|
|
472
|
+
return evaluateAccess(policy, { ...args, config });
|
|
384
473
|
}
|
|
385
474
|
try {
|
|
386
475
|
const resolver = policy;
|
|
387
476
|
return await resolver({ ...args, params: access.params });
|
|
388
477
|
} catch (err) {
|
|
389
|
-
|
|
478
|
+
getConfigLogger(config, "access").error({
|
|
479
|
+
err,
|
|
480
|
+
msg: "Access policy failed",
|
|
481
|
+
policy: access.policy
|
|
482
|
+
});
|
|
390
483
|
return false;
|
|
391
484
|
}
|
|
392
485
|
}
|
|
@@ -438,32 +531,48 @@ async function applyFieldReadAccess(context, doc) {
|
|
|
438
531
|
const result = { ...doc };
|
|
439
532
|
for (const field of context.fields) {
|
|
440
533
|
if (field.type === "row" && field.fields) {
|
|
441
|
-
Object.assign(
|
|
534
|
+
Object.assign(
|
|
535
|
+
result,
|
|
536
|
+
await applyFieldReadAccess(
|
|
537
|
+
{ ...context, fields: field.fields },
|
|
538
|
+
result
|
|
539
|
+
)
|
|
540
|
+
);
|
|
442
541
|
continue;
|
|
443
542
|
}
|
|
444
543
|
if (!field.name) continue;
|
|
445
544
|
const value = result[field.name];
|
|
446
|
-
const canRead = await resolveBooleanAccess(
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
545
|
+
const canRead = await resolveBooleanAccess(
|
|
546
|
+
context.config,
|
|
547
|
+
field.access?.read,
|
|
548
|
+
{
|
|
549
|
+
user: context.user,
|
|
550
|
+
req: context.req,
|
|
551
|
+
doc: context.doc,
|
|
552
|
+
data: context.data,
|
|
553
|
+
id: resolveFieldAccessId(context)
|
|
554
|
+
}
|
|
555
|
+
);
|
|
453
556
|
if (!canRead) {
|
|
454
557
|
delete result[field.name];
|
|
455
558
|
continue;
|
|
456
559
|
}
|
|
457
560
|
if (value === void 0 || value === null) continue;
|
|
458
561
|
if (field.type === "object" && field.fields && typeof value === "object" && !Array.isArray(value)) {
|
|
459
|
-
result[field.name] = await applyFieldReadAccess(
|
|
562
|
+
result[field.name] = await applyFieldReadAccess(
|
|
563
|
+
{ ...context, fields: field.fields },
|
|
564
|
+
value
|
|
565
|
+
);
|
|
460
566
|
continue;
|
|
461
567
|
}
|
|
462
568
|
if (field.type === "array" && field.fields && Array.isArray(value)) {
|
|
463
569
|
const childFields = field.fields;
|
|
464
570
|
result[field.name] = await Promise.all(
|
|
465
571
|
value.map(
|
|
466
|
-
(item) => typeof item === "object" && item !== null ? applyFieldReadAccess(
|
|
572
|
+
(item) => typeof item === "object" && item !== null ? applyFieldReadAccess(
|
|
573
|
+
{ ...context, fields: childFields },
|
|
574
|
+
item
|
|
575
|
+
) : item
|
|
467
576
|
)
|
|
468
577
|
);
|
|
469
578
|
continue;
|
|
@@ -473,9 +582,14 @@ async function applyFieldReadAccess(context, doc) {
|
|
|
473
582
|
value.map(async (item) => {
|
|
474
583
|
if (typeof item !== "object" || item === null) return item;
|
|
475
584
|
const typedItem = item;
|
|
476
|
-
const block = field.blocks?.find(
|
|
585
|
+
const block = field.blocks?.find(
|
|
586
|
+
(candidate) => candidate.slug === typedItem.blockType
|
|
587
|
+
);
|
|
477
588
|
if (!block || !block.fields) return item;
|
|
478
|
-
return applyFieldReadAccess(
|
|
589
|
+
return applyFieldReadAccess(
|
|
590
|
+
{ ...context, fields: block.fields },
|
|
591
|
+
typedItem
|
|
592
|
+
);
|
|
479
593
|
})
|
|
480
594
|
);
|
|
481
595
|
}
|
|
@@ -487,7 +601,13 @@ async function applyFieldWriteAccess(context, data) {
|
|
|
487
601
|
const result = { ...data };
|
|
488
602
|
for (const field of context.fields) {
|
|
489
603
|
if (field.type === "row" && field.fields) {
|
|
490
|
-
Object.assign(
|
|
604
|
+
Object.assign(
|
|
605
|
+
result,
|
|
606
|
+
await applyFieldWriteAccess(
|
|
607
|
+
{ ...context, fields: field.fields },
|
|
608
|
+
result
|
|
609
|
+
)
|
|
610
|
+
);
|
|
491
611
|
continue;
|
|
492
612
|
}
|
|
493
613
|
if (!field.name || !(field.name in result)) continue;
|
|
@@ -506,14 +626,20 @@ async function applyFieldWriteAccess(context, data) {
|
|
|
506
626
|
const value = result[field.name];
|
|
507
627
|
if (value === void 0 || value === null) continue;
|
|
508
628
|
if (field.type === "object" && field.fields && typeof value === "object" && !Array.isArray(value)) {
|
|
509
|
-
result[field.name] = await applyFieldWriteAccess(
|
|
629
|
+
result[field.name] = await applyFieldWriteAccess(
|
|
630
|
+
{ ...context, fields: field.fields },
|
|
631
|
+
value
|
|
632
|
+
);
|
|
510
633
|
continue;
|
|
511
634
|
}
|
|
512
635
|
if (field.type === "array" && field.fields && Array.isArray(value)) {
|
|
513
636
|
const childFields = field.fields;
|
|
514
637
|
result[field.name] = await Promise.all(
|
|
515
638
|
value.map(
|
|
516
|
-
(item) => typeof item === "object" && item !== null ? applyFieldWriteAccess(
|
|
639
|
+
(item) => typeof item === "object" && item !== null ? applyFieldWriteAccess(
|
|
640
|
+
{ ...context, fields: childFields },
|
|
641
|
+
item
|
|
642
|
+
) : item
|
|
517
643
|
)
|
|
518
644
|
);
|
|
519
645
|
continue;
|
|
@@ -523,9 +649,14 @@ async function applyFieldWriteAccess(context, data) {
|
|
|
523
649
|
value.map(async (item) => {
|
|
524
650
|
if (typeof item !== "object" || item === null) return item;
|
|
525
651
|
const typedItem = item;
|
|
526
|
-
const block = field.blocks?.find(
|
|
652
|
+
const block = field.blocks?.find(
|
|
653
|
+
(candidate) => candidate.slug === typedItem.blockType
|
|
654
|
+
);
|
|
527
655
|
if (!block || !block.fields) return item;
|
|
528
|
-
return applyFieldWriteAccess(
|
|
656
|
+
return applyFieldWriteAccess(
|
|
657
|
+
{ ...context, fields: block.fields },
|
|
658
|
+
typedItem
|
|
659
|
+
);
|
|
529
660
|
})
|
|
530
661
|
);
|
|
531
662
|
}
|
|
@@ -587,7 +718,13 @@ var CollectionController = class {
|
|
|
587
718
|
} catch {
|
|
588
719
|
}
|
|
589
720
|
}
|
|
590
|
-
const paginatedResult = await provider.members.list({
|
|
721
|
+
const paginatedResult = await provider.members.list({
|
|
722
|
+
limit: limit2,
|
|
723
|
+
page: page2,
|
|
724
|
+
sort: sort2,
|
|
725
|
+
where: where2,
|
|
726
|
+
req: hookReq
|
|
727
|
+
});
|
|
591
728
|
const mappedDocs = [];
|
|
592
729
|
for (const m of paginatedResult.docs) {
|
|
593
730
|
let localId = m.id;
|
|
@@ -635,12 +772,15 @@ var CollectionController = class {
|
|
|
635
772
|
}
|
|
636
773
|
}
|
|
637
774
|
}
|
|
638
|
-
const beforeReadResult = await runCollectionHooks(
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
775
|
+
const beforeReadResult = await runCollectionHooks(
|
|
776
|
+
this.collection.hooks?.beforeRead,
|
|
777
|
+
{
|
|
778
|
+
req: c.req,
|
|
779
|
+
query: where,
|
|
780
|
+
user,
|
|
781
|
+
db: readonlyDb
|
|
782
|
+
}
|
|
783
|
+
);
|
|
644
784
|
if (beforeReadResult !== void 0) {
|
|
645
785
|
where = beforeReadResult;
|
|
646
786
|
}
|
|
@@ -649,7 +789,13 @@ var CollectionController = class {
|
|
|
649
789
|
}
|
|
650
790
|
const access = await this.evaluateAccess(c, "read");
|
|
651
791
|
if (!access.allowed) {
|
|
652
|
-
return c.json(
|
|
792
|
+
return c.json(
|
|
793
|
+
{
|
|
794
|
+
error: true,
|
|
795
|
+
message: `Access denied: read on ${this.collection.slug}`
|
|
796
|
+
},
|
|
797
|
+
403
|
|
798
|
+
);
|
|
653
799
|
}
|
|
654
800
|
if (access.constraint) {
|
|
655
801
|
where = mergeWhereConstraint(where, access.constraint);
|
|
@@ -663,7 +809,10 @@ var CollectionController = class {
|
|
|
663
809
|
fields: this.collection.fields
|
|
664
810
|
});
|
|
665
811
|
if (result.total === 0 && this.collection.initialData && !where && page === 1) {
|
|
666
|
-
|
|
812
|
+
getRequestLogger(c, "collection").info({
|
|
813
|
+
msg: "Auto-seeding collection from config.initialData",
|
|
814
|
+
collection: this.collection.slug
|
|
815
|
+
});
|
|
667
816
|
for (const data of this.collection.initialData) {
|
|
668
817
|
await db.create({ collection: this.collection.slug, data });
|
|
669
818
|
}
|
|
@@ -676,31 +825,54 @@ var CollectionController = class {
|
|
|
676
825
|
fields: this.collection.fields
|
|
677
826
|
});
|
|
678
827
|
}
|
|
679
|
-
result.docs = result.docs.map(
|
|
828
|
+
result.docs = result.docs.map(
|
|
829
|
+
(doc) => this.collection.workflow ? materializeWorkflowDocument(
|
|
830
|
+
doc,
|
|
831
|
+
this.collection.workflow,
|
|
832
|
+
user
|
|
833
|
+
) : doc
|
|
834
|
+
).filter((doc) => doc !== null);
|
|
680
835
|
const processedDocs = [];
|
|
681
|
-
const readonlyDbForHooks = createReadonlyDb(db);
|
|
682
836
|
for (const doc of result.docs) {
|
|
683
|
-
const docWithDefaults = DefaultsService.apply(
|
|
684
|
-
|
|
685
|
-
doc
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
837
|
+
const docWithDefaults = DefaultsService.apply(
|
|
838
|
+
this.collection.fields,
|
|
839
|
+
doc
|
|
840
|
+
);
|
|
841
|
+
const docWithCollectionHooks = await runCollectionHooks(
|
|
842
|
+
this.collection.hooks?.afterRead,
|
|
843
|
+
{
|
|
844
|
+
doc: docWithDefaults,
|
|
845
|
+
req: c.req,
|
|
846
|
+
user,
|
|
847
|
+
db: readonlyDb
|
|
848
|
+
}
|
|
849
|
+
);
|
|
850
|
+
const docWithFieldHooks = await executeFieldAfterRead(
|
|
851
|
+
this.collection.fields,
|
|
852
|
+
docWithCollectionHooks,
|
|
694
853
|
user,
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
854
|
+
readonlyDb
|
|
855
|
+
);
|
|
856
|
+
const docWithFieldAccess = await applyFieldReadAccess(
|
|
857
|
+
{
|
|
858
|
+
config,
|
|
859
|
+
fields: this.collection.fields,
|
|
860
|
+
user,
|
|
861
|
+
req: this.toHookRequestContext(c),
|
|
862
|
+
doc: docWithFieldHooks
|
|
863
|
+
},
|
|
864
|
+
docWithFieldHooks
|
|
865
|
+
);
|
|
698
866
|
processedDocs.push(docWithFieldAccess);
|
|
699
867
|
}
|
|
700
868
|
result.docs = processedDocs;
|
|
701
869
|
if (depth > 0) {
|
|
702
870
|
const populationService = new PopulationService(db, config.collections);
|
|
703
|
-
result = await populationService.populateResult(
|
|
871
|
+
result = await populationService.populateResult(
|
|
872
|
+
result,
|
|
873
|
+
this.collection.fields,
|
|
874
|
+
depth
|
|
875
|
+
);
|
|
704
876
|
}
|
|
705
877
|
return c.json(result);
|
|
706
878
|
}
|
|
@@ -713,7 +885,10 @@ var CollectionController = class {
|
|
|
713
885
|
const hookReq = this.toHookRequestContext(c);
|
|
714
886
|
const id2 = c.req.param("id");
|
|
715
887
|
if (!id2) return c.json({ message: "Missing ID" }, 400);
|
|
716
|
-
const localDoc = await db.findOne({
|
|
888
|
+
const localDoc = await db.findOne({
|
|
889
|
+
collection: this.collection.slug,
|
|
890
|
+
id: id2
|
|
891
|
+
});
|
|
717
892
|
const externalSubject = localDoc?.externalSubject || id2;
|
|
718
893
|
let member = null;
|
|
719
894
|
if (provider.members.get) {
|
|
@@ -742,23 +917,40 @@ var CollectionController = class {
|
|
|
742
917
|
}
|
|
743
918
|
const access = await this.evaluateAccess(c, "read", { id, doc });
|
|
744
919
|
if (!access.allowed) {
|
|
745
|
-
return c.json(
|
|
920
|
+
return c.json(
|
|
921
|
+
{
|
|
922
|
+
error: true,
|
|
923
|
+
message: `Access denied: read on ${this.collection.slug}`
|
|
924
|
+
},
|
|
925
|
+
403
|
|
926
|
+
);
|
|
746
927
|
}
|
|
747
928
|
const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
|
|
748
|
-
const docWithCollectionHooks = await runCollectionHooks(
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
929
|
+
const docWithCollectionHooks = await runCollectionHooks(
|
|
930
|
+
this.collection.hooks?.afterRead,
|
|
931
|
+
{
|
|
932
|
+
doc: docWithDefaults,
|
|
933
|
+
req: c.req,
|
|
934
|
+
user,
|
|
935
|
+
db: readonlyDb
|
|
936
|
+
}
|
|
937
|
+
);
|
|
938
|
+
const docWithFieldHooks = await executeFieldAfterRead(
|
|
939
|
+
this.collection.fields,
|
|
940
|
+
docWithCollectionHooks,
|
|
758
941
|
user,
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
942
|
+
readonlyDb
|
|
943
|
+
);
|
|
944
|
+
const docWithFieldAccess = await applyFieldReadAccess(
|
|
945
|
+
{
|
|
946
|
+
config,
|
|
947
|
+
fields: this.collection.fields,
|
|
948
|
+
user,
|
|
949
|
+
req: this.toHookRequestContext(c),
|
|
950
|
+
doc: docWithFieldHooks
|
|
951
|
+
},
|
|
952
|
+
docWithFieldHooks
|
|
953
|
+
);
|
|
762
954
|
if (depth > 0 && docWithFieldAccess) {
|
|
763
955
|
const populationService = new PopulationService(db, config.collections);
|
|
764
956
|
const populatedDoc = await populationService.populate({
|
|
@@ -783,12 +975,18 @@ var CollectionController = class {
|
|
|
783
975
|
return this.upload(c);
|
|
784
976
|
}
|
|
785
977
|
const body2 = await c.req.json();
|
|
786
|
-
const member = await provider.members.create({
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
978
|
+
const member = await provider.members.create({
|
|
979
|
+
data: body2,
|
|
980
|
+
req: hookReq
|
|
981
|
+
});
|
|
982
|
+
return c.json(
|
|
983
|
+
{
|
|
984
|
+
...member,
|
|
985
|
+
id: member.id,
|
|
986
|
+
externalSubject: member.id
|
|
987
|
+
},
|
|
988
|
+
201
|
|
989
|
+
);
|
|
792
990
|
}
|
|
793
991
|
const readonlyDb = createReadonlyDb(db);
|
|
794
992
|
const contentType = c.req.header("Content-Type") || "";
|
|
@@ -810,20 +1008,35 @@ var CollectionController = class {
|
|
|
810
1008
|
}
|
|
811
1009
|
const createAccess = await this.evaluateAccess(c, "create", { data });
|
|
812
1010
|
if (!createAccess.allowed) {
|
|
813
|
-
return c.json(
|
|
1011
|
+
return c.json(
|
|
1012
|
+
{
|
|
1013
|
+
error: true,
|
|
1014
|
+
message: `Access denied: create on ${this.collection.slug}`
|
|
1015
|
+
},
|
|
1016
|
+
403
|
|
1017
|
+
);
|
|
814
1018
|
}
|
|
815
1019
|
if (this.collection.auth && data.password) {
|
|
816
1020
|
data.password = await hashPassword(data.password);
|
|
817
1021
|
}
|
|
818
|
-
data = await applyFieldWriteAccess(
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1022
|
+
data = await applyFieldWriteAccess(
|
|
1023
|
+
{
|
|
1024
|
+
config,
|
|
1025
|
+
fields: this.collection.fields,
|
|
1026
|
+
user,
|
|
1027
|
+
req: this.toHookRequestContext(c),
|
|
1028
|
+
data,
|
|
1029
|
+
operation: "create"
|
|
1030
|
+
},
|
|
1031
|
+
data
|
|
1032
|
+
);
|
|
1033
|
+
data = await executeFieldBeforeChange(
|
|
1034
|
+
this.collection.fields,
|
|
823
1035
|
data,
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1036
|
+
null,
|
|
1037
|
+
user,
|
|
1038
|
+
readonlyDb
|
|
1039
|
+
);
|
|
827
1040
|
data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
|
|
828
1041
|
data,
|
|
829
1042
|
req: c.req,
|
|
@@ -831,7 +1044,12 @@ var CollectionController = class {
|
|
|
831
1044
|
operation: "create",
|
|
832
1045
|
db: readonlyDb
|
|
833
1046
|
});
|
|
834
|
-
const doc = this.collection.workflow ? (await createWorkflowDocument({
|
|
1047
|
+
const doc = this.collection.workflow ? (await createWorkflowDocument({
|
|
1048
|
+
config,
|
|
1049
|
+
collection: this.collection,
|
|
1050
|
+
data,
|
|
1051
|
+
user
|
|
1052
|
+
})).doc : await db.create({ collection: this.collection.slug, data });
|
|
835
1053
|
if (this.collection.audit && db) {
|
|
836
1054
|
AuditService.log(db, {
|
|
837
1055
|
operation: "create",
|
|
@@ -840,15 +1058,19 @@ var CollectionController = class {
|
|
|
840
1058
|
user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
|
|
841
1059
|
before: null,
|
|
842
1060
|
after: doc
|
|
843
|
-
});
|
|
1061
|
+
}, config);
|
|
844
1062
|
}
|
|
845
|
-
await runCollectionHooks(
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1063
|
+
await runCollectionHooks(
|
|
1064
|
+
this.collection.hooks?.afterChange,
|
|
1065
|
+
{
|
|
1066
|
+
doc,
|
|
1067
|
+
user,
|
|
1068
|
+
req: c.req,
|
|
1069
|
+
operation: "create",
|
|
1070
|
+
db
|
|
1071
|
+
},
|
|
1072
|
+
{ isolated: true }
|
|
1073
|
+
);
|
|
852
1074
|
const responseDoc = this.collection.workflow ? materializeWorkflowDocument(doc, this.collection.workflow, user) : doc;
|
|
853
1075
|
const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
854
1076
|
doc: responseDoc,
|
|
@@ -856,14 +1078,22 @@ var CollectionController = class {
|
|
|
856
1078
|
user,
|
|
857
1079
|
db: readonlyDb
|
|
858
1080
|
});
|
|
859
|
-
const finalDoc = await executeFieldAfterRead(
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
fields: this.collection.fields,
|
|
1081
|
+
const finalDoc = await executeFieldAfterRead(
|
|
1082
|
+
this.collection.fields,
|
|
1083
|
+
readDoc,
|
|
863
1084
|
user,
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1085
|
+
readonlyDb
|
|
1086
|
+
);
|
|
1087
|
+
const accessibleDoc = await applyFieldReadAccess(
|
|
1088
|
+
{
|
|
1089
|
+
config,
|
|
1090
|
+
fields: this.collection.fields,
|
|
1091
|
+
user,
|
|
1092
|
+
req: this.toHookRequestContext(c),
|
|
1093
|
+
doc: finalDoc
|
|
1094
|
+
},
|
|
1095
|
+
finalDoc
|
|
1096
|
+
);
|
|
867
1097
|
return c.json(accessibleDoc, 201);
|
|
868
1098
|
}
|
|
869
1099
|
async upload(c) {
|
|
@@ -879,7 +1109,10 @@ var CollectionController = class {
|
|
|
879
1109
|
const uploadConfig = typeof this.collection.upload === "object" ? this.collection.upload : void 0;
|
|
880
1110
|
const validationError = validateUpload(file, uploadConfig);
|
|
881
1111
|
if (validationError) {
|
|
882
|
-
return c.json(
|
|
1112
|
+
return c.json(
|
|
1113
|
+
{ message: validationError.message },
|
|
1114
|
+
validationError.status
|
|
1115
|
+
);
|
|
883
1116
|
}
|
|
884
1117
|
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
885
1118
|
const siteId = c.get("siteId");
|
|
@@ -909,17 +1142,32 @@ var CollectionController = class {
|
|
|
909
1142
|
};
|
|
910
1143
|
const createAccess = await this.evaluateAccess(c, "create", { data });
|
|
911
1144
|
if (!createAccess.allowed) {
|
|
912
|
-
return c.json(
|
|
1145
|
+
return c.json(
|
|
1146
|
+
{
|
|
1147
|
+
error: true,
|
|
1148
|
+
message: `Access denied: create on ${this.collection.slug}`
|
|
1149
|
+
},
|
|
1150
|
+
403
|
|
1151
|
+
);
|
|
913
1152
|
}
|
|
914
|
-
data = await applyFieldWriteAccess(
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1153
|
+
data = await applyFieldWriteAccess(
|
|
1154
|
+
{
|
|
1155
|
+
config,
|
|
1156
|
+
fields: this.collection.fields,
|
|
1157
|
+
user,
|
|
1158
|
+
req: this.toHookRequestContext(c),
|
|
1159
|
+
data,
|
|
1160
|
+
operation: "create"
|
|
1161
|
+
},
|
|
1162
|
+
data
|
|
1163
|
+
);
|
|
1164
|
+
data = await executeFieldBeforeChange(
|
|
1165
|
+
this.collection.fields,
|
|
919
1166
|
data,
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1167
|
+
null,
|
|
1168
|
+
user,
|
|
1169
|
+
readonlyDb
|
|
1170
|
+
);
|
|
923
1171
|
data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
|
|
924
1172
|
data,
|
|
925
1173
|
req: c.req,
|
|
@@ -931,13 +1179,17 @@ var CollectionController = class {
|
|
|
931
1179
|
collection: this.collection.slug,
|
|
932
1180
|
data
|
|
933
1181
|
});
|
|
934
|
-
await runCollectionHooks(
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1182
|
+
await runCollectionHooks(
|
|
1183
|
+
this.collection.hooks?.afterChange,
|
|
1184
|
+
{
|
|
1185
|
+
doc,
|
|
1186
|
+
user,
|
|
1187
|
+
req: c.req,
|
|
1188
|
+
operation: "create",
|
|
1189
|
+
db
|
|
1190
|
+
},
|
|
1191
|
+
{ isolated: true }
|
|
1192
|
+
);
|
|
941
1193
|
const responseDoc = this.collection.workflow ? materializeWorkflowDocument(doc, this.collection.workflow, user) : doc;
|
|
942
1194
|
const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
943
1195
|
doc: responseDoc,
|
|
@@ -945,14 +1197,22 @@ var CollectionController = class {
|
|
|
945
1197
|
user,
|
|
946
1198
|
db: readonlyDb
|
|
947
1199
|
});
|
|
948
|
-
const finalDoc = await executeFieldAfterRead(
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
fields: this.collection.fields,
|
|
1200
|
+
const finalDoc = await executeFieldAfterRead(
|
|
1201
|
+
this.collection.fields,
|
|
1202
|
+
readDoc,
|
|
952
1203
|
user,
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1204
|
+
readonlyDb
|
|
1205
|
+
);
|
|
1206
|
+
const accessibleDoc = await applyFieldReadAccess(
|
|
1207
|
+
{
|
|
1208
|
+
config,
|
|
1209
|
+
fields: this.collection.fields,
|
|
1210
|
+
user,
|
|
1211
|
+
req: this.toHookRequestContext(c),
|
|
1212
|
+
doc: finalDoc
|
|
1213
|
+
},
|
|
1214
|
+
finalDoc
|
|
1215
|
+
);
|
|
956
1216
|
return c.json(accessibleDoc, 201);
|
|
957
1217
|
}
|
|
958
1218
|
async update(c) {
|
|
@@ -965,9 +1225,16 @@ var CollectionController = class {
|
|
|
965
1225
|
const id2 = c.req.param("id");
|
|
966
1226
|
if (!id2) return c.json({ message: "Missing ID" }, 400);
|
|
967
1227
|
const body2 = await c.req.json();
|
|
968
|
-
const localDoc = await db.findOne({
|
|
1228
|
+
const localDoc = await db.findOne({
|
|
1229
|
+
collection: this.collection.slug,
|
|
1230
|
+
id: id2
|
|
1231
|
+
});
|
|
969
1232
|
const externalSubject = localDoc?.externalSubject || id2;
|
|
970
|
-
const member = await provider.members.update({
|
|
1233
|
+
const member = await provider.members.update({
|
|
1234
|
+
externalSubject,
|
|
1235
|
+
data: body2,
|
|
1236
|
+
req: hookReq
|
|
1237
|
+
});
|
|
971
1238
|
return c.json({
|
|
972
1239
|
...member,
|
|
973
1240
|
id: localDoc ? localDoc.id : member.id,
|
|
@@ -989,25 +1256,47 @@ var CollectionController = class {
|
|
|
989
1256
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
990
1257
|
updatedBy: user?.sub ?? null
|
|
991
1258
|
});
|
|
992
|
-
const originalDoc = await db.findOne({
|
|
1259
|
+
const originalDoc = await db.findOne({
|
|
1260
|
+
collection: this.collection.slug,
|
|
1261
|
+
id
|
|
1262
|
+
});
|
|
993
1263
|
if (!originalDoc) return c.json({ message: "Not Found" }, 404);
|
|
994
|
-
const updateAccess = await this.evaluateAccess(c, "update", {
|
|
1264
|
+
const updateAccess = await this.evaluateAccess(c, "update", {
|
|
1265
|
+
id,
|
|
1266
|
+
doc: originalDoc,
|
|
1267
|
+
data
|
|
1268
|
+
});
|
|
995
1269
|
if (!updateAccess.allowed) {
|
|
996
|
-
return c.json(
|
|
1270
|
+
return c.json(
|
|
1271
|
+
{
|
|
1272
|
+
error: true,
|
|
1273
|
+
message: `Access denied: update on ${this.collection.slug}`
|
|
1274
|
+
},
|
|
1275
|
+
403
|
|
1276
|
+
);
|
|
997
1277
|
}
|
|
998
1278
|
let before = null;
|
|
999
1279
|
if (this.collection.audit) {
|
|
1000
1280
|
before = originalDoc;
|
|
1001
1281
|
}
|
|
1002
|
-
data = await applyFieldWriteAccess(
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1282
|
+
data = await applyFieldWriteAccess(
|
|
1283
|
+
{
|
|
1284
|
+
config,
|
|
1285
|
+
fields: this.collection.fields,
|
|
1286
|
+
user,
|
|
1287
|
+
req: this.toHookRequestContext(c),
|
|
1288
|
+
doc: originalDoc,
|
|
1289
|
+
data
|
|
1290
|
+
},
|
|
1008
1291
|
data
|
|
1009
|
-
|
|
1010
|
-
data = await executeFieldBeforeChange(
|
|
1292
|
+
);
|
|
1293
|
+
data = await executeFieldBeforeChange(
|
|
1294
|
+
this.collection.fields,
|
|
1295
|
+
data,
|
|
1296
|
+
originalDoc,
|
|
1297
|
+
user,
|
|
1298
|
+
readonlyDb
|
|
1299
|
+
);
|
|
1011
1300
|
data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
|
|
1012
1301
|
data,
|
|
1013
1302
|
doc: originalDoc,
|
|
@@ -1016,7 +1305,14 @@ var CollectionController = class {
|
|
|
1016
1305
|
operation: "update",
|
|
1017
1306
|
db: readonlyDb
|
|
1018
1307
|
});
|
|
1019
|
-
const doc = this.collection.workflow ? (await saveWorkflowDraft({
|
|
1308
|
+
const doc = this.collection.workflow ? (await saveWorkflowDraft({
|
|
1309
|
+
config,
|
|
1310
|
+
collection: this.collection,
|
|
1311
|
+
id,
|
|
1312
|
+
originalDoc,
|
|
1313
|
+
data,
|
|
1314
|
+
user
|
|
1315
|
+
})).doc : await db.update({ collection: this.collection.slug, id, data });
|
|
1020
1316
|
if (this.collection.audit && db) {
|
|
1021
1317
|
AuditService.log(db, {
|
|
1022
1318
|
operation: "update",
|
|
@@ -1025,36 +1321,52 @@ var CollectionController = class {
|
|
|
1025
1321
|
user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
|
|
1026
1322
|
before,
|
|
1027
1323
|
after: doc
|
|
1028
|
-
});
|
|
1324
|
+
}, config);
|
|
1029
1325
|
}
|
|
1030
|
-
await runCollectionHooks(
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1326
|
+
await runCollectionHooks(
|
|
1327
|
+
this.collection.hooks?.afterChange,
|
|
1328
|
+
{
|
|
1329
|
+
doc,
|
|
1330
|
+
previousDoc: originalDoc,
|
|
1331
|
+
user,
|
|
1332
|
+
req: c.req,
|
|
1333
|
+
operation: "update",
|
|
1334
|
+
db
|
|
1335
|
+
},
|
|
1336
|
+
{ isolated: true }
|
|
1337
|
+
);
|
|
1038
1338
|
const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
|
|
1039
1339
|
doc,
|
|
1040
1340
|
req: c.req,
|
|
1041
1341
|
user,
|
|
1042
1342
|
db: readonlyDb
|
|
1043
1343
|
});
|
|
1044
|
-
const finalDoc = await executeFieldAfterRead(
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
fields: this.collection.fields,
|
|
1344
|
+
const finalDoc = await executeFieldAfterRead(
|
|
1345
|
+
this.collection.fields,
|
|
1346
|
+
readDoc,
|
|
1048
1347
|
user,
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1348
|
+
readonlyDb
|
|
1349
|
+
);
|
|
1350
|
+
const accessibleDoc = await applyFieldReadAccess(
|
|
1351
|
+
{
|
|
1352
|
+
config,
|
|
1353
|
+
fields: this.collection.fields,
|
|
1354
|
+
user,
|
|
1355
|
+
req: this.toHookRequestContext(c),
|
|
1356
|
+
doc: finalDoc
|
|
1357
|
+
},
|
|
1358
|
+
finalDoc
|
|
1359
|
+
);
|
|
1052
1360
|
return c.json(accessibleDoc);
|
|
1053
1361
|
}
|
|
1054
1362
|
async transition(c) {
|
|
1055
1363
|
const config = c.get("config");
|
|
1056
1364
|
if (!config.db) return c.json({ message: "Database not configured" }, 500);
|
|
1057
|
-
if (!this.collection.workflow)
|
|
1365
|
+
if (!this.collection.workflow)
|
|
1366
|
+
return c.json(
|
|
1367
|
+
{ message: "Workflows are not enabled for this collection" },
|
|
1368
|
+
404
|
|
1369
|
+
);
|
|
1058
1370
|
const id = c.req.param("id");
|
|
1059
1371
|
const transitionName = c.req.param("transition");
|
|
1060
1372
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -1069,19 +1381,38 @@ var CollectionController = class {
|
|
|
1069
1381
|
user: c.get("user"),
|
|
1070
1382
|
req: { query: c.req.query(), headers: c.req.header(), raw: c.req.raw }
|
|
1071
1383
|
});
|
|
1072
|
-
return c.json(
|
|
1384
|
+
return c.json(
|
|
1385
|
+
materializeWorkflowDocument(
|
|
1386
|
+
doc,
|
|
1387
|
+
this.collection.workflow,
|
|
1388
|
+
c.get("user")
|
|
1389
|
+
)
|
|
1390
|
+
);
|
|
1073
1391
|
} catch (error) {
|
|
1074
1392
|
const status = typeof error.statusCode === "number" ? error.statusCode : 500;
|
|
1075
|
-
return c.json(
|
|
1393
|
+
return c.json(
|
|
1394
|
+
{
|
|
1395
|
+
error: true,
|
|
1396
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1397
|
+
},
|
|
1398
|
+
status
|
|
1399
|
+
);
|
|
1076
1400
|
}
|
|
1077
1401
|
}
|
|
1078
1402
|
async workflowHistory(c) {
|
|
1079
1403
|
const config = c.get("config");
|
|
1080
1404
|
if (!config.db) return c.json({ message: "Database not configured" }, 500);
|
|
1081
|
-
if (!this.collection.workflow)
|
|
1405
|
+
if (!this.collection.workflow)
|
|
1406
|
+
return c.json(
|
|
1407
|
+
{ message: "Workflows are not enabled for this collection" },
|
|
1408
|
+
404
|
|
1409
|
+
);
|
|
1082
1410
|
const documentId = c.req.param("id");
|
|
1083
1411
|
if (!documentId) return c.json({ message: "Missing ID" }, 400);
|
|
1084
|
-
const document = await config.db.findOne({
|
|
1412
|
+
const document = await config.db.findOne({
|
|
1413
|
+
collection: this.collection.slug,
|
|
1414
|
+
id: documentId
|
|
1415
|
+
});
|
|
1085
1416
|
if (!document) return c.json({ message: "Not Found" }, 404);
|
|
1086
1417
|
const readAccess = this.collection.access?.read;
|
|
1087
1418
|
if (readAccess !== void 0 && readAccess !== null) {
|
|
@@ -1096,11 +1427,21 @@ var CollectionController = class {
|
|
|
1096
1427
|
});
|
|
1097
1428
|
allowed = match.total > 0;
|
|
1098
1429
|
}
|
|
1099
|
-
if (!allowed)
|
|
1430
|
+
if (!allowed)
|
|
1431
|
+
return c.json(
|
|
1432
|
+
{
|
|
1433
|
+
error: true,
|
|
1434
|
+
message: `Access denied: read on ${this.collection.slug}`
|
|
1435
|
+
},
|
|
1436
|
+
403
|
|
1437
|
+
);
|
|
1100
1438
|
}
|
|
1101
1439
|
const result = await config.db.find({
|
|
1102
1440
|
collection: WORKFLOW_HISTORY_COLLECTION,
|
|
1103
|
-
where: {
|
|
1441
|
+
where: {
|
|
1442
|
+
collection: { equals: this.collection.slug },
|
|
1443
|
+
documentId: { equals: documentId }
|
|
1444
|
+
},
|
|
1104
1445
|
sort: "-createdAt",
|
|
1105
1446
|
limit: Math.min(Number(c.req.query("limit")) || 50, 100)
|
|
1106
1447
|
});
|
|
@@ -1122,7 +1463,10 @@ var CollectionController = class {
|
|
|
1122
1463
|
const db = config.db;
|
|
1123
1464
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
1124
1465
|
if (!this.collection.auth) {
|
|
1125
|
-
return c.json(
|
|
1466
|
+
return c.json(
|
|
1467
|
+
{ message: "This collection does not support authentication" },
|
|
1468
|
+
400
|
|
1469
|
+
);
|
|
1126
1470
|
}
|
|
1127
1471
|
const id = c.req.param("id");
|
|
1128
1472
|
if (!id) return c.json({ message: "Missing ID" }, 400);
|
|
@@ -1142,29 +1486,44 @@ var CollectionController = class {
|
|
|
1142
1486
|
const isAdmin = Array.isArray(user.roles) && user.roles.includes("admin");
|
|
1143
1487
|
const isSelf = user.sub === id;
|
|
1144
1488
|
if (!isAdmin && !isSelf) {
|
|
1145
|
-
return c.json(
|
|
1489
|
+
return c.json(
|
|
1490
|
+
{ message: "You are not authorised to change this password" },
|
|
1491
|
+
403
|
|
1492
|
+
);
|
|
1146
1493
|
}
|
|
1147
1494
|
if (!isAdmin) {
|
|
1148
1495
|
if (!oldPassword) {
|
|
1149
1496
|
return c.json({ message: "Current password is required" }, 400);
|
|
1150
1497
|
}
|
|
1151
|
-
const existing = await db.findOne({
|
|
1498
|
+
const existing = await db.findOne({
|
|
1499
|
+
collection: this.collection.slug,
|
|
1500
|
+
id
|
|
1501
|
+
});
|
|
1152
1502
|
if (!existing) return c.json({ message: "User not found" }, 404);
|
|
1153
|
-
const valid = await verifyPassword(
|
|
1503
|
+
const valid = await verifyPassword(
|
|
1504
|
+
oldPassword,
|
|
1505
|
+
existing.password
|
|
1506
|
+
);
|
|
1154
1507
|
if (!valid) {
|
|
1155
1508
|
return c.json({ message: "Invalid current password" }, 400);
|
|
1156
1509
|
}
|
|
1157
1510
|
}
|
|
1158
1511
|
const hashed = await hashPassword(newPassword);
|
|
1159
|
-
|
|
1512
|
+
await db.update({
|
|
1160
1513
|
collection: this.collection.slug,
|
|
1161
1514
|
id,
|
|
1162
1515
|
data: {
|
|
1163
1516
|
password: hashed,
|
|
1517
|
+
loginAttempts: 0,
|
|
1518
|
+
lockedUntil: null,
|
|
1164
1519
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1165
1520
|
updatedBy: user.sub
|
|
1166
1521
|
}
|
|
1167
1522
|
});
|
|
1523
|
+
await revokeAllAuthSessions(config, {
|
|
1524
|
+
userId: id,
|
|
1525
|
+
collection: this.collection.slug
|
|
1526
|
+
});
|
|
1168
1527
|
if (this.collection.audit) {
|
|
1169
1528
|
AuditService.log(db, {
|
|
1170
1529
|
operation: "update",
|
|
@@ -1173,7 +1532,7 @@ var CollectionController = class {
|
|
|
1173
1532
|
user: { id: user.sub, collection: user.collection, email: user.email },
|
|
1174
1533
|
before: null,
|
|
1175
1534
|
after: { id }
|
|
1176
|
-
});
|
|
1535
|
+
}, config);
|
|
1177
1536
|
}
|
|
1178
1537
|
return c.json({ success: true, message: "Password updated successfully" });
|
|
1179
1538
|
}
|
|
@@ -1186,7 +1545,10 @@ var CollectionController = class {
|
|
|
1186
1545
|
const hookReq = this.toHookRequestContext(c);
|
|
1187
1546
|
const id2 = c.req.param("id");
|
|
1188
1547
|
if (!id2) return c.json({ message: "Missing ID" }, 400);
|
|
1189
|
-
const localDoc = await db.findOne({
|
|
1548
|
+
const localDoc = await db.findOne({
|
|
1549
|
+
collection: this.collection.slug,
|
|
1550
|
+
id: id2
|
|
1551
|
+
});
|
|
1190
1552
|
const externalSubject = localDoc?.externalSubject || id2;
|
|
1191
1553
|
await provider.members.delete({ externalSubject, req: hookReq });
|
|
1192
1554
|
return c.json({ message: "Deleted" });
|
|
@@ -1199,7 +1561,13 @@ var CollectionController = class {
|
|
|
1199
1561
|
if (!doc) return c.json({ message: "Not Found" }, 404);
|
|
1200
1562
|
const deleteAccess = await this.evaluateAccess(c, "delete", { id, doc });
|
|
1201
1563
|
if (!deleteAccess.allowed) {
|
|
1202
|
-
return c.json(
|
|
1564
|
+
return c.json(
|
|
1565
|
+
{
|
|
1566
|
+
error: true,
|
|
1567
|
+
message: `Access denied: delete on ${this.collection.slug}`
|
|
1568
|
+
},
|
|
1569
|
+
403
|
|
1570
|
+
);
|
|
1203
1571
|
}
|
|
1204
1572
|
let before = null;
|
|
1205
1573
|
if (this.collection.audit) {
|
|
@@ -1221,15 +1589,19 @@ var CollectionController = class {
|
|
|
1221
1589
|
user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
|
|
1222
1590
|
before,
|
|
1223
1591
|
after: null
|
|
1224
|
-
});
|
|
1592
|
+
}, config);
|
|
1225
1593
|
}
|
|
1226
|
-
await runCollectionHooks(
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1594
|
+
await runCollectionHooks(
|
|
1595
|
+
this.collection.hooks?.afterDelete,
|
|
1596
|
+
{
|
|
1597
|
+
id,
|
|
1598
|
+
doc,
|
|
1599
|
+
user,
|
|
1600
|
+
req: c.req,
|
|
1601
|
+
db
|
|
1602
|
+
},
|
|
1603
|
+
{ isolated: true }
|
|
1604
|
+
);
|
|
1233
1605
|
return c.json({ message: "Deleted" });
|
|
1234
1606
|
}
|
|
1235
1607
|
async deleteMany(c) {
|
|
@@ -1260,7 +1632,10 @@ var CollectionController = class {
|
|
|
1260
1632
|
failed.push({ id, error: "Not Found" });
|
|
1261
1633
|
continue;
|
|
1262
1634
|
}
|
|
1263
|
-
const deleteAccess = await this.evaluateAccess(c, "delete", {
|
|
1635
|
+
const deleteAccess = await this.evaluateAccess(c, "delete", {
|
|
1636
|
+
id,
|
|
1637
|
+
doc
|
|
1638
|
+
});
|
|
1264
1639
|
if (!deleteAccess.allowed) {
|
|
1265
1640
|
failed.push({ id, error: "Access denied" });
|
|
1266
1641
|
continue;
|
|
@@ -1286,15 +1661,19 @@ var CollectionController = class {
|
|
|
1286
1661
|
user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
|
|
1287
1662
|
before,
|
|
1288
1663
|
after: null
|
|
1289
|
-
});
|
|
1664
|
+
}, config);
|
|
1290
1665
|
}
|
|
1291
|
-
await runCollectionHooks(
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1666
|
+
await runCollectionHooks(
|
|
1667
|
+
this.collection.hooks?.afterDelete,
|
|
1668
|
+
{
|
|
1669
|
+
id,
|
|
1670
|
+
doc,
|
|
1671
|
+
user,
|
|
1672
|
+
req: c.req,
|
|
1673
|
+
db
|
|
1674
|
+
},
|
|
1675
|
+
{ isolated: true }
|
|
1676
|
+
);
|
|
1298
1677
|
} catch (err) {
|
|
1299
1678
|
failed.push({ id, error: err?.message ?? "Unknown error" });
|
|
1300
1679
|
}
|
|
@@ -1314,17 +1693,26 @@ var CollectionController = class {
|
|
|
1314
1693
|
if (!initialData || !Array.isArray(initialData)) {
|
|
1315
1694
|
return c.json({ message: "Invalid initial data" }, 400);
|
|
1316
1695
|
}
|
|
1317
|
-
const result = await db.find({
|
|
1696
|
+
const result = await db.find({
|
|
1697
|
+
collection: this.collection.slug,
|
|
1698
|
+
limit: 1
|
|
1699
|
+
});
|
|
1318
1700
|
if (result.total > 0) {
|
|
1319
1701
|
return c.json({ message: "Collection is not empty, skipping seed" });
|
|
1320
1702
|
}
|
|
1321
|
-
|
|
1703
|
+
getRequestLogger(c, "collection").info({
|
|
1704
|
+
msg: "Auto-seeding collection",
|
|
1705
|
+
collection: this.collection.slug
|
|
1706
|
+
});
|
|
1322
1707
|
const createdDocs = [];
|
|
1323
1708
|
for (const data of initialData) {
|
|
1324
1709
|
const doc = await db.create({ collection: this.collection.slug, data });
|
|
1325
1710
|
createdDocs.push(doc);
|
|
1326
1711
|
}
|
|
1327
|
-
return c.json(
|
|
1712
|
+
return c.json(
|
|
1713
|
+
{ message: "Seed successful", count: createdDocs.length },
|
|
1714
|
+
201
|
|
1715
|
+
);
|
|
1328
1716
|
}
|
|
1329
1717
|
};
|
|
1330
1718
|
|
|
@@ -1341,46 +1729,66 @@ var GlobalController = class {
|
|
|
1341
1729
|
const readonlyDb = createReadonlyDb(db);
|
|
1342
1730
|
const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
|
|
1343
1731
|
const user = c.get("user");
|
|
1344
|
-
|
|
1345
|
-
const beforeReadResult = await runCollectionHooks(this.global.hooks?.beforeRead, {
|
|
1732
|
+
await runCollectionHooks(this.global.hooks?.beforeRead, {
|
|
1346
1733
|
req: c.req,
|
|
1347
|
-
query,
|
|
1734
|
+
query: void 0,
|
|
1348
1735
|
user,
|
|
1349
1736
|
db: readonlyDb
|
|
1350
1737
|
});
|
|
1351
|
-
if (beforeReadResult !== void 0) {
|
|
1352
|
-
query = beforeReadResult;
|
|
1353
|
-
}
|
|
1354
1738
|
let data = await db.getGlobal({ slug: this.global.slug });
|
|
1355
1739
|
const isEmpty = !data || isFunctionallyEmpty(data);
|
|
1356
1740
|
if (isEmpty && this.global.initialData) {
|
|
1357
|
-
|
|
1358
|
-
|
|
1741
|
+
getRequestLogger(c, "global").info({
|
|
1742
|
+
msg: "Auto-seeding global from config.initialData",
|
|
1743
|
+
global: this.global.slug
|
|
1744
|
+
});
|
|
1745
|
+
await db.updateGlobal({
|
|
1746
|
+
slug: this.global.slug,
|
|
1747
|
+
data: this.global.initialData
|
|
1748
|
+
});
|
|
1359
1749
|
data = this.global.initialData;
|
|
1360
1750
|
}
|
|
1361
|
-
const canRead = await resolveBooleanAccess(
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1751
|
+
const canRead = await resolveBooleanAccess(
|
|
1752
|
+
config,
|
|
1753
|
+
this.global.access?.read,
|
|
1754
|
+
{
|
|
1755
|
+
user,
|
|
1756
|
+
req: toHookRequestContext(c.req),
|
|
1757
|
+
doc: data
|
|
1758
|
+
}
|
|
1759
|
+
);
|
|
1366
1760
|
if (!canRead) {
|
|
1367
|
-
return c.json(
|
|
1761
|
+
return c.json(
|
|
1762
|
+
{ error: true, message: `Access denied: read on ${this.global.slug}` },
|
|
1763
|
+
403
|
|
1764
|
+
);
|
|
1368
1765
|
}
|
|
1369
1766
|
const dataWithDefaults = DefaultsService.apply(this.global.fields, data);
|
|
1370
|
-
const docWithCollectionHooks = await runCollectionHooks(
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1767
|
+
const docWithCollectionHooks = await runCollectionHooks(
|
|
1768
|
+
this.global.hooks?.afterRead,
|
|
1769
|
+
{
|
|
1770
|
+
doc: dataWithDefaults,
|
|
1771
|
+
req: c.req,
|
|
1772
|
+
user,
|
|
1773
|
+
db: readonlyDb
|
|
1774
|
+
}
|
|
1775
|
+
);
|
|
1776
|
+
const docWithFieldHooks = await executeFieldAfterRead(
|
|
1777
|
+
this.global.fields,
|
|
1778
|
+
docWithCollectionHooks,
|
|
1380
1779
|
user,
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1780
|
+
readonlyDb
|
|
1781
|
+
);
|
|
1782
|
+
const docWithFieldAccess = await applyFieldReadAccess(
|
|
1783
|
+
{
|
|
1784
|
+
config,
|
|
1785
|
+
fields: this.global.fields,
|
|
1786
|
+
user,
|
|
1787
|
+
req: toHookRequestContext(c.req),
|
|
1788
|
+
doc: docWithFieldHooks
|
|
1789
|
+
},
|
|
1790
|
+
docWithFieldHooks
|
|
1791
|
+
);
|
|
1384
1792
|
if (depth > 0 && docWithFieldAccess) {
|
|
1385
1793
|
const populationService = new PopulationService(db, config.collections);
|
|
1386
1794
|
const populatedData = await populationService.populate({
|
|
@@ -1401,24 +1809,43 @@ var GlobalController = class {
|
|
|
1401
1809
|
const body = await c.req.json();
|
|
1402
1810
|
const user = c.get("user");
|
|
1403
1811
|
const originalDoc = await db.getGlobal({ slug: this.global.slug }) || {};
|
|
1404
|
-
const canUpdate = await resolveBooleanAccess(
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1812
|
+
const canUpdate = await resolveBooleanAccess(
|
|
1813
|
+
config,
|
|
1814
|
+
this.global.access?.update,
|
|
1815
|
+
{
|
|
1816
|
+
user,
|
|
1817
|
+
req: toHookRequestContext(c.req),
|
|
1818
|
+
doc: originalDoc,
|
|
1819
|
+
data: body
|
|
1820
|
+
}
|
|
1821
|
+
);
|
|
1410
1822
|
if (!canUpdate) {
|
|
1411
|
-
return c.json(
|
|
1823
|
+
return c.json(
|
|
1824
|
+
{
|
|
1825
|
+
error: true,
|
|
1826
|
+
message: `Access denied: update on ${this.global.slug}`
|
|
1827
|
+
},
|
|
1828
|
+
403
|
|
1829
|
+
);
|
|
1412
1830
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1831
|
+
const sanitizedBody = await applyFieldWriteAccess(
|
|
1832
|
+
{
|
|
1833
|
+
config,
|
|
1834
|
+
fields: this.global.fields,
|
|
1835
|
+
user,
|
|
1836
|
+
req: toHookRequestContext(c.req),
|
|
1837
|
+
doc: originalDoc,
|
|
1838
|
+
data: body
|
|
1839
|
+
},
|
|
1840
|
+
body
|
|
1841
|
+
);
|
|
1842
|
+
let data = await executeFieldBeforeChange(
|
|
1843
|
+
this.global.fields,
|
|
1844
|
+
sanitizedBody,
|
|
1845
|
+
originalDoc,
|
|
1416
1846
|
user,
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
data: body
|
|
1420
|
-
}, body);
|
|
1421
|
-
let data = await executeFieldBeforeChange(this.global.fields, sanitizedBody, originalDoc, user, readonlyDb);
|
|
1847
|
+
readonlyDb
|
|
1848
|
+
);
|
|
1422
1849
|
data = await runCollectionHooks(this.global.hooks?.beforeChange, {
|
|
1423
1850
|
data,
|
|
1424
1851
|
doc: originalDoc,
|
|
@@ -1428,28 +1855,40 @@ var GlobalController = class {
|
|
|
1428
1855
|
db: readonlyDb
|
|
1429
1856
|
});
|
|
1430
1857
|
const updated = await db.updateGlobal({ slug: this.global.slug, data });
|
|
1431
|
-
await runCollectionHooks(
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1858
|
+
await runCollectionHooks(
|
|
1859
|
+
this.global.hooks?.afterChange,
|
|
1860
|
+
{
|
|
1861
|
+
doc: updated,
|
|
1862
|
+
previousDoc: originalDoc,
|
|
1863
|
+
user,
|
|
1864
|
+
req: c.req,
|
|
1865
|
+
operation: "update",
|
|
1866
|
+
db
|
|
1867
|
+
},
|
|
1868
|
+
{ isolated: true }
|
|
1869
|
+
);
|
|
1439
1870
|
const readDoc = await runCollectionHooks(this.global.hooks?.afterRead, {
|
|
1440
1871
|
doc: updated,
|
|
1441
1872
|
req: c.req,
|
|
1442
1873
|
user,
|
|
1443
1874
|
db: readonlyDb
|
|
1444
1875
|
});
|
|
1445
|
-
const finalDoc = await executeFieldAfterRead(
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
fields: this.global.fields,
|
|
1876
|
+
const finalDoc = await executeFieldAfterRead(
|
|
1877
|
+
this.global.fields,
|
|
1878
|
+
readDoc,
|
|
1449
1879
|
user,
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1880
|
+
readonlyDb
|
|
1881
|
+
);
|
|
1882
|
+
const accessibleDoc = await applyFieldReadAccess(
|
|
1883
|
+
{
|
|
1884
|
+
config,
|
|
1885
|
+
fields: this.global.fields,
|
|
1886
|
+
user,
|
|
1887
|
+
req: toHookRequestContext(c.req),
|
|
1888
|
+
doc: finalDoc
|
|
1889
|
+
},
|
|
1890
|
+
finalDoc
|
|
1891
|
+
);
|
|
1453
1892
|
return c.json(accessibleDoc);
|
|
1454
1893
|
}
|
|
1455
1894
|
async seed(c) {
|
|
@@ -1465,7 +1904,10 @@ var GlobalController = class {
|
|
|
1465
1904
|
if (existing && !isFunctionallyEmpty(existing)) {
|
|
1466
1905
|
return c.json({ message: "Global is not empty, skipping seed" });
|
|
1467
1906
|
}
|
|
1468
|
-
|
|
1907
|
+
getRequestLogger(c, "global").info({
|
|
1908
|
+
msg: "Auto-seeding global",
|
|
1909
|
+
global: this.global.slug
|
|
1910
|
+
});
|
|
1469
1911
|
await db.updateGlobal({ slug: this.global.slug, data: initialData });
|
|
1470
1912
|
return c.json({ message: "Seed successful", data: initialData }, 201);
|
|
1471
1913
|
}
|
|
@@ -1487,6 +1929,7 @@ function isFunctionallyEmpty(obj) {
|
|
|
1487
1929
|
// src/controllers/media.controller.ts
|
|
1488
1930
|
var MediaController = class {
|
|
1489
1931
|
collection;
|
|
1932
|
+
loggerComponent = "media";
|
|
1490
1933
|
constructor(collection = "media") {
|
|
1491
1934
|
this.collection = collection;
|
|
1492
1935
|
}
|
|
@@ -1505,15 +1948,25 @@ var MediaController = class {
|
|
|
1505
1948
|
return c.json({ message: "No file uploaded" }, 400);
|
|
1506
1949
|
}
|
|
1507
1950
|
const siteId = c.get("siteId");
|
|
1508
|
-
let colConfig = config.collections.find(
|
|
1951
|
+
let colConfig = config.collections.find(
|
|
1952
|
+
(col) => col.slug === this.collection
|
|
1953
|
+
);
|
|
1509
1954
|
if (!colConfig && config.onSchemaFetch && siteId) {
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1955
|
+
const requestConfig = mergeDynamicConfig(
|
|
1956
|
+
config,
|
|
1957
|
+
await config.onSchemaFetch(siteId)
|
|
1958
|
+
);
|
|
1959
|
+
colConfig = requestConfig.collections.find(
|
|
1960
|
+
(col) => col.slug === this.collection
|
|
1961
|
+
);
|
|
1512
1962
|
}
|
|
1513
1963
|
const uploadConfig = typeof colConfig?.upload === "object" ? colConfig.upload : void 0;
|
|
1514
1964
|
const validationError = validateUpload(file, uploadConfig);
|
|
1515
1965
|
if (validationError) {
|
|
1516
|
-
return c.json(
|
|
1966
|
+
return c.json(
|
|
1967
|
+
{ message: validationError.message },
|
|
1968
|
+
validationError.status
|
|
1969
|
+
);
|
|
1517
1970
|
}
|
|
1518
1971
|
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
1519
1972
|
const workspaceId = c.get("workspaceId");
|
|
@@ -1531,7 +1984,12 @@ var MediaController = class {
|
|
|
1531
1984
|
imageMetadata = processed.metadata;
|
|
1532
1985
|
imageSizes = processed.sizes;
|
|
1533
1986
|
} catch (err) {
|
|
1534
|
-
|
|
1987
|
+
getRequestLogger(c, this.loggerComponent).error({
|
|
1988
|
+
err,
|
|
1989
|
+
msg: "Image processing failed",
|
|
1990
|
+
collection: this.collection,
|
|
1991
|
+
filename: file.name
|
|
1992
|
+
});
|
|
1535
1993
|
}
|
|
1536
1994
|
}
|
|
1537
1995
|
const fileData = await storage.upload({
|
|
@@ -1564,7 +2022,13 @@ var MediaController = class {
|
|
|
1564
2022
|
height: sizeData.height
|
|
1565
2023
|
};
|
|
1566
2024
|
} catch (err) {
|
|
1567
|
-
|
|
2025
|
+
getRequestLogger(c, this.loggerComponent).error({
|
|
2026
|
+
err,
|
|
2027
|
+
msg: "Failed to upload image size",
|
|
2028
|
+
collection: this.collection,
|
|
2029
|
+
filename: file.name,
|
|
2030
|
+
sizeName
|
|
2031
|
+
});
|
|
1568
2032
|
}
|
|
1569
2033
|
}
|
|
1570
2034
|
}
|
|
@@ -1628,25 +2092,37 @@ var MediaController = class {
|
|
|
1628
2092
|
}
|
|
1629
2093
|
};
|
|
1630
2094
|
|
|
1631
|
-
// src/auth/
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
function
|
|
1635
|
-
const
|
|
1636
|
-
if (!
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
2095
|
+
// src/auth/lockout.ts
|
|
2096
|
+
var DEFAULT_MAX_LOGIN_ATTEMPTS = 5;
|
|
2097
|
+
var DEFAULT_LOCK_TIME_MS = 10 * 60 * 1e3;
|
|
2098
|
+
function resolveAuthLockoutConfig(collection) {
|
|
2099
|
+
const auth = collection.auth;
|
|
2100
|
+
if (!auth) {
|
|
2101
|
+
return {
|
|
2102
|
+
enabled: false,
|
|
2103
|
+
maxLoginAttempts: DEFAULT_MAX_LOGIN_ATTEMPTS,
|
|
2104
|
+
lockTime: DEFAULT_LOCK_TIME_MS
|
|
2105
|
+
};
|
|
1640
2106
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
2107
|
+
if (auth === true) {
|
|
2108
|
+
return {
|
|
2109
|
+
enabled: true,
|
|
2110
|
+
maxLoginAttempts: DEFAULT_MAX_LOGIN_ATTEMPTS,
|
|
2111
|
+
lockTime: DEFAULT_LOCK_TIME_MS
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
const maxLoginAttempts = typeof auth.maxLoginAttempts === "number" ? auth.maxLoginAttempts : DEFAULT_MAX_LOGIN_ATTEMPTS;
|
|
2115
|
+
const lockTime = typeof auth.lockTime === "number" && auth.lockTime > 0 ? auth.lockTime : DEFAULT_LOCK_TIME_MS;
|
|
2116
|
+
return {
|
|
2117
|
+
enabled: maxLoginAttempts > 0,
|
|
2118
|
+
maxLoginAttempts,
|
|
2119
|
+
lockTime
|
|
2120
|
+
};
|
|
1646
2121
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
2122
|
+
function getLockedUntilMs(value) {
|
|
2123
|
+
if (typeof value !== "string") return null;
|
|
2124
|
+
const parsed = Date.parse(value);
|
|
2125
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1650
2126
|
}
|
|
1651
2127
|
|
|
1652
2128
|
// src/services/email-template.ts
|
|
@@ -1746,6 +2222,7 @@ async function getDevSend() {
|
|
|
1746
2222
|
if (_devSend) return _devSend;
|
|
1747
2223
|
if (_devSendPromise) return _devSendPromise;
|
|
1748
2224
|
_devSendPromise = (async () => {
|
|
2225
|
+
const logger = getConfigLogger(void 0, "email");
|
|
1749
2226
|
try {
|
|
1750
2227
|
const nodemailer = await import("nodemailer");
|
|
1751
2228
|
const account = await nodemailer.default.createTestAccount();
|
|
@@ -1754,23 +2231,50 @@ async function getDevSend() {
|
|
|
1754
2231
|
port: 587,
|
|
1755
2232
|
auth: { user: account.user, pass: account.pass }
|
|
1756
2233
|
});
|
|
1757
|
-
|
|
1758
|
-
|
|
2234
|
+
logger.info({
|
|
2235
|
+
msg: "No email config; using Ethereal for development email preview"
|
|
2236
|
+
});
|
|
2237
|
+
logger.info({
|
|
2238
|
+
msg: "Ethereal credentials",
|
|
2239
|
+
loginUrl: "https://ethereal.email",
|
|
2240
|
+
user: account.user,
|
|
2241
|
+
password: account.pass
|
|
2242
|
+
});
|
|
1759
2243
|
_devSend = async ({ to, subject, html }) => {
|
|
1760
2244
|
const info = await transport.sendMail({ from: '"Dyrected Dev" <dev@dyrected.local>', to, subject, html });
|
|
1761
|
-
|
|
2245
|
+
logger.info({
|
|
2246
|
+
msg: "Email preview URL",
|
|
2247
|
+
previewUrl: nodemailer.default.getTestMessageUrl(info)
|
|
2248
|
+
});
|
|
1762
2249
|
};
|
|
1763
2250
|
return _devSend;
|
|
1764
2251
|
} catch {
|
|
1765
|
-
|
|
2252
|
+
logger.warn({
|
|
2253
|
+
msg: "nodemailer not available; development emails will not be sent"
|
|
2254
|
+
});
|
|
1766
2255
|
return null;
|
|
1767
2256
|
}
|
|
1768
2257
|
})();
|
|
1769
2258
|
return _devSendPromise;
|
|
1770
2259
|
}
|
|
1771
2260
|
async function sendEmail(config, payload) {
|
|
2261
|
+
const logger = getConfigLogger(config, "email");
|
|
2262
|
+
const observability = getObservabilityRuntime(config);
|
|
1772
2263
|
if (config.email) {
|
|
1773
|
-
|
|
2264
|
+
try {
|
|
2265
|
+
await config.email.send(payload);
|
|
2266
|
+
} catch (err) {
|
|
2267
|
+
observability?.recordEmailSendFailure({
|
|
2268
|
+
to: payload.to
|
|
2269
|
+
});
|
|
2270
|
+
logger.error({
|
|
2271
|
+
err,
|
|
2272
|
+
msg: "Email provider send failed",
|
|
2273
|
+
to: payload.to,
|
|
2274
|
+
subject: payload.subject
|
|
2275
|
+
});
|
|
2276
|
+
throw err;
|
|
2277
|
+
}
|
|
1774
2278
|
return;
|
|
1775
2279
|
}
|
|
1776
2280
|
if (process.env.NODE_ENV !== "production") {
|
|
@@ -1834,6 +2338,55 @@ var AuthController = class {
|
|
|
1834
2338
|
constructor(collection) {
|
|
1835
2339
|
this.collection = collection;
|
|
1836
2340
|
}
|
|
2341
|
+
sanitizeUser(user) {
|
|
2342
|
+
const {
|
|
2343
|
+
password: _password,
|
|
2344
|
+
loginAttempts: _loginAttempts,
|
|
2345
|
+
lockedUntil: _lockedUntil,
|
|
2346
|
+
...safeUser
|
|
2347
|
+
} = user;
|
|
2348
|
+
return safeUser;
|
|
2349
|
+
}
|
|
2350
|
+
clearLockoutState(c, userId) {
|
|
2351
|
+
const db = c.get("config").db;
|
|
2352
|
+
if (!db) return Promise.resolve(null);
|
|
2353
|
+
return db.update({
|
|
2354
|
+
collection: this.collection.slug,
|
|
2355
|
+
id: userId,
|
|
2356
|
+
data: {
|
|
2357
|
+
loginAttempts: 0,
|
|
2358
|
+
lockedUntil: null
|
|
2359
|
+
}
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
async recordFailedLogin(c, user) {
|
|
2363
|
+
const db = c.get("config").db;
|
|
2364
|
+
if (!db)
|
|
2365
|
+
return { justLocked: false, retryAfterSeconds: null };
|
|
2366
|
+
const lockout = resolveAuthLockoutConfig(this.collection);
|
|
2367
|
+
if (!lockout.enabled) {
|
|
2368
|
+
return { justLocked: false, retryAfterSeconds: null };
|
|
2369
|
+
}
|
|
2370
|
+
const now = Date.now();
|
|
2371
|
+
const lockedUntilMs = getLockedUntilMs(user.lockedUntil);
|
|
2372
|
+
const lockExpired = lockedUntilMs !== null && lockedUntilMs <= now;
|
|
2373
|
+
const currentAttempts = !lockExpired && typeof user.loginAttempts === "number" && user.loginAttempts > 0 ? user.loginAttempts : 0;
|
|
2374
|
+
const loginAttempts = currentAttempts + 1;
|
|
2375
|
+
const justLocked = loginAttempts >= lockout.maxLoginAttempts;
|
|
2376
|
+
const nextLockedUntil = justLocked ? new Date(now + lockout.lockTime).toISOString() : null;
|
|
2377
|
+
await db.update({
|
|
2378
|
+
collection: this.collection.slug,
|
|
2379
|
+
id: String(user.id),
|
|
2380
|
+
data: {
|
|
2381
|
+
loginAttempts,
|
|
2382
|
+
lockedUntil: nextLockedUntil
|
|
2383
|
+
}
|
|
2384
|
+
});
|
|
2385
|
+
return {
|
|
2386
|
+
justLocked,
|
|
2387
|
+
retryAfterSeconds: justLocked ? Math.max(1, Math.ceil(lockout.lockTime / 1e3)) : null
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
1837
2390
|
// ---------------------------------------------------------------------------
|
|
1838
2391
|
// GET /init
|
|
1839
2392
|
// Checks if the first user needs to be created.
|
|
@@ -1862,11 +2415,17 @@ var AuthController = class {
|
|
|
1862
2415
|
limit: 1
|
|
1863
2416
|
});
|
|
1864
2417
|
if (check.total > 0) {
|
|
1865
|
-
return c.json(
|
|
2418
|
+
return c.json(
|
|
2419
|
+
{ error: true, message: "Initial user already exists." },
|
|
2420
|
+
403
|
|
2421
|
+
);
|
|
1866
2422
|
}
|
|
1867
2423
|
const body = await c.req.json().catch(() => null);
|
|
1868
2424
|
if (!body?.email || !body?.password) {
|
|
1869
|
-
return c.json(
|
|
2425
|
+
return c.json(
|
|
2426
|
+
{ error: true, message: "email and password are required." },
|
|
2427
|
+
400
|
|
2428
|
+
);
|
|
1870
2429
|
}
|
|
1871
2430
|
const hashedPassword = await hashPassword(body.password);
|
|
1872
2431
|
const user = await db.create({
|
|
@@ -1878,16 +2437,23 @@ var AuthController = class {
|
|
|
1878
2437
|
// Default first user to admin
|
|
1879
2438
|
}
|
|
1880
2439
|
});
|
|
1881
|
-
const token = await
|
|
1882
|
-
|
|
2440
|
+
const token = await issueAuthSessionToken({
|
|
2441
|
+
config,
|
|
2442
|
+
userId: user.id,
|
|
1883
2443
|
email: user.email,
|
|
1884
|
-
collection: this.collection.slug
|
|
2444
|
+
collection: this.collection.slug,
|
|
2445
|
+
ip: c.get("clientIp"),
|
|
2446
|
+
authSource: "local"
|
|
1885
2447
|
});
|
|
1886
2448
|
const { subject, html } = buildWelcomeEmail(config, { email: body.email });
|
|
1887
2449
|
sendEmail(config, { to: body.email, subject, html }).catch(
|
|
1888
|
-
(err) =>
|
|
2450
|
+
(err) => getRequestLogger(c, "auth").error({
|
|
2451
|
+
err,
|
|
2452
|
+
msg: "Failed to send welcome email",
|
|
2453
|
+
email: body.email
|
|
2454
|
+
})
|
|
1889
2455
|
);
|
|
1890
|
-
const
|
|
2456
|
+
const safeUser = this.sanitizeUser(user);
|
|
1891
2457
|
return c.json({ token, user: safeUser });
|
|
1892
2458
|
}
|
|
1893
2459
|
// ---------------------------------------------------------------------------
|
|
@@ -1898,7 +2464,10 @@ var AuthController = class {
|
|
|
1898
2464
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
1899
2465
|
const body = await c.req.json().catch(() => null);
|
|
1900
2466
|
if (!body?.email || !body?.password) {
|
|
1901
|
-
return c.json(
|
|
2467
|
+
return c.json(
|
|
2468
|
+
{ error: true, message: "email and password are required." },
|
|
2469
|
+
400
|
|
2470
|
+
);
|
|
1902
2471
|
}
|
|
1903
2472
|
const result = await db.find({
|
|
1904
2473
|
collection: this.collection.slug,
|
|
@@ -1907,27 +2476,107 @@ var AuthController = class {
|
|
|
1907
2476
|
});
|
|
1908
2477
|
const user = result.docs[0];
|
|
1909
2478
|
if (!user) {
|
|
1910
|
-
return c.json(
|
|
2479
|
+
return c.json(
|
|
2480
|
+
{ error: true, message: "Invalid email or password." },
|
|
2481
|
+
401
|
|
2482
|
+
);
|
|
2483
|
+
}
|
|
2484
|
+
const lockedUntilMs = getLockedUntilMs(user.lockedUntil);
|
|
2485
|
+
if (lockedUntilMs !== null && lockedUntilMs > Date.now()) {
|
|
2486
|
+
const retryAfterSeconds = Math.max(
|
|
2487
|
+
1,
|
|
2488
|
+
Math.ceil((lockedUntilMs - Date.now()) / 1e3)
|
|
2489
|
+
);
|
|
2490
|
+
c.header("Retry-After", String(retryAfterSeconds));
|
|
2491
|
+
return c.json(
|
|
2492
|
+
{
|
|
2493
|
+
error: true,
|
|
2494
|
+
message: "Too many login attempts. Try again later.",
|
|
2495
|
+
retryAfterSeconds
|
|
2496
|
+
},
|
|
2497
|
+
429
|
|
2498
|
+
);
|
|
1911
2499
|
}
|
|
1912
2500
|
const valid = await verifyPassword(body.password, user.password);
|
|
1913
2501
|
if (!valid) {
|
|
1914
|
-
|
|
2502
|
+
const { justLocked, retryAfterSeconds } = await this.recordFailedLogin(
|
|
2503
|
+
c,
|
|
2504
|
+
user
|
|
2505
|
+
);
|
|
2506
|
+
if (justLocked && retryAfterSeconds) {
|
|
2507
|
+
c.header("Retry-After", String(retryAfterSeconds));
|
|
2508
|
+
return c.json(
|
|
2509
|
+
{
|
|
2510
|
+
error: true,
|
|
2511
|
+
message: "Too many login attempts. Try again later.",
|
|
2512
|
+
retryAfterSeconds
|
|
2513
|
+
},
|
|
2514
|
+
429
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2517
|
+
return c.json(
|
|
2518
|
+
{ error: true, message: "Invalid email or password." },
|
|
2519
|
+
401
|
|
2520
|
+
);
|
|
1915
2521
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
2522
|
+
if (typeof user.loginAttempts === "number" && user.loginAttempts > 0 || user.lockedUntil != null) {
|
|
2523
|
+
await this.clearLockoutState(c, String(user.id));
|
|
2524
|
+
}
|
|
2525
|
+
const token = await issueAuthSessionToken({
|
|
2526
|
+
config: c.get("config"),
|
|
2527
|
+
userId: user.id,
|
|
1918
2528
|
email: user.email,
|
|
1919
|
-
collection: this.collection.slug
|
|
2529
|
+
collection: this.collection.slug,
|
|
2530
|
+
ip: c.get("clientIp"),
|
|
2531
|
+
authSource: "local"
|
|
1920
2532
|
});
|
|
1921
|
-
const
|
|
2533
|
+
const safeUser = this.sanitizeUser(user);
|
|
1922
2534
|
return c.json({ token, user: safeUser });
|
|
1923
2535
|
}
|
|
1924
2536
|
// ---------------------------------------------------------------------------
|
|
1925
2537
|
// POST /logout
|
|
1926
|
-
//
|
|
1927
|
-
//
|
|
2538
|
+
// Revoke the current session by default. Pass `?allSessions=true` to revoke
|
|
2539
|
+
// every active session for the current account.
|
|
1928
2540
|
// ---------------------------------------------------------------------------
|
|
1929
2541
|
async logout(c) {
|
|
1930
|
-
|
|
2542
|
+
const requestUser = c.get("user");
|
|
2543
|
+
const tokenPayload = c.get("authTokenPayload");
|
|
2544
|
+
const allSessions = ["1", "true", "yes"].includes(
|
|
2545
|
+
(c.req.query("allSessions") || "").toLowerCase()
|
|
2546
|
+
);
|
|
2547
|
+
if (!requestUser) {
|
|
2548
|
+
if (allSessions) {
|
|
2549
|
+
return c.json(
|
|
2550
|
+
{ error: true, message: "Authentication required." },
|
|
2551
|
+
401
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
return c.json({
|
|
2555
|
+
success: true,
|
|
2556
|
+
message: "Logged out. Discard your token."
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
if (allSessions) {
|
|
2560
|
+
await revokeAllAuthSessions(c.get("config"), {
|
|
2561
|
+
userId: requestUser.sub,
|
|
2562
|
+
collection: this.collection.slug
|
|
2563
|
+
});
|
|
2564
|
+
return c.json({
|
|
2565
|
+
success: true,
|
|
2566
|
+
message: "All sessions have been logged out."
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
if (tokenPayload?.sid) {
|
|
2570
|
+
await revokeAuthSession(c.get("config"), tokenPayload.sid);
|
|
2571
|
+
return c.json({
|
|
2572
|
+
success: true,
|
|
2573
|
+
message: "Logged out."
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
return c.json({
|
|
2577
|
+
success: true,
|
|
2578
|
+
message: "Logged out. Discard your token."
|
|
2579
|
+
});
|
|
1931
2580
|
}
|
|
1932
2581
|
// ---------------------------------------------------------------------------
|
|
1933
2582
|
// GET /me
|
|
@@ -1939,11 +2588,14 @@ var AuthController = class {
|
|
|
1939
2588
|
if (!requestUser) {
|
|
1940
2589
|
return c.json({ error: true, message: "Authentication required." }, 401);
|
|
1941
2590
|
}
|
|
1942
|
-
const user = await db.findOne({
|
|
2591
|
+
const user = await db.findOne({
|
|
2592
|
+
collection: this.collection.slug,
|
|
2593
|
+
id: requestUser.sub
|
|
2594
|
+
});
|
|
1943
2595
|
if (!user) {
|
|
1944
2596
|
return c.json({ error: true, message: "User not found." }, 404);
|
|
1945
2597
|
}
|
|
1946
|
-
const
|
|
2598
|
+
const safeUser = this.sanitizeUser(user);
|
|
1947
2599
|
return c.json(safeUser);
|
|
1948
2600
|
}
|
|
1949
2601
|
// ---------------------------------------------------------------------------
|
|
@@ -1954,10 +2606,29 @@ var AuthController = class {
|
|
|
1954
2606
|
if (!requestUser) {
|
|
1955
2607
|
return c.json({ error: true, message: "Authentication required." }, 401);
|
|
1956
2608
|
}
|
|
1957
|
-
|
|
1958
|
-
|
|
2609
|
+
if (!requestUser.email) {
|
|
2610
|
+
return c.json(
|
|
2611
|
+
{ error: true, message: "Authenticated user is missing an email." },
|
|
2612
|
+
400
|
|
2613
|
+
);
|
|
2614
|
+
}
|
|
2615
|
+
const tokenPayload = c.get("authTokenPayload");
|
|
2616
|
+
if (tokenPayload?.sid) {
|
|
2617
|
+
const token2 = await signCollectionToken({
|
|
2618
|
+
sub: requestUser.sub,
|
|
2619
|
+
email: requestUser.email,
|
|
2620
|
+
collection: this.collection.slug,
|
|
2621
|
+
sid: tokenPayload.sid
|
|
2622
|
+
});
|
|
2623
|
+
return c.json({ token: token2 });
|
|
2624
|
+
}
|
|
2625
|
+
const token = await issueAuthSessionToken({
|
|
2626
|
+
config: c.get("config"),
|
|
2627
|
+
userId: requestUser.sub,
|
|
1959
2628
|
email: requestUser.email,
|
|
1960
|
-
collection: this.collection.slug
|
|
2629
|
+
collection: this.collection.slug,
|
|
2630
|
+
ip: c.get("clientIp"),
|
|
2631
|
+
authSource: "local"
|
|
1961
2632
|
});
|
|
1962
2633
|
return c.json({ token });
|
|
1963
2634
|
}
|
|
@@ -1982,16 +2653,28 @@ var AuthController = class {
|
|
|
1982
2653
|
const user = result.docs[0];
|
|
1983
2654
|
if (user) {
|
|
1984
2655
|
const resetToken = await signCollectionToken(
|
|
1985
|
-
{
|
|
2656
|
+
{
|
|
2657
|
+
sub: user.id,
|
|
2658
|
+
email: user.email,
|
|
2659
|
+
collection: this.collection.slug,
|
|
2660
|
+
purpose: "reset"
|
|
2661
|
+
},
|
|
1986
2662
|
"1h"
|
|
1987
2663
|
);
|
|
1988
2664
|
const resetUrl = body?.resetUrl;
|
|
1989
2665
|
const url = resetUrl ? `${resetUrl}${resetUrl.includes("?") ? "&" : "?"}token=${encodeURIComponent(resetToken)}` : void 0;
|
|
1990
2666
|
try {
|
|
1991
|
-
const { subject, html } = buildResetPasswordEmail(config, {
|
|
2667
|
+
const { subject, html } = buildResetPasswordEmail(config, {
|
|
2668
|
+
token: resetToken,
|
|
2669
|
+
url
|
|
2670
|
+
});
|
|
1992
2671
|
await sendEmail(config, { to: user.email, subject, html });
|
|
1993
2672
|
} catch (err) {
|
|
1994
|
-
|
|
2673
|
+
getRequestLogger(c, "auth").error({
|
|
2674
|
+
err,
|
|
2675
|
+
msg: "Failed to send password reset email",
|
|
2676
|
+
email: user.email
|
|
2677
|
+
});
|
|
1995
2678
|
}
|
|
1996
2679
|
}
|
|
1997
2680
|
return c.json({
|
|
@@ -2010,28 +2693,54 @@ var AuthController = class {
|
|
|
2010
2693
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2011
2694
|
const body = await c.req.json().catch(() => null);
|
|
2012
2695
|
if (!body?.token || !body?.password) {
|
|
2013
|
-
return c.json(
|
|
2696
|
+
return c.json(
|
|
2697
|
+
{ error: true, message: "token and password are required." },
|
|
2698
|
+
400
|
|
2699
|
+
);
|
|
2014
2700
|
}
|
|
2015
2701
|
let payload;
|
|
2016
2702
|
try {
|
|
2017
2703
|
payload = await verifyCollectionToken(body.token);
|
|
2018
2704
|
} catch {
|
|
2019
|
-
return c.json(
|
|
2705
|
+
return c.json(
|
|
2706
|
+
{ error: true, message: "Reset token is invalid or has expired." },
|
|
2707
|
+
400
|
|
2708
|
+
);
|
|
2020
2709
|
}
|
|
2021
2710
|
if (payload.collection !== this.collection.slug || payload.purpose !== "reset") {
|
|
2022
|
-
return c.json(
|
|
2711
|
+
return c.json(
|
|
2712
|
+
{ error: true, message: "Reset token is invalid or has expired." },
|
|
2713
|
+
400
|
|
2714
|
+
);
|
|
2023
2715
|
}
|
|
2024
2716
|
const hashedPassword = await hashPassword(body.password);
|
|
2025
2717
|
await db.update({
|
|
2026
2718
|
collection: this.collection.slug,
|
|
2027
2719
|
id: payload.sub,
|
|
2028
|
-
data: {
|
|
2720
|
+
data: {
|
|
2721
|
+
password: hashedPassword,
|
|
2722
|
+
loginAttempts: 0,
|
|
2723
|
+
lockedUntil: null
|
|
2724
|
+
}
|
|
2725
|
+
});
|
|
2726
|
+
await revokeAllAuthSessions(config, {
|
|
2727
|
+
userId: payload.sub,
|
|
2728
|
+
collection: this.collection.slug
|
|
2729
|
+
});
|
|
2730
|
+
const { subject, html } = buildPasswordChangedEmail(config, {
|
|
2731
|
+
email: payload.email
|
|
2029
2732
|
});
|
|
2030
|
-
const { subject, html } = buildPasswordChangedEmail(config, { email: payload.email });
|
|
2031
2733
|
sendEmail(config, { to: payload.email, subject, html }).catch(
|
|
2032
|
-
(err) =>
|
|
2734
|
+
(err) => getRequestLogger(c, "auth").error({
|
|
2735
|
+
err,
|
|
2736
|
+
msg: "Failed to send password-changed email",
|
|
2737
|
+
email: payload.email
|
|
2738
|
+
})
|
|
2033
2739
|
);
|
|
2034
|
-
return c.json({
|
|
2740
|
+
return c.json({
|
|
2741
|
+
success: true,
|
|
2742
|
+
message: "Password has been reset. You can now log in."
|
|
2743
|
+
});
|
|
2035
2744
|
}
|
|
2036
2745
|
// ---------------------------------------------------------------------------
|
|
2037
2746
|
// POST /invite
|
|
@@ -2055,10 +2764,18 @@ var AuthController = class {
|
|
|
2055
2764
|
limit: 1
|
|
2056
2765
|
});
|
|
2057
2766
|
if (existing.total > 0) {
|
|
2058
|
-
return c.json(
|
|
2767
|
+
return c.json(
|
|
2768
|
+
{ error: true, message: "An account with that email already exists." },
|
|
2769
|
+
409
|
|
2770
|
+
);
|
|
2059
2771
|
}
|
|
2060
2772
|
const inviteToken = await signCollectionToken(
|
|
2061
|
-
{
|
|
2773
|
+
{
|
|
2774
|
+
sub: body.email,
|
|
2775
|
+
email: body.email,
|
|
2776
|
+
collection: this.collection.slug,
|
|
2777
|
+
purpose: "invite"
|
|
2778
|
+
},
|
|
2062
2779
|
"7d"
|
|
2063
2780
|
);
|
|
2064
2781
|
try {
|
|
@@ -2068,7 +2785,11 @@ var AuthController = class {
|
|
|
2068
2785
|
});
|
|
2069
2786
|
await sendEmail(config, { to: body.email, subject, html });
|
|
2070
2787
|
} catch (err) {
|
|
2071
|
-
|
|
2788
|
+
getRequestLogger(c, "auth").error({
|
|
2789
|
+
err,
|
|
2790
|
+
msg: "Failed to send invite email",
|
|
2791
|
+
email: body.email
|
|
2792
|
+
});
|
|
2072
2793
|
}
|
|
2073
2794
|
return c.json({ success: true, message: `Invite sent to ${body.email}.` });
|
|
2074
2795
|
}
|
|
@@ -2083,16 +2804,25 @@ var AuthController = class {
|
|
|
2083
2804
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
2084
2805
|
const body = await c.req.json().catch(() => null);
|
|
2085
2806
|
if (!body?.token || !body?.password) {
|
|
2086
|
-
return c.json(
|
|
2807
|
+
return c.json(
|
|
2808
|
+
{ error: true, message: "token and password are required." },
|
|
2809
|
+
400
|
|
2810
|
+
);
|
|
2087
2811
|
}
|
|
2088
2812
|
let payload;
|
|
2089
2813
|
try {
|
|
2090
2814
|
payload = await verifyCollectionToken(body.token);
|
|
2091
2815
|
} catch {
|
|
2092
|
-
return c.json(
|
|
2816
|
+
return c.json(
|
|
2817
|
+
{ error: true, message: "Invite token is invalid or has expired." },
|
|
2818
|
+
400
|
|
2819
|
+
);
|
|
2093
2820
|
}
|
|
2094
2821
|
if (payload.collection !== this.collection.slug || payload.purpose !== "invite") {
|
|
2095
|
-
return c.json(
|
|
2822
|
+
return c.json(
|
|
2823
|
+
{ error: true, message: "Invite token is invalid or has expired." },
|
|
2824
|
+
400
|
|
2825
|
+
);
|
|
2096
2826
|
}
|
|
2097
2827
|
const inviteeEmail = payload.sub;
|
|
2098
2828
|
const existing = await db.find({
|
|
@@ -2101,7 +2831,10 @@ var AuthController = class {
|
|
|
2101
2831
|
limit: 1
|
|
2102
2832
|
});
|
|
2103
2833
|
if (existing.total > 0) {
|
|
2104
|
-
return c.json(
|
|
2834
|
+
return c.json(
|
|
2835
|
+
{ error: true, message: "An account with that email already exists." },
|
|
2836
|
+
409
|
|
2837
|
+
);
|
|
2105
2838
|
}
|
|
2106
2839
|
const { token: _t, password: _p, ...extraFields } = body;
|
|
2107
2840
|
const hashedPassword = await hashPassword(body.password);
|
|
@@ -2109,28 +2842,37 @@ var AuthController = class {
|
|
|
2109
2842
|
collection: this.collection.slug,
|
|
2110
2843
|
data: { ...extraFields, email: inviteeEmail, password: hashedPassword }
|
|
2111
2844
|
});
|
|
2112
|
-
const sessionToken = await
|
|
2113
|
-
|
|
2845
|
+
const sessionToken = await issueAuthSessionToken({
|
|
2846
|
+
config,
|
|
2847
|
+
userId: user.id,
|
|
2114
2848
|
email: inviteeEmail,
|
|
2115
|
-
collection: this.collection.slug
|
|
2849
|
+
collection: this.collection.slug,
|
|
2850
|
+
ip: c.get("clientIp"),
|
|
2851
|
+
authSource: "local"
|
|
2852
|
+
});
|
|
2853
|
+
const { subject, html } = buildWelcomeEmail(config, {
|
|
2854
|
+
email: inviteeEmail
|
|
2116
2855
|
});
|
|
2117
|
-
const { subject, html } = buildWelcomeEmail(config, { email: inviteeEmail });
|
|
2118
2856
|
sendEmail(config, { to: inviteeEmail, subject, html }).catch(
|
|
2119
|
-
(err) =>
|
|
2857
|
+
(err) => getRequestLogger(c, "auth").error({
|
|
2858
|
+
err,
|
|
2859
|
+
msg: "Failed to send welcome email",
|
|
2860
|
+
email: inviteeEmail
|
|
2861
|
+
})
|
|
2120
2862
|
);
|
|
2121
|
-
const
|
|
2863
|
+
const safeUser = this.sanitizeUser(user);
|
|
2122
2864
|
return c.json({ token: sessionToken, user: safeUser }, 201);
|
|
2123
2865
|
}
|
|
2124
2866
|
};
|
|
2125
2867
|
|
|
2126
2868
|
// src/controllers/admin-auth.controller.ts
|
|
2127
2869
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
2128
|
-
import { TextEncoder
|
|
2870
|
+
import { TextEncoder } from "util";
|
|
2129
2871
|
import {
|
|
2130
|
-
SignJWT
|
|
2872
|
+
SignJWT,
|
|
2131
2873
|
createRemoteJWKSet,
|
|
2132
|
-
jwtVerify
|
|
2133
|
-
decodeJwt
|
|
2874
|
+
jwtVerify,
|
|
2875
|
+
decodeJwt
|
|
2134
2876
|
} from "jose";
|
|
2135
2877
|
var AdminAuthController = class {
|
|
2136
2878
|
constructor(config) {
|
|
@@ -2139,13 +2881,21 @@ var AdminAuthController = class {
|
|
|
2139
2881
|
config;
|
|
2140
2882
|
async providers(c) {
|
|
2141
2883
|
const requestConfig = await this.getRequestConfig(c);
|
|
2142
|
-
return c.json(
|
|
2884
|
+
return c.json(
|
|
2885
|
+
getPublicAdminAuthConfig(
|
|
2886
|
+
requestConfig.adminAuth,
|
|
2887
|
+
requestConfig.collections
|
|
2888
|
+
)
|
|
2889
|
+
);
|
|
2143
2890
|
}
|
|
2144
2891
|
async start(c) {
|
|
2145
2892
|
const requestConfig = await this.getRequestConfig(c);
|
|
2146
2893
|
const provider = this.getProvider(requestConfig, c.req.param("provider"));
|
|
2147
2894
|
if (!provider) {
|
|
2148
|
-
return c.json(
|
|
2895
|
+
return c.json(
|
|
2896
|
+
{ error: true, message: "Admin auth provider not found." },
|
|
2897
|
+
404
|
|
2898
|
+
);
|
|
2149
2899
|
}
|
|
2150
2900
|
const siteId = this.getSiteId(c);
|
|
2151
2901
|
const returnTo = this.normalizeReturnTo(c.req.query("returnTo"), c);
|
|
@@ -2159,9 +2909,15 @@ var AdminAuthController = class {
|
|
|
2159
2909
|
return c.redirect(redirectUrl, 302);
|
|
2160
2910
|
}
|
|
2161
2911
|
if (!provider.startUrl) {
|
|
2162
|
-
return c.json(
|
|
2912
|
+
return c.json(
|
|
2913
|
+
{ error: true, message: "This provider does not expose a start URL." },
|
|
2914
|
+
400
|
|
2915
|
+
);
|
|
2163
2916
|
}
|
|
2164
|
-
const url = this.buildCustomProviderStartUrl(
|
|
2917
|
+
const url = this.buildCustomProviderStartUrl(
|
|
2918
|
+
provider.startUrl,
|
|
2919
|
+
new URL(c.req.url).origin
|
|
2920
|
+
);
|
|
2165
2921
|
if (!url) {
|
|
2166
2922
|
return c.json(
|
|
2167
2923
|
{
|
|
@@ -2180,13 +2936,23 @@ var AdminAuthController = class {
|
|
|
2180
2936
|
const requestConfig = await this.getRequestConfig(c);
|
|
2181
2937
|
const provider = this.getProvider(requestConfig, c.req.param("provider"));
|
|
2182
2938
|
if (!provider) {
|
|
2183
|
-
return c.json(
|
|
2939
|
+
return c.json(
|
|
2940
|
+
{ error: true, message: "Admin auth provider not found." },
|
|
2941
|
+
404
|
|
2942
|
+
);
|
|
2184
2943
|
}
|
|
2185
2944
|
try {
|
|
2186
|
-
const exchange = await this.completeProviderAuth(
|
|
2945
|
+
const exchange = await this.completeProviderAuth(
|
|
2946
|
+
requestConfig,
|
|
2947
|
+
provider,
|
|
2948
|
+
c
|
|
2949
|
+
);
|
|
2187
2950
|
const redirectUrl = new URL(exchange.returnTo);
|
|
2188
2951
|
redirectUrl.searchParams.set("dyrectedExternalToken", exchange.token);
|
|
2189
|
-
redirectUrl.searchParams.set(
|
|
2952
|
+
redirectUrl.searchParams.set(
|
|
2953
|
+
"dyrectedAdminCollection",
|
|
2954
|
+
exchange.collectionSlug
|
|
2955
|
+
);
|
|
2190
2956
|
return c.redirect(redirectUrl.toString(), 302);
|
|
2191
2957
|
} catch (error) {
|
|
2192
2958
|
const message = error instanceof Error ? error.message : "External authentication failed.";
|
|
@@ -2200,10 +2966,17 @@ var AdminAuthController = class {
|
|
|
2200
2966
|
const requestConfig = await this.getRequestConfig(c);
|
|
2201
2967
|
const provider = this.getProvider(requestConfig, c.req.param("provider"));
|
|
2202
2968
|
if (!provider) {
|
|
2203
|
-
return c.json(
|
|
2969
|
+
return c.json(
|
|
2970
|
+
{ error: true, message: "Admin auth provider not found." },
|
|
2971
|
+
404
|
|
2972
|
+
);
|
|
2204
2973
|
}
|
|
2205
2974
|
try {
|
|
2206
|
-
const exchange = await this.completeProviderAuth(
|
|
2975
|
+
const exchange = await this.completeProviderAuth(
|
|
2976
|
+
requestConfig,
|
|
2977
|
+
provider,
|
|
2978
|
+
c
|
|
2979
|
+
);
|
|
2207
2980
|
return c.json({
|
|
2208
2981
|
token: exchange.token,
|
|
2209
2982
|
collectionSlug: exchange.collectionSlug,
|
|
@@ -2214,9 +2987,46 @@ var AdminAuthController = class {
|
|
|
2214
2987
|
const status = message === "Access denied for this site." ? 403 : 401;
|
|
2215
2988
|
return c.json({ error: true, message }, status);
|
|
2216
2989
|
}
|
|
2217
|
-
}
|
|
2218
|
-
async logout(c) {
|
|
2219
|
-
|
|
2990
|
+
}
|
|
2991
|
+
async logout(c) {
|
|
2992
|
+
const requestUser = c.get("user");
|
|
2993
|
+
const tokenPayload = c.get("authTokenPayload");
|
|
2994
|
+
const allSessions = ["1", "true", "yes"].includes(
|
|
2995
|
+
(c.req.query("allSessions") || "").toLowerCase()
|
|
2996
|
+
);
|
|
2997
|
+
if (!requestUser) {
|
|
2998
|
+
if (allSessions) {
|
|
2999
|
+
return c.json(
|
|
3000
|
+
{ error: true, message: "Authentication required." },
|
|
3001
|
+
401
|
|
3002
|
+
);
|
|
3003
|
+
}
|
|
3004
|
+
return c.json({
|
|
3005
|
+
success: true,
|
|
3006
|
+
message: "Logged out. Discard your token."
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
if (allSessions) {
|
|
3010
|
+
await revokeAllAuthSessions(c.get("config"), {
|
|
3011
|
+
userId: requestUser.sub,
|
|
3012
|
+
collection: requestUser.collection
|
|
3013
|
+
});
|
|
3014
|
+
return c.json({
|
|
3015
|
+
success: true,
|
|
3016
|
+
message: "All sessions have been logged out."
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
if (tokenPayload?.sid) {
|
|
3020
|
+
await revokeAuthSession(c.get("config"), tokenPayload.sid);
|
|
3021
|
+
return c.json({
|
|
3022
|
+
success: true,
|
|
3023
|
+
message: "Logged out."
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
3026
|
+
return c.json({
|
|
3027
|
+
success: true,
|
|
3028
|
+
message: "Logged out. Discard your token."
|
|
3029
|
+
});
|
|
2220
3030
|
}
|
|
2221
3031
|
getProvider(config, id) {
|
|
2222
3032
|
if (config.adminAuth?.mode !== "external") return void 0;
|
|
@@ -2232,7 +3042,12 @@ var AdminAuthController = class {
|
|
|
2232
3042
|
}
|
|
2233
3043
|
const db = c.get("config").db;
|
|
2234
3044
|
if (!db) throw new Error("Database not configured.");
|
|
2235
|
-
let user = await this.findUserByExternalIdentity(
|
|
3045
|
+
let user = await this.findUserByExternalIdentity(
|
|
3046
|
+
db,
|
|
3047
|
+
adminCollection.slug,
|
|
3048
|
+
provider.id,
|
|
3049
|
+
identity.sub
|
|
3050
|
+
) ?? (identity.email ? (await db.find({
|
|
2236
3051
|
collection: adminCollection.slug,
|
|
2237
3052
|
where: { email: identity.email },
|
|
2238
3053
|
limit: 1
|
|
@@ -2240,9 +3055,18 @@ var AdminAuthController = class {
|
|
|
2240
3055
|
const provisioningMode = requestConfig.adminAuth?.provisioningMode ?? "jit_plus_membership_management";
|
|
2241
3056
|
const allowJitProvisioning = provisioningMode !== "preprovisioned_only" && provider.allowJitProvisioning !== false;
|
|
2242
3057
|
if (!user && !allowJitProvisioning) {
|
|
2243
|
-
throw new Error(
|
|
3058
|
+
throw new Error(
|
|
3059
|
+
"This account has not been provisioned for admin access."
|
|
3060
|
+
);
|
|
2244
3061
|
}
|
|
2245
|
-
const access = await this.resolveAccess(
|
|
3062
|
+
const access = await this.resolveAccess(
|
|
3063
|
+
requestConfig,
|
|
3064
|
+
identity,
|
|
3065
|
+
provider.id,
|
|
3066
|
+
siteId,
|
|
3067
|
+
c,
|
|
3068
|
+
user
|
|
3069
|
+
);
|
|
2246
3070
|
if (!access.allowed) {
|
|
2247
3071
|
throw new Error("Access denied for this site.");
|
|
2248
3072
|
}
|
|
@@ -2250,7 +3074,12 @@ var AdminAuthController = class {
|
|
|
2250
3074
|
user = await db.create({
|
|
2251
3075
|
collection: adminCollection.slug,
|
|
2252
3076
|
data: {
|
|
2253
|
-
...await this.buildUserData(
|
|
3077
|
+
...await this.buildUserData(
|
|
3078
|
+
identity,
|
|
3079
|
+
provider.id,
|
|
3080
|
+
access.roles,
|
|
3081
|
+
access.data
|
|
3082
|
+
),
|
|
2254
3083
|
password: await hashPassword(randomBytes2(32).toString("hex"))
|
|
2255
3084
|
}
|
|
2256
3085
|
});
|
|
@@ -2258,18 +3087,25 @@ var AdminAuthController = class {
|
|
|
2258
3087
|
user = await db.update({
|
|
2259
3088
|
collection: adminCollection.slug,
|
|
2260
3089
|
id: user.id,
|
|
2261
|
-
data: await this.buildUserData(
|
|
3090
|
+
data: await this.buildUserData(
|
|
3091
|
+
identity,
|
|
3092
|
+
provider.id,
|
|
3093
|
+
access.roles,
|
|
3094
|
+
access.data
|
|
3095
|
+
)
|
|
2262
3096
|
});
|
|
2263
3097
|
}
|
|
2264
3098
|
if (!user?.email) {
|
|
2265
3099
|
throw new Error("Authenticated admin user is missing an email address.");
|
|
2266
3100
|
}
|
|
2267
|
-
const token = await
|
|
2268
|
-
|
|
3101
|
+
const token = await issueAuthSessionToken({
|
|
3102
|
+
config: c.get("config"),
|
|
3103
|
+
userId: user.id,
|
|
2269
3104
|
email: user.email,
|
|
2270
3105
|
collection: adminCollection.slug,
|
|
2271
3106
|
providerId: provider.id,
|
|
2272
|
-
authSource: "external"
|
|
3107
|
+
authSource: "external",
|
|
3108
|
+
ip: c.get("clientIp")
|
|
2273
3109
|
});
|
|
2274
3110
|
return {
|
|
2275
3111
|
token,
|
|
@@ -2323,14 +3159,10 @@ var AdminAuthController = class {
|
|
|
2323
3159
|
async getRequestConfig(c) {
|
|
2324
3160
|
const siteId = this.getSiteId(c);
|
|
2325
3161
|
if (!siteId || !this.config.onSchemaFetch) return this.config;
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
...dynamic.adminAuth ? { adminAuth: { ...this.config.adminAuth, ...dynamic.adminAuth } } : {},
|
|
2331
|
-
collections: dynamic.collections ? [...this.config.collections, ...dynamic.collections] : this.config.collections,
|
|
2332
|
-
globals: dynamic.globals ? [...this.config.globals, ...dynamic.globals] : this.config.globals
|
|
2333
|
-
};
|
|
3162
|
+
return mergeDynamicConfig(
|
|
3163
|
+
this.config,
|
|
3164
|
+
await this.config.onSchemaFetch(siteId)
|
|
3165
|
+
);
|
|
2334
3166
|
}
|
|
2335
3167
|
async buildOidcStartUrl(provider, returnTo, siteId, origin) {
|
|
2336
3168
|
const discovery = await this.getOidcDiscovery(provider);
|
|
@@ -2341,11 +3173,19 @@ var AdminAuthController = class {
|
|
|
2341
3173
|
siteId,
|
|
2342
3174
|
nonce
|
|
2343
3175
|
});
|
|
2344
|
-
const url = new URL(
|
|
3176
|
+
const url = new URL(
|
|
3177
|
+
provider.authorizationEndpoint || discovery.authorization_endpoint
|
|
3178
|
+
);
|
|
2345
3179
|
url.searchParams.set("response_type", "code");
|
|
2346
3180
|
url.searchParams.set("client_id", provider.clientId);
|
|
2347
|
-
url.searchParams.set(
|
|
2348
|
-
|
|
3181
|
+
url.searchParams.set(
|
|
3182
|
+
"redirect_uri",
|
|
3183
|
+
provider.redirectUri || this.defaultCallbackPath(provider.id, origin)
|
|
3184
|
+
);
|
|
3185
|
+
url.searchParams.set(
|
|
3186
|
+
"scope",
|
|
3187
|
+
(provider.scopes ?? ["openid", "profile", "email"]).join(" ")
|
|
3188
|
+
);
|
|
2349
3189
|
url.searchParams.set("state", state);
|
|
2350
3190
|
url.searchParams.set("nonce", nonce);
|
|
2351
3191
|
return url.toString();
|
|
@@ -2357,26 +3197,29 @@ var AdminAuthController = class {
|
|
|
2357
3197
|
const parsedState = await this.verifyState(state, provider.id);
|
|
2358
3198
|
const discovery = await this.getOidcDiscovery(provider);
|
|
2359
3199
|
const redirectUri = provider.redirectUri || this.defaultCallbackPath(provider.id, new URL(c.req.url).origin);
|
|
2360
|
-
const tokenResponse = await fetch(
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
3200
|
+
const tokenResponse = await fetch(
|
|
3201
|
+
provider.tokenEndpoint || discovery.token_endpoint,
|
|
3202
|
+
{
|
|
3203
|
+
method: "POST",
|
|
3204
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
3205
|
+
body: new URLSearchParams({
|
|
3206
|
+
grant_type: "authorization_code",
|
|
3207
|
+
code,
|
|
3208
|
+
redirect_uri: redirectUri,
|
|
3209
|
+
client_id: provider.clientId,
|
|
3210
|
+
client_secret: provider.clientSecret
|
|
3211
|
+
})
|
|
3212
|
+
}
|
|
3213
|
+
);
|
|
2371
3214
|
if (!tokenResponse.ok) {
|
|
2372
3215
|
throw new Error("OIDC token exchange failed.");
|
|
2373
3216
|
}
|
|
2374
3217
|
const tokenBody = await tokenResponse.json();
|
|
2375
3218
|
const idToken = tokenBody.id_token;
|
|
2376
|
-
let claims
|
|
3219
|
+
let claims;
|
|
2377
3220
|
if (idToken) {
|
|
2378
3221
|
const jwks = createRemoteJWKSet(new URL(discovery.jwks_uri));
|
|
2379
|
-
const { payload } = await
|
|
3222
|
+
const { payload } = await jwtVerify(idToken, jwks, {
|
|
2380
3223
|
issuer: provider.issuer,
|
|
2381
3224
|
audience: provider.clientId
|
|
2382
3225
|
});
|
|
@@ -2385,9 +3228,12 @@ var AdminAuthController = class {
|
|
|
2385
3228
|
}
|
|
2386
3229
|
claims = payload;
|
|
2387
3230
|
} else if (tokenBody.access_token && (provider.userInfoEndpoint || discovery.userinfo_endpoint)) {
|
|
2388
|
-
const userInfo = await fetch(
|
|
2389
|
-
|
|
2390
|
-
|
|
3231
|
+
const userInfo = await fetch(
|
|
3232
|
+
provider.userInfoEndpoint || discovery.userinfo_endpoint,
|
|
3233
|
+
{
|
|
3234
|
+
headers: { Authorization: `Bearer ${tokenBody.access_token}` }
|
|
3235
|
+
}
|
|
3236
|
+
);
|
|
2391
3237
|
if (!userInfo.ok) throw new Error("OIDC userinfo request failed.");
|
|
2392
3238
|
claims = await userInfo.json();
|
|
2393
3239
|
} else {
|
|
@@ -2407,21 +3253,21 @@ var AdminAuthController = class {
|
|
|
2407
3253
|
}
|
|
2408
3254
|
let claims;
|
|
2409
3255
|
if (provider.secret) {
|
|
2410
|
-
const secret = new
|
|
2411
|
-
const verified = await
|
|
3256
|
+
const secret = new TextEncoder().encode(provider.secret);
|
|
3257
|
+
const verified = await jwtVerify(token, secret, {
|
|
2412
3258
|
issuer: provider.issuer,
|
|
2413
3259
|
audience: provider.audience
|
|
2414
3260
|
});
|
|
2415
3261
|
claims = verified.payload;
|
|
2416
3262
|
} else if (provider.jwksUri) {
|
|
2417
3263
|
const jwks = createRemoteJWKSet(new URL(provider.jwksUri));
|
|
2418
|
-
const verified = await
|
|
3264
|
+
const verified = await jwtVerify(token, jwks, {
|
|
2419
3265
|
issuer: provider.issuer,
|
|
2420
3266
|
audience: provider.audience
|
|
2421
3267
|
});
|
|
2422
3268
|
claims = verified.payload;
|
|
2423
3269
|
} else {
|
|
2424
|
-
claims =
|
|
3270
|
+
claims = decodeJwt(token);
|
|
2425
3271
|
if (provider.issuer && claims.iss !== provider.issuer) {
|
|
2426
3272
|
throw new Error("External auth issuer mismatch.");
|
|
2427
3273
|
}
|
|
@@ -2432,7 +3278,10 @@ var AdminAuthController = class {
|
|
|
2432
3278
|
}
|
|
2433
3279
|
}
|
|
2434
3280
|
return {
|
|
2435
|
-
identity: this.mapClaims(
|
|
3281
|
+
identity: this.mapClaims(
|
|
3282
|
+
claims,
|
|
3283
|
+
provider.claimMapping
|
|
3284
|
+
),
|
|
2436
3285
|
returnTo: this.normalizeReturnTo(
|
|
2437
3286
|
body?.returnTo || c.req.query("returnTo"),
|
|
2438
3287
|
c
|
|
@@ -2456,9 +3305,15 @@ var AdminAuthController = class {
|
|
|
2456
3305
|
email: this.readStringClaim(claims[emailKey]),
|
|
2457
3306
|
name: this.readStringClaim(claims[nameKey]),
|
|
2458
3307
|
roles: this.readStringArrayClaim(rolesKey ? claims[rolesKey] : void 0),
|
|
2459
|
-
groups: this.readStringArrayClaim(
|
|
2460
|
-
|
|
2461
|
-
|
|
3308
|
+
groups: this.readStringArrayClaim(
|
|
3309
|
+
groupsKey ? claims[groupsKey] : void 0
|
|
3310
|
+
),
|
|
3311
|
+
siteIds: this.readStringArrayClaim(
|
|
3312
|
+
siteIdsKey ? claims[siteIdsKey] : void 0
|
|
3313
|
+
),
|
|
3314
|
+
workspaceIds: this.readStringArrayClaim(
|
|
3315
|
+
workspaceIdsKey ? claims[workspaceIdsKey] : void 0
|
|
3316
|
+
),
|
|
2462
3317
|
rawClaims: claims
|
|
2463
3318
|
};
|
|
2464
3319
|
}
|
|
@@ -2468,7 +3323,9 @@ var AdminAuthController = class {
|
|
|
2468
3323
|
readStringArrayClaim(value) {
|
|
2469
3324
|
if (!value) return void 0;
|
|
2470
3325
|
if (Array.isArray(value)) {
|
|
2471
|
-
return value.filter(
|
|
3326
|
+
return value.filter(
|
|
3327
|
+
(entry) => typeof entry === "string"
|
|
3328
|
+
);
|
|
2472
3329
|
}
|
|
2473
3330
|
if (typeof value === "string") return [value];
|
|
2474
3331
|
return void 0;
|
|
@@ -2478,7 +3335,8 @@ var AdminAuthController = class {
|
|
|
2478
3335
|
provider.issuer.replace(/\/$/, "") + "/.well-known/openid-configuration"
|
|
2479
3336
|
);
|
|
2480
3337
|
const response = await fetch(discoveryUrl.toString());
|
|
2481
|
-
if (!response.ok)
|
|
3338
|
+
if (!response.ok)
|
|
3339
|
+
throw new Error("Failed to load OIDC discovery document.");
|
|
2482
3340
|
return response.json();
|
|
2483
3341
|
}
|
|
2484
3342
|
defaultCallbackPath(providerId, origin = "") {
|
|
@@ -2496,10 +3354,10 @@ var AdminAuthController = class {
|
|
|
2496
3354
|
}
|
|
2497
3355
|
}
|
|
2498
3356
|
async signState(payload) {
|
|
2499
|
-
return new
|
|
3357
|
+
return new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime("15m").sign(this.getStateSecret());
|
|
2500
3358
|
}
|
|
2501
3359
|
async verifyState(token, providerId) {
|
|
2502
|
-
const { payload } = await
|
|
3360
|
+
const { payload } = await jwtVerify(token, this.getStateSecret());
|
|
2503
3361
|
const state = payload;
|
|
2504
3362
|
if (state.providerId !== providerId) {
|
|
2505
3363
|
throw new Error("OIDC state verification failed.");
|
|
@@ -2511,7 +3369,7 @@ var AdminAuthController = class {
|
|
|
2511
3369
|
if (!secret) {
|
|
2512
3370
|
throw new Error("[dyrected/core] DYRECTED_JWT_SECRET is not set.");
|
|
2513
3371
|
}
|
|
2514
|
-
return new
|
|
3372
|
+
return new TextEncoder().encode(secret);
|
|
2515
3373
|
}
|
|
2516
3374
|
normalizeReturnTo(returnTo, c) {
|
|
2517
3375
|
if (returnTo) return returnTo;
|
|
@@ -2521,12 +3379,12 @@ var AdminAuthController = class {
|
|
|
2521
3379
|
};
|
|
2522
3380
|
|
|
2523
3381
|
// src/controllers/preview.controller.ts
|
|
2524
|
-
import { SignJWT as
|
|
2525
|
-
import { TextEncoder as
|
|
3382
|
+
import { SignJWT as SignJWT2, jwtVerify as jwtVerify2 } from "jose";
|
|
3383
|
+
import { TextEncoder as TextEncoder2 } from "util";
|
|
2526
3384
|
var PreviewController = class {
|
|
2527
3385
|
getSecret() {
|
|
2528
3386
|
const secret = process.env.DYRECTED_JWT_SECRET || "dyrected-preview-secret-change-me";
|
|
2529
|
-
return new
|
|
3387
|
+
return new TextEncoder2().encode(secret);
|
|
2530
3388
|
}
|
|
2531
3389
|
/**
|
|
2532
3390
|
* POST /api/preview-token
|
|
@@ -2535,9 +3393,12 @@ var PreviewController = class {
|
|
|
2535
3393
|
async createToken(c) {
|
|
2536
3394
|
const body = await c.req.json().catch(() => null);
|
|
2537
3395
|
if (!body?.collectionSlug || !body?.data) {
|
|
2538
|
-
return c.json(
|
|
3396
|
+
return c.json(
|
|
3397
|
+
{ error: true, message: "collectionSlug and data are required." },
|
|
3398
|
+
400
|
|
3399
|
+
);
|
|
2539
3400
|
}
|
|
2540
|
-
const token = await new
|
|
3401
|
+
const token = await new SignJWT2({
|
|
2541
3402
|
collectionSlug: body.collectionSlug,
|
|
2542
3403
|
documentId: body.documentId,
|
|
2543
3404
|
data: body.data
|
|
@@ -2552,13 +3413,19 @@ var PreviewController = class {
|
|
|
2552
3413
|
async getData(c) {
|
|
2553
3414
|
const token = c.req.query("token");
|
|
2554
3415
|
if (!token) {
|
|
2555
|
-
return c.json(
|
|
3416
|
+
return c.json(
|
|
3417
|
+
{ error: true, message: "token query parameter is required." },
|
|
3418
|
+
400
|
|
3419
|
+
);
|
|
2556
3420
|
}
|
|
2557
3421
|
try {
|
|
2558
|
-
const { payload } = await
|
|
3422
|
+
const { payload } = await jwtVerify2(token, this.getSecret());
|
|
2559
3423
|
return c.json(payload);
|
|
2560
|
-
} catch (
|
|
2561
|
-
return c.json(
|
|
3424
|
+
} catch (_err) {
|
|
3425
|
+
return c.json(
|
|
3426
|
+
{ error: true, message: "Invalid or expired preview token." },
|
|
3427
|
+
401
|
|
3428
|
+
);
|
|
2562
3429
|
}
|
|
2563
3430
|
}
|
|
2564
3431
|
};
|
|
@@ -2634,9 +3501,15 @@ var AuditController = class {
|
|
|
2634
3501
|
if (!access.allowed) {
|
|
2635
3502
|
return { denied: true, where: {} };
|
|
2636
3503
|
}
|
|
2637
|
-
let where = {
|
|
3504
|
+
let where = {
|
|
3505
|
+
collection: { equals: collection.slug }
|
|
3506
|
+
};
|
|
2638
3507
|
if (access.constraint) {
|
|
2639
|
-
const ids = await this.collectAccessibleDocumentIds(
|
|
3508
|
+
const ids = await this.collectAccessibleDocumentIds(
|
|
3509
|
+
config.db,
|
|
3510
|
+
collection,
|
|
3511
|
+
access.constraint
|
|
3512
|
+
);
|
|
2640
3513
|
where = mergeWhereConstraint(where, { documentId: { in: ids } });
|
|
2641
3514
|
}
|
|
2642
3515
|
return { denied: false, where };
|
|
@@ -2649,8 +3522,11 @@ var AuditController = class {
|
|
|
2649
3522
|
}
|
|
2650
3523
|
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
2651
3524
|
if (config.onSchemaFetch && siteId) {
|
|
2652
|
-
const
|
|
2653
|
-
|
|
3525
|
+
const requestConfig = mergeDynamicConfig(
|
|
3526
|
+
config,
|
|
3527
|
+
await config.onSchemaFetch(siteId)
|
|
3528
|
+
);
|
|
3529
|
+
for (const collection of requestConfig.collections) {
|
|
2654
3530
|
if (!collections.has(collection.slug)) {
|
|
2655
3531
|
collections.set(collection.slug, collection);
|
|
2656
3532
|
}
|
|
@@ -2661,11 +3537,18 @@ var AuditController = class {
|
|
|
2661
3537
|
async findForCollection(c, collection) {
|
|
2662
3538
|
const config = c.get("config");
|
|
2663
3539
|
if (!config.db) return c.json({ message: "Database not configured" }, 500);
|
|
2664
|
-
if (!collection.audit)
|
|
3540
|
+
if (!collection.audit)
|
|
3541
|
+
return c.json(
|
|
3542
|
+
{ message: "Audit is not enabled for this collection" },
|
|
3543
|
+
404
|
|
3544
|
+
);
|
|
2665
3545
|
const query = this.parseQuery(c);
|
|
2666
3546
|
const scope = await this.buildCollectionAuditScope(c, collection);
|
|
2667
3547
|
if (scope.denied) {
|
|
2668
|
-
return c.json(
|
|
3548
|
+
return c.json(
|
|
3549
|
+
{ error: true, message: `Access denied: read on ${collection.slug}` },
|
|
3550
|
+
403
|
|
3551
|
+
);
|
|
2669
3552
|
}
|
|
2670
3553
|
const where = query.where ? mergeWhereConstraint(scope.where, query.where) : scope.where;
|
|
2671
3554
|
const result = await config.db.find({
|
|
@@ -2681,7 +3564,9 @@ var AuditController = class {
|
|
|
2681
3564
|
const config = c.get("config");
|
|
2682
3565
|
if (!config.db) return c.json({ message: "Database not configured" }, 500);
|
|
2683
3566
|
const query = this.parseQuery(c);
|
|
2684
|
-
const collections = (await this.getVisibleCollections(c)).filter(
|
|
3567
|
+
const collections = (await this.getVisibleCollections(c)).filter(
|
|
3568
|
+
(collection) => collection.audit
|
|
3569
|
+
);
|
|
2685
3570
|
const scopes = [];
|
|
2686
3571
|
for (const collection of collections) {
|
|
2687
3572
|
const scope = await this.buildCollectionAuditScope(c, collection);
|
|
@@ -2712,8 +3597,7 @@ function getBearerToken(c) {
|
|
|
2712
3597
|
const authHeader = c.req.header("Authorization");
|
|
2713
3598
|
return authHeader?.replace(/^Bearer\s+/i, "") || void 0;
|
|
2714
3599
|
}
|
|
2715
|
-
async function resolveUser(
|
|
2716
|
-
const payload = await verifyCollectionToken(token);
|
|
3600
|
+
async function resolveUser(payload, config) {
|
|
2717
3601
|
if (payload.purpose) {
|
|
2718
3602
|
return payload;
|
|
2719
3603
|
}
|
|
@@ -2728,7 +3612,12 @@ async function resolveUser(token, config) {
|
|
|
2728
3612
|
id: payload.sub
|
|
2729
3613
|
});
|
|
2730
3614
|
} catch (err) {
|
|
2731
|
-
|
|
3615
|
+
getConfigLogger(config, "auth").error({
|
|
3616
|
+
err,
|
|
3617
|
+
msg: "Failed to hydrate user from token",
|
|
3618
|
+
collection: payload.collection,
|
|
3619
|
+
userId: payload.sub
|
|
3620
|
+
});
|
|
2732
3621
|
return payload;
|
|
2733
3622
|
}
|
|
2734
3623
|
if (!doc) {
|
|
@@ -2742,6 +3631,18 @@ async function resolveUser(token, config) {
|
|
|
2742
3631
|
collection: payload.collection
|
|
2743
3632
|
};
|
|
2744
3633
|
}
|
|
3634
|
+
async function resolveAuthenticatedRequest(token, config, clientIp) {
|
|
3635
|
+
const payload = await verifyCollectionToken(token);
|
|
3636
|
+
if (payload.sid && config?.db) {
|
|
3637
|
+
const session = await getAuthSession(config, payload.sid);
|
|
3638
|
+
if (!isAuthSessionActive(session) || session.userId !== payload.sub || session.collection !== payload.collection) {
|
|
3639
|
+
throw new Error("Invalid or expired session.");
|
|
3640
|
+
}
|
|
3641
|
+
await touchAuthSession(config, payload.sid, { ip: clientIp });
|
|
3642
|
+
}
|
|
3643
|
+
const user = await resolveUser(payload, config);
|
|
3644
|
+
return { user, payload };
|
|
3645
|
+
}
|
|
2745
3646
|
function requireAuth(config) {
|
|
2746
3647
|
return async (c, next) => {
|
|
2747
3648
|
if (c.get("user")) {
|
|
@@ -2749,18 +3650,42 @@ function requireAuth(config) {
|
|
|
2749
3650
|
}
|
|
2750
3651
|
const token = getBearerToken(c);
|
|
2751
3652
|
if (!token) {
|
|
3653
|
+
c.get("observability")?.recordAuthFailure({
|
|
3654
|
+
reason: "missing_token",
|
|
3655
|
+
path: c.req.path
|
|
3656
|
+
});
|
|
2752
3657
|
return c.json({ error: true, message: "Authentication required." }, 401);
|
|
2753
3658
|
}
|
|
2754
3659
|
let user;
|
|
3660
|
+
let payload;
|
|
2755
3661
|
try {
|
|
2756
|
-
|
|
2757
|
-
|
|
3662
|
+
const resolved = await resolveAuthenticatedRequest(
|
|
3663
|
+
token,
|
|
3664
|
+
config ?? c.get("config"),
|
|
3665
|
+
c.get("clientIp")
|
|
3666
|
+
);
|
|
3667
|
+
user = resolved.user;
|
|
3668
|
+
payload = resolved.payload;
|
|
3669
|
+
} catch (err) {
|
|
3670
|
+
c.get("observability")?.recordAuthFailure({
|
|
3671
|
+
reason: "invalid_token",
|
|
3672
|
+
path: c.req.path
|
|
3673
|
+
});
|
|
3674
|
+
getRequestLogger(c, "auth").warn({
|
|
3675
|
+
err,
|
|
3676
|
+
msg: "Rejected invalid or expired token"
|
|
3677
|
+
});
|
|
2758
3678
|
return c.json({ error: true, message: "Invalid or expired token." }, 401);
|
|
2759
3679
|
}
|
|
2760
3680
|
if (!user) {
|
|
3681
|
+
c.get("observability")?.recordAuthFailure({
|
|
3682
|
+
reason: "missing_user",
|
|
3683
|
+
path: c.req.path
|
|
3684
|
+
});
|
|
2761
3685
|
return c.json({ error: true, message: "Invalid or expired token." }, 401);
|
|
2762
3686
|
}
|
|
2763
3687
|
c.set("user", user);
|
|
3688
|
+
c.set("authTokenPayload", payload);
|
|
2764
3689
|
await next();
|
|
2765
3690
|
};
|
|
2766
3691
|
}
|
|
@@ -2772,10 +3697,16 @@ function optionalAuth(config) {
|
|
|
2772
3697
|
const token = getBearerToken(c);
|
|
2773
3698
|
if (token) {
|
|
2774
3699
|
try {
|
|
2775
|
-
const
|
|
3700
|
+
const resolved = await resolveAuthenticatedRequest(
|
|
3701
|
+
token,
|
|
3702
|
+
config ?? c.get("config"),
|
|
3703
|
+
c.get("clientIp")
|
|
3704
|
+
);
|
|
3705
|
+
const user = resolved.user;
|
|
2776
3706
|
if (user) {
|
|
2777
3707
|
c.set("user", user);
|
|
2778
3708
|
}
|
|
3709
|
+
c.set("authTokenPayload", resolved.payload);
|
|
2779
3710
|
} catch {
|
|
2780
3711
|
}
|
|
2781
3712
|
}
|
|
@@ -2850,7 +3781,10 @@ function accessGate(config, target, action) {
|
|
|
2850
3781
|
req: toHookRequestContext(c.req)
|
|
2851
3782
|
});
|
|
2852
3783
|
if (!allowed) {
|
|
2853
|
-
return c.json(
|
|
3784
|
+
return c.json(
|
|
3785
|
+
{ error: true, message: `Access denied: ${action} on ${target.slug}` },
|
|
3786
|
+
403
|
|
3787
|
+
);
|
|
2854
3788
|
}
|
|
2855
3789
|
await next();
|
|
2856
3790
|
};
|
|
@@ -2874,228 +3808,276 @@ function serializeFieldForApi(f) {
|
|
|
2874
3808
|
if (serialized.fields) {
|
|
2875
3809
|
serialized.fields = serialized.fields.map(serializeFieldForApi);
|
|
2876
3810
|
}
|
|
2877
|
-
if (serialized.blocks) {
|
|
3811
|
+
if (serialized.blocks && !serialized.blockReferences?.length) {
|
|
2878
3812
|
serialized.blocks = serialized.blocks.map((b) => ({
|
|
2879
3813
|
...b,
|
|
2880
3814
|
fields: b.fields?.map(serializeFieldForApi)
|
|
2881
3815
|
}));
|
|
3816
|
+
} else if (serialized.blockReferences?.length) {
|
|
3817
|
+
delete serialized.blocks;
|
|
2882
3818
|
}
|
|
2883
3819
|
return serialized;
|
|
2884
3820
|
}
|
|
3821
|
+
function serializeBlockForApi(block) {
|
|
3822
|
+
return {
|
|
3823
|
+
...block,
|
|
3824
|
+
fields: block.fields?.map(serializeFieldForApi)
|
|
3825
|
+
};
|
|
3826
|
+
}
|
|
2885
3827
|
function registerRoutes(app, config) {
|
|
2886
3828
|
const optionsCache = /* @__PURE__ */ new Map();
|
|
2887
3829
|
app.get("/api/schemas", optionalAuth(config), async (c) => {
|
|
2888
3830
|
const siteId = c.req.header("X-Site-Id");
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
const dynamic = await config.onSchemaFetch(siteId);
|
|
2893
|
-
if (dynamic.collections) collections = [...collections, ...dynamic.collections];
|
|
2894
|
-
if (dynamic.globals) globals = [...globals, ...dynamic.globals];
|
|
2895
|
-
if (dynamic.admin) {
|
|
2896
|
-
config.admin = { ...config.admin, ...dynamic.admin };
|
|
2897
|
-
}
|
|
2898
|
-
if (dynamic.adminAuth) {
|
|
2899
|
-
config.adminAuth = { ...config.adminAuth, ...dynamic.adminAuth };
|
|
2900
|
-
}
|
|
2901
|
-
}
|
|
3831
|
+
const requestConfig = siteId && config.onSchemaFetch ? mergeDynamicConfig(config, await config.onSchemaFetch(siteId)) : config;
|
|
3832
|
+
const collections = [...requestConfig.collections];
|
|
3833
|
+
const globals = [...requestConfig.globals];
|
|
2902
3834
|
const user = c.get("user");
|
|
2903
3835
|
const accessArgs = { user, req: toHookRequestContext(c.req) };
|
|
2904
3836
|
const serializeAccess = async (access) => {
|
|
2905
3837
|
if (typeof access === "string") return access;
|
|
2906
3838
|
if (typeof access === "boolean") return access;
|
|
2907
3839
|
if (access && typeof access === "object" && typeof access.policy === "string") {
|
|
2908
|
-
const policy =
|
|
2909
|
-
if (typeof policy === "string" || typeof policy === "boolean")
|
|
3840
|
+
const policy = requestConfig.accessPolicies?.[access.policy];
|
|
3841
|
+
if (typeof policy === "string" || typeof policy === "boolean")
|
|
3842
|
+
return policy;
|
|
2910
3843
|
}
|
|
2911
|
-
return resolveBooleanAccess(
|
|
3844
|
+
return resolveBooleanAccess(requestConfig, access, accessArgs);
|
|
2912
3845
|
};
|
|
2913
|
-
const filteredCollections = await Promise.all(
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
update: await serializeAccess(col.access?.update),
|
|
2920
|
-
delete: await serializeAccess(col.access?.delete)
|
|
2921
|
-
},
|
|
2922
|
-
fields: await Promise.all(col.fields.map(serializeFieldForApi).map(async (f) => ({
|
|
2923
|
-
name: f.name,
|
|
2924
|
-
type: f.type,
|
|
2925
|
-
label: f.label,
|
|
2926
|
-
required: f.required,
|
|
2927
|
-
defaultValue: f.defaultValue,
|
|
2928
|
-
options: f.options,
|
|
2929
|
-
relationTo: f.relationTo,
|
|
2930
|
-
hasMany: f.hasMany,
|
|
2931
|
-
fields: f.fields,
|
|
2932
|
-
blocks: f.blocks,
|
|
2933
|
-
collection: f.collection,
|
|
2934
|
-
on: f.on,
|
|
2935
|
-
limit: f.limit,
|
|
2936
|
-
admin: f.admin,
|
|
3846
|
+
const filteredCollections = await Promise.all(
|
|
3847
|
+
collections.filter(
|
|
3848
|
+
(col) => !siteId || col.shared || !col.siteId || col.siteId === siteId
|
|
3849
|
+
).map(async (col) => ({
|
|
3850
|
+
slug: col.slug,
|
|
3851
|
+
labels: col.labels,
|
|
2937
3852
|
access: {
|
|
2938
|
-
read: await serializeAccess(
|
|
2939
|
-
create: await serializeAccess(
|
|
2940
|
-
update: await serializeAccess(
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3853
|
+
read: await serializeAccess(col.access?.read),
|
|
3854
|
+
create: await serializeAccess(col.access?.create),
|
|
3855
|
+
update: await serializeAccess(col.access?.update),
|
|
3856
|
+
delete: await serializeAccess(col.access?.delete)
|
|
3857
|
+
},
|
|
3858
|
+
fields: await Promise.all(
|
|
3859
|
+
col.fields.map(serializeFieldForApi).map(async (f) => ({
|
|
3860
|
+
name: f.name,
|
|
3861
|
+
type: f.type,
|
|
3862
|
+
label: f.label,
|
|
3863
|
+
required: f.required,
|
|
3864
|
+
defaultValue: f.defaultValue,
|
|
3865
|
+
options: f.options,
|
|
3866
|
+
relationTo: f.relationTo,
|
|
3867
|
+
hasMany: f.hasMany,
|
|
3868
|
+
fields: f.fields,
|
|
3869
|
+
blocks: f.blocks,
|
|
3870
|
+
blockReferences: f.blockReferences,
|
|
3871
|
+
collection: f.collection,
|
|
3872
|
+
on: f.on,
|
|
3873
|
+
limit: f.limit,
|
|
3874
|
+
admin: f.admin,
|
|
3875
|
+
access: {
|
|
3876
|
+
read: await serializeAccess(f.access?.read),
|
|
3877
|
+
create: await serializeAccess(f.access?.create),
|
|
3878
|
+
update: await serializeAccess(f.access?.update)
|
|
3879
|
+
}
|
|
3880
|
+
}))
|
|
3881
|
+
),
|
|
3882
|
+
upload: !!col.upload,
|
|
3883
|
+
auth: !!col.auth,
|
|
3884
|
+
audit: !!col.audit,
|
|
3885
|
+
drafts: !!col.drafts,
|
|
3886
|
+
admin: col.admin,
|
|
3887
|
+
workflow: col.workflow ? {
|
|
3888
|
+
initialState: col.workflow.initialState,
|
|
3889
|
+
draftState: col.workflow.draftState,
|
|
3890
|
+
states: col.workflow.states,
|
|
3891
|
+
transitions: col.workflow.transitions.map((t) => ({
|
|
3892
|
+
name: t.name,
|
|
3893
|
+
label: t.label,
|
|
3894
|
+
from: t.from,
|
|
3895
|
+
to: t.to,
|
|
3896
|
+
requiredCapabilities: t.requiredCapabilities,
|
|
3897
|
+
requireComment: t.requireComment,
|
|
3898
|
+
unpublish: t.unpublish
|
|
3899
|
+
})),
|
|
3900
|
+
roles: col.workflow.roles
|
|
3901
|
+
} : void 0
|
|
3902
|
+
}))
|
|
3903
|
+
);
|
|
3904
|
+
const filteredGlobals = await Promise.all(
|
|
3905
|
+
globals.filter(
|
|
3906
|
+
(glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId
|
|
3907
|
+
).map(async (glb) => ({
|
|
3908
|
+
slug: glb.slug,
|
|
3909
|
+
label: glb.label,
|
|
2986
3910
|
access: {
|
|
2987
|
-
read: await serializeAccess(
|
|
2988
|
-
update: await serializeAccess(
|
|
2989
|
-
}
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
3911
|
+
read: await serializeAccess(glb.access?.read),
|
|
3912
|
+
update: await serializeAccess(glb.access?.update)
|
|
3913
|
+
},
|
|
3914
|
+
fields: await Promise.all(
|
|
3915
|
+
glb.fields.map(serializeFieldForApi).map(async (f) => ({
|
|
3916
|
+
name: f.name,
|
|
3917
|
+
type: f.type,
|
|
3918
|
+
label: f.label,
|
|
3919
|
+
required: f.required,
|
|
3920
|
+
defaultValue: f.defaultValue,
|
|
3921
|
+
options: f.options,
|
|
3922
|
+
relationTo: f.relationTo,
|
|
3923
|
+
hasMany: f.hasMany,
|
|
3924
|
+
fields: f.fields,
|
|
3925
|
+
blocks: f.blocks,
|
|
3926
|
+
blockReferences: f.blockReferences,
|
|
3927
|
+
collection: f.collection,
|
|
3928
|
+
on: f.on,
|
|
3929
|
+
limit: f.limit,
|
|
3930
|
+
admin: f.admin,
|
|
3931
|
+
access: {
|
|
3932
|
+
read: await serializeAccess(f.access?.read),
|
|
3933
|
+
update: await serializeAccess(f.access?.update)
|
|
3934
|
+
}
|
|
3935
|
+
}))
|
|
3936
|
+
),
|
|
3937
|
+
admin: glb.admin
|
|
3938
|
+
}))
|
|
3939
|
+
);
|
|
2993
3940
|
return c.json({
|
|
3941
|
+
blocks: requestConfig.blocks?.map(serializeBlockForApi),
|
|
2994
3942
|
collections: filteredCollections,
|
|
2995
3943
|
globals: filteredGlobals,
|
|
2996
|
-
admin:
|
|
2997
|
-
adminAuth: getPublicAdminAuthConfig(
|
|
3944
|
+
admin: requestConfig.admin || {},
|
|
3945
|
+
adminAuth: getPublicAdminAuthConfig(requestConfig.adminAuth, collections)
|
|
2998
3946
|
});
|
|
2999
3947
|
});
|
|
3000
|
-
app.get(
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
const
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3948
|
+
app.get(
|
|
3949
|
+
"/api/dyrected/options/:collection/:field",
|
|
3950
|
+
optionalAuth(config),
|
|
3951
|
+
async (c) => {
|
|
3952
|
+
const { collection: colSlug, field: fieldName } = c.req.param();
|
|
3953
|
+
const siteId = c.req.header("X-Site-Id");
|
|
3954
|
+
const requestConfig = siteId && config.onSchemaFetch ? mergeDynamicConfig(config, await config.onSchemaFetch(siteId)) : config;
|
|
3955
|
+
let collections = [...requestConfig.collections];
|
|
3956
|
+
const user = c.get("user");
|
|
3957
|
+
let collection = collections.find((col) => col.slug === colSlug);
|
|
3958
|
+
let field;
|
|
3959
|
+
if (collection) {
|
|
3960
|
+
const accessExpr = collection.access?.read;
|
|
3961
|
+
if (accessExpr !== void 0 && accessExpr !== null) {
|
|
3962
|
+
const allowed = await resolveBooleanAccess(config, accessExpr, {
|
|
3963
|
+
user,
|
|
3964
|
+
req: toHookRequestContext(c.req)
|
|
3965
|
+
});
|
|
3966
|
+
if (!allowed) {
|
|
3967
|
+
return c.json(
|
|
3968
|
+
{ error: true, message: `Access denied: read on ${colSlug}` },
|
|
3969
|
+
403
|
|
3970
|
+
);
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
field = collection.fields.find((f) => f.name === fieldName);
|
|
3974
|
+
} else {
|
|
3975
|
+
const globals = [...requestConfig.globals];
|
|
3976
|
+
const glb = globals.find((g) => g.slug === colSlug);
|
|
3977
|
+
if (!glb) {
|
|
3978
|
+
return c.json(
|
|
3979
|
+
{
|
|
3980
|
+
error: true,
|
|
3981
|
+
message: `${colSlug} not found as collection or global`
|
|
3982
|
+
},
|
|
3983
|
+
404
|
|
3984
|
+
);
|
|
3020
3985
|
}
|
|
3986
|
+
const accessExpr = glb.access?.read;
|
|
3987
|
+
if (accessExpr !== void 0 && accessExpr !== null) {
|
|
3988
|
+
const allowed = await resolveBooleanAccess(config, accessExpr, {
|
|
3989
|
+
user,
|
|
3990
|
+
req: toHookRequestContext(c.req)
|
|
3991
|
+
});
|
|
3992
|
+
if (!allowed) {
|
|
3993
|
+
return c.json(
|
|
3994
|
+
{
|
|
3995
|
+
error: true,
|
|
3996
|
+
message: `Access denied: read on global ${colSlug}`
|
|
3997
|
+
},
|
|
3998
|
+
403
|
|
3999
|
+
);
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
field = glb.fields.find((f) => f.name === fieldName);
|
|
3021
4003
|
}
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
4004
|
+
if (!field) {
|
|
4005
|
+
return c.json(
|
|
4006
|
+
{
|
|
4007
|
+
error: true,
|
|
4008
|
+
message: `Field ${fieldName} not found in ${colSlug}`
|
|
4009
|
+
},
|
|
4010
|
+
404
|
|
4011
|
+
);
|
|
4012
|
+
}
|
|
4013
|
+
let resolver;
|
|
4014
|
+
let cacheTTL;
|
|
4015
|
+
if (typeof field.options === "function") {
|
|
4016
|
+
resolver = field.options;
|
|
4017
|
+
} else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
|
|
4018
|
+
resolver = field.options.resolve;
|
|
4019
|
+
cacheTTL = field.options.cacheTTL;
|
|
4020
|
+
}
|
|
4021
|
+
if (!resolver) {
|
|
4022
|
+
return c.json(
|
|
4023
|
+
{
|
|
4024
|
+
error: true,
|
|
4025
|
+
message: `Field ${fieldName} in ${colSlug} is not dynamic`
|
|
4026
|
+
},
|
|
4027
|
+
400
|
|
4028
|
+
);
|
|
4029
|
+
}
|
|
4030
|
+
try {
|
|
4031
|
+
const db = c.get("db") || config.db;
|
|
4032
|
+
const queryParams = c.req.query();
|
|
4033
|
+
const reqContext = {
|
|
4034
|
+
query: queryParams,
|
|
4035
|
+
headers: c.req.header(),
|
|
4036
|
+
raw: c.req.raw
|
|
4037
|
+
};
|
|
4038
|
+
const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
|
|
4039
|
+
let cacheKey = "";
|
|
4040
|
+
if (shouldCache) {
|
|
4041
|
+
cacheKey = JSON.stringify([
|
|
4042
|
+
siteId ?? "",
|
|
4043
|
+
colSlug,
|
|
4044
|
+
fieldName,
|
|
4045
|
+
user?.id ?? null,
|
|
4046
|
+
queryParams
|
|
4047
|
+
]);
|
|
4048
|
+
const hit = optionsCache.get(cacheKey);
|
|
4049
|
+
if (hit && hit.expires > Date.now()) {
|
|
4050
|
+
return c.json(hit.value);
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
const result = await resolver({
|
|
4054
|
+
db,
|
|
3036
4055
|
user,
|
|
3037
|
-
req:
|
|
4056
|
+
req: reqContext
|
|
3038
4057
|
});
|
|
3039
|
-
if (
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
}
|
|
3045
|
-
if (!field) {
|
|
3046
|
-
return c.json({ error: true, message: `Field ${fieldName} not found in ${colSlug}` }, 404);
|
|
3047
|
-
}
|
|
3048
|
-
let resolver;
|
|
3049
|
-
let cacheTTL;
|
|
3050
|
-
if (typeof field.options === "function") {
|
|
3051
|
-
resolver = field.options;
|
|
3052
|
-
} else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
|
|
3053
|
-
resolver = field.options.resolve;
|
|
3054
|
-
cacheTTL = field.options.cacheTTL;
|
|
3055
|
-
}
|
|
3056
|
-
if (!resolver) {
|
|
3057
|
-
return c.json({ error: true, message: `Field ${fieldName} in ${colSlug} is not dynamic` }, 400);
|
|
3058
|
-
}
|
|
3059
|
-
try {
|
|
3060
|
-
const db = c.get("db") || config.db;
|
|
3061
|
-
const queryParams = c.req.query();
|
|
3062
|
-
const reqContext = {
|
|
3063
|
-
query: queryParams,
|
|
3064
|
-
headers: c.req.header(),
|
|
3065
|
-
raw: c.req.raw
|
|
3066
|
-
};
|
|
3067
|
-
const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
|
|
3068
|
-
let cacheKey = "";
|
|
3069
|
-
if (shouldCache) {
|
|
3070
|
-
cacheKey = JSON.stringify([
|
|
3071
|
-
siteId ?? "",
|
|
3072
|
-
colSlug,
|
|
3073
|
-
fieldName,
|
|
3074
|
-
user?.id ?? null,
|
|
3075
|
-
queryParams
|
|
3076
|
-
]);
|
|
3077
|
-
const hit = optionsCache.get(cacheKey);
|
|
3078
|
-
if (hit && hit.expires > Date.now()) {
|
|
3079
|
-
return c.json(hit.value);
|
|
4058
|
+
if (shouldCache) {
|
|
4059
|
+
optionsCache.set(cacheKey, {
|
|
4060
|
+
expires: Date.now() + cacheTTL * 1e3,
|
|
4061
|
+
value: result
|
|
4062
|
+
});
|
|
3080
4063
|
}
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
if (shouldCache) {
|
|
3088
|
-
optionsCache.set(cacheKey, {
|
|
3089
|
-
expires: Date.now() + cacheTTL * 1e3,
|
|
3090
|
-
value: result
|
|
4064
|
+
return c.json(result);
|
|
4065
|
+
} catch (err) {
|
|
4066
|
+
getRequestLogger(c, "router").error({
|
|
4067
|
+
err,
|
|
4068
|
+
msg: "Failed to resolve dynamic field options",
|
|
4069
|
+
fieldName
|
|
3091
4070
|
});
|
|
4071
|
+
return c.json(
|
|
4072
|
+
{
|
|
4073
|
+
error: true,
|
|
4074
|
+
message: err.message || "Failed to resolve dynamic options"
|
|
4075
|
+
},
|
|
4076
|
+
500
|
|
4077
|
+
);
|
|
3092
4078
|
}
|
|
3093
|
-
return c.json(result);
|
|
3094
|
-
} catch (err) {
|
|
3095
|
-
console.error(`[dyrected/core] Failed to resolve dynamic options for field ${fieldName}:`, err);
|
|
3096
|
-
return c.json({ error: true, message: err.message || "Failed to resolve dynamic options" }, 500);
|
|
3097
4079
|
}
|
|
3098
|
-
|
|
4080
|
+
);
|
|
3099
4081
|
app.get("/api/openapi.json", (c) => {
|
|
3100
4082
|
return c.json(generateOpenApi(config));
|
|
3101
4083
|
});
|
|
@@ -3108,10 +4090,18 @@ function registerRoutes(app, config) {
|
|
|
3108
4090
|
const key = c.req.param("key");
|
|
3109
4091
|
const scope = c.req.query("scope");
|
|
3110
4092
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
3111
|
-
if (!user?.collection || !user.sub)
|
|
3112
|
-
|
|
4093
|
+
if (!user?.collection || !user.sub)
|
|
4094
|
+
return c.json({ error: true, message: "Authentication required." }, 401);
|
|
4095
|
+
if (!key)
|
|
4096
|
+
return c.json(
|
|
4097
|
+
{ error: true, message: "Preference key is required." },
|
|
4098
|
+
400
|
|
4099
|
+
);
|
|
3113
4100
|
const getGlobalPreference = async () => {
|
|
3114
|
-
const globalDoc = await db.findOne({
|
|
4101
|
+
const globalDoc = await db.findOne({
|
|
4102
|
+
collection: "__global_preferences",
|
|
4103
|
+
id: key
|
|
4104
|
+
});
|
|
3115
4105
|
return globalDoc ? globalDoc.value : null;
|
|
3116
4106
|
};
|
|
3117
4107
|
if (scope === "global") {
|
|
@@ -3133,15 +4123,29 @@ function registerRoutes(app, config) {
|
|
|
3133
4123
|
const key = c.req.param("key");
|
|
3134
4124
|
const scope = c.req.query("scope");
|
|
3135
4125
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
3136
|
-
if (!user?.collection || !user.sub)
|
|
3137
|
-
|
|
4126
|
+
if (!user?.collection || !user.sub)
|
|
4127
|
+
return c.json({ error: true, message: "Authentication required." }, 401);
|
|
4128
|
+
if (!key)
|
|
4129
|
+
return c.json(
|
|
4130
|
+
{ error: true, message: "Preference key is required." },
|
|
4131
|
+
400
|
|
4132
|
+
);
|
|
3138
4133
|
const body = await c.req.json().catch(() => ({}));
|
|
3139
4134
|
if (scope === "global") {
|
|
3140
4135
|
const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
|
|
3141
4136
|
if (!isAdminUser) {
|
|
3142
|
-
return c.json(
|
|
4137
|
+
return c.json(
|
|
4138
|
+
{
|
|
4139
|
+
error: true,
|
|
4140
|
+
message: "Only administrators can save global preferences."
|
|
4141
|
+
},
|
|
4142
|
+
403
|
|
4143
|
+
);
|
|
3143
4144
|
}
|
|
3144
|
-
const existing = await db.findOne({
|
|
4145
|
+
const existing = await db.findOne({
|
|
4146
|
+
collection: "__global_preferences",
|
|
4147
|
+
id: key
|
|
4148
|
+
});
|
|
3145
4149
|
if (existing) {
|
|
3146
4150
|
await db.update({
|
|
3147
4151
|
collection: "__global_preferences",
|
|
@@ -3173,12 +4177,23 @@ function registerRoutes(app, config) {
|
|
|
3173
4177
|
const key = c.req.param("key");
|
|
3174
4178
|
const scope = c.req.query("scope");
|
|
3175
4179
|
if (!db) return c.json({ message: "Database not configured" }, 500);
|
|
3176
|
-
if (!user?.collection || !user.sub)
|
|
3177
|
-
|
|
4180
|
+
if (!user?.collection || !user.sub)
|
|
4181
|
+
return c.json({ error: true, message: "Authentication required." }, 401);
|
|
4182
|
+
if (!key)
|
|
4183
|
+
return c.json(
|
|
4184
|
+
{ error: true, message: "Preference key is required." },
|
|
4185
|
+
400
|
|
4186
|
+
);
|
|
3178
4187
|
if (scope === "global") {
|
|
3179
4188
|
const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
|
|
3180
4189
|
if (!isAdminUser) {
|
|
3181
|
-
return c.json(
|
|
4190
|
+
return c.json(
|
|
4191
|
+
{
|
|
4192
|
+
error: true,
|
|
4193
|
+
message: "Only administrators can delete global preferences."
|
|
4194
|
+
},
|
|
4195
|
+
403
|
|
4196
|
+
);
|
|
3182
4197
|
}
|
|
3183
4198
|
await db.delete({ collection: "__global_preferences", id: key });
|
|
3184
4199
|
return c.json({ success: true });
|
|
@@ -3207,17 +4222,41 @@ function registerRoutes(app, config) {
|
|
|
3207
4222
|
for (const col of uploadCollections) {
|
|
3208
4223
|
const mediaController = new MediaController(col.slug);
|
|
3209
4224
|
const prefix = `/api/collections/${col.slug}`;
|
|
3210
|
-
app.get(
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
4225
|
+
app.get(
|
|
4226
|
+
`${prefix}/media`,
|
|
4227
|
+
accessGate(config, col, "read"),
|
|
4228
|
+
(c) => mediaController.find(c)
|
|
4229
|
+
);
|
|
4230
|
+
app.get(
|
|
4231
|
+
`${prefix}/media/:filename{.+$}`,
|
|
4232
|
+
(c) => mediaController.serve(c)
|
|
4233
|
+
);
|
|
4234
|
+
app.post(
|
|
4235
|
+
`${prefix}/media`,
|
|
4236
|
+
accessGate(config, col, "create"),
|
|
4237
|
+
(c) => mediaController.upload(c)
|
|
4238
|
+
);
|
|
4239
|
+
app.delete(
|
|
4240
|
+
`${prefix}/media/:id`,
|
|
4241
|
+
accessGate(config, col, "delete"),
|
|
4242
|
+
(c) => mediaController.delete(c)
|
|
4243
|
+
);
|
|
3214
4244
|
}
|
|
3215
4245
|
}
|
|
3216
4246
|
const adminAuthController = new AdminAuthController(config);
|
|
3217
4247
|
app.get("/api/admin/auth/providers", (c) => adminAuthController.providers(c));
|
|
3218
|
-
app.get(
|
|
3219
|
-
|
|
3220
|
-
|
|
4248
|
+
app.get(
|
|
4249
|
+
"/api/admin/auth/:provider/start",
|
|
4250
|
+
(c) => adminAuthController.start(c)
|
|
4251
|
+
);
|
|
4252
|
+
app.get(
|
|
4253
|
+
"/api/admin/auth/:provider/callback",
|
|
4254
|
+
(c) => adminAuthController.callback(c)
|
|
4255
|
+
);
|
|
4256
|
+
app.post(
|
|
4257
|
+
"/api/admin/auth/:provider/exchange",
|
|
4258
|
+
(c) => adminAuthController.exchange(c)
|
|
4259
|
+
);
|
|
3221
4260
|
app.post("/api/admin/logout", (c) => adminAuthController.logout(c));
|
|
3222
4261
|
for (const collection of config.collections) {
|
|
3223
4262
|
if (!collection.auth) continue;
|
|
@@ -3228,10 +4267,21 @@ function registerRoutes(app, config) {
|
|
|
3228
4267
|
app.get(`${path}/init`, (c) => authController.init(c));
|
|
3229
4268
|
app.post(`${path}/first-user`, (c) => authController.registerFirstUser(c));
|
|
3230
4269
|
app.get(`${path}/me`, requireAuth(config), (c) => authController.me(c));
|
|
3231
|
-
app.post(
|
|
3232
|
-
|
|
4270
|
+
app.post(
|
|
4271
|
+
`${path}/refresh-token`,
|
|
4272
|
+
requireAuth(config),
|
|
4273
|
+
(c) => authController.refreshToken(c)
|
|
4274
|
+
);
|
|
4275
|
+
app.post(
|
|
4276
|
+
`${path}/forgot-password`,
|
|
4277
|
+
(c) => authController.forgotPassword(c)
|
|
4278
|
+
);
|
|
3233
4279
|
app.post(`${path}/reset-password`, (c) => authController.resetPassword(c));
|
|
3234
|
-
app.post(
|
|
4280
|
+
app.post(
|
|
4281
|
+
`${path}/invite`,
|
|
4282
|
+
requireAuth(config),
|
|
4283
|
+
(c) => authController.invite(c)
|
|
4284
|
+
);
|
|
3235
4285
|
app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
|
|
3236
4286
|
}
|
|
3237
4287
|
const auditController = new AuditController();
|
|
@@ -3243,18 +4293,33 @@ function registerRoutes(app, config) {
|
|
|
3243
4293
|
app.post(`${path}/media`, (c) => controller.create(c));
|
|
3244
4294
|
app.delete(`${path}/delete-many`, (c) => controller.deleteMany(c));
|
|
3245
4295
|
if (collection.audit) {
|
|
3246
|
-
app.get(
|
|
4296
|
+
app.get(
|
|
4297
|
+
`${path}/__audit`,
|
|
4298
|
+
(c) => auditController.findForCollection(c, collection)
|
|
4299
|
+
);
|
|
3247
4300
|
}
|
|
3248
4301
|
app.get(`${path}/:id`, (c) => controller.findOne(c));
|
|
3249
4302
|
app.patch(`${path}/:id`, (c) => controller.update(c));
|
|
3250
4303
|
app.delete(`${path}/:id`, (c) => controller.delete(c));
|
|
3251
4304
|
app.post(`${path}/seed`, (c) => controller.seed(c));
|
|
3252
4305
|
if (collection.auth) {
|
|
3253
|
-
app.post(
|
|
4306
|
+
app.post(
|
|
4307
|
+
`${path}/:id/change-password`,
|
|
4308
|
+
requireAuth(config),
|
|
4309
|
+
(c) => controller.changePassword(c)
|
|
4310
|
+
);
|
|
3254
4311
|
}
|
|
3255
4312
|
if (collection.workflow) {
|
|
3256
|
-
app.post(
|
|
3257
|
-
|
|
4313
|
+
app.post(
|
|
4314
|
+
`${path}/:id/transitions/:transition`,
|
|
4315
|
+
requireAuth(config),
|
|
4316
|
+
(c) => controller.transition(c)
|
|
4317
|
+
);
|
|
4318
|
+
app.get(
|
|
4319
|
+
`${path}/:id/workflow-history`,
|
|
4320
|
+
requireAuth(config),
|
|
4321
|
+
(c) => controller.workflowHistory(c)
|
|
4322
|
+
);
|
|
3258
4323
|
}
|
|
3259
4324
|
}
|
|
3260
4325
|
for (const global of config.globals) {
|
|
@@ -3265,12 +4330,16 @@ function registerRoutes(app, config) {
|
|
|
3265
4330
|
app.post(`${path}/seed`, (c) => controller.seed(c));
|
|
3266
4331
|
}
|
|
3267
4332
|
if (!process.env.DYRECTED_JWT_SECRET) {
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
);
|
|
4333
|
+
getConfigLogger(config, "router").warn({
|
|
4334
|
+
msg: "DYRECTED_JWT_SECRET is not set; token-mode live preview is signing with an insecure default"
|
|
4335
|
+
});
|
|
3271
4336
|
}
|
|
3272
4337
|
const previewController = new PreviewController();
|
|
3273
|
-
app.post(
|
|
4338
|
+
app.post(
|
|
4339
|
+
"/api/preview-token",
|
|
4340
|
+
requireAuth(config),
|
|
4341
|
+
(c) => previewController.createToken(c)
|
|
4342
|
+
);
|
|
3274
4343
|
app.get("/api/preview-data", (c) => previewController.getData(c));
|
|
3275
4344
|
app.get("/api/audit", (c) => auditController.findAll(c));
|
|
3276
4345
|
app.get("/api/collections/:slug/__audit", async (c) => {
|
|
@@ -3283,49 +4352,81 @@ function registerRoutes(app, config) {
|
|
|
3283
4352
|
if (!config2.onSchemaFetch || !siteId) {
|
|
3284
4353
|
return c.json({ message: `Collection "${slug}" not found` }, 404);
|
|
3285
4354
|
}
|
|
3286
|
-
const
|
|
3287
|
-
|
|
4355
|
+
const requestConfig = mergeDynamicConfig(
|
|
4356
|
+
config2,
|
|
4357
|
+
await config2.onSchemaFetch(siteId)
|
|
4358
|
+
);
|
|
4359
|
+
const collection = requestConfig.collections.find(
|
|
4360
|
+
(col) => col.slug === slug
|
|
4361
|
+
);
|
|
3288
4362
|
if (!collection?.audit) {
|
|
3289
|
-
return c.json(
|
|
4363
|
+
return c.json(
|
|
4364
|
+
{ message: `Collection "${slug}" not found or has no audit log` },
|
|
4365
|
+
404
|
|
4366
|
+
);
|
|
3290
4367
|
}
|
|
3291
4368
|
return auditController.findForCollection(c, collection);
|
|
3292
4369
|
});
|
|
3293
|
-
app.post(
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
4370
|
+
app.post(
|
|
4371
|
+
"/api/collections/:slug/:id/transitions/:transition",
|
|
4372
|
+
requireAuth(config),
|
|
4373
|
+
async (c) => {
|
|
4374
|
+
const slug = c.req.param("slug");
|
|
4375
|
+
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
4376
|
+
const config2 = c.get("config");
|
|
4377
|
+
if (config2.collections.some((col) => col.slug === slug)) {
|
|
4378
|
+
return c.json({ message: "Not Found" }, 404);
|
|
4379
|
+
}
|
|
4380
|
+
if (!config2.onSchemaFetch || !siteId) {
|
|
4381
|
+
return c.json({ message: `Collection "${slug}" not found` }, 404);
|
|
4382
|
+
}
|
|
4383
|
+
const requestConfig = mergeDynamicConfig(
|
|
4384
|
+
config2,
|
|
4385
|
+
await config2.onSchemaFetch(siteId)
|
|
4386
|
+
);
|
|
4387
|
+
const collection = requestConfig.collections.find(
|
|
4388
|
+
(col) => col.slug === slug
|
|
4389
|
+
);
|
|
4390
|
+
if (!collection?.workflow) {
|
|
4391
|
+
return c.json(
|
|
4392
|
+
{ message: `Collection "${slug}" not found or has no workflow` },
|
|
4393
|
+
404
|
|
4394
|
+
);
|
|
4395
|
+
}
|
|
4396
|
+
const controller = new CollectionController(collection);
|
|
4397
|
+
return controller.transition(c);
|
|
4398
|
+
}
|
|
4399
|
+
);
|
|
4400
|
+
app.get(
|
|
4401
|
+
"/api/collections/:slug/:id/workflow-history",
|
|
4402
|
+
requireAuth(config),
|
|
4403
|
+
async (c) => {
|
|
4404
|
+
const slug = c.req.param("slug");
|
|
4405
|
+
const siteId = c.req.header("X-Site-Id") || c.get("siteId");
|
|
4406
|
+
const config2 = c.get("config");
|
|
4407
|
+
if (config2.collections.some((col) => col.slug === slug)) {
|
|
4408
|
+
return c.json({ message: "Not Found" }, 404);
|
|
4409
|
+
}
|
|
4410
|
+
if (!config2.onSchemaFetch || !siteId) {
|
|
4411
|
+
return c.json({ message: `Collection "${slug}" not found` }, 404);
|
|
4412
|
+
}
|
|
4413
|
+
const requestConfig = mergeDynamicConfig(
|
|
4414
|
+
config2,
|
|
4415
|
+
await config2.onSchemaFetch(siteId)
|
|
4416
|
+
);
|
|
4417
|
+
const collection = requestConfig.collections.find(
|
|
4418
|
+
(col) => col.slug === slug
|
|
4419
|
+
);
|
|
4420
|
+
if (!collection?.workflow) {
|
|
4421
|
+
return c.json(
|
|
4422
|
+
{ message: `Collection "${slug}" not found or has no workflow` },
|
|
4423
|
+
404
|
|
4424
|
+
);
|
|
4425
|
+
}
|
|
4426
|
+
const controller = new CollectionController(collection);
|
|
4427
|
+
return controller.workflowHistory(c);
|
|
3325
4428
|
}
|
|
3326
|
-
|
|
3327
|
-
return controller.workflowHistory(c);
|
|
3328
|
-
});
|
|
4429
|
+
);
|
|
3329
4430
|
app.all("/api/collections/:slug/:id?", async (c) => {
|
|
3330
4431
|
const slug = c.req.param("slug");
|
|
3331
4432
|
const id = c.req.param("id");
|
|
@@ -3335,8 +4436,13 @@ function registerRoutes(app, config) {
|
|
|
3335
4436
|
return c.json({ message: "Method Not Allowed" }, 405);
|
|
3336
4437
|
}
|
|
3337
4438
|
if (config2.onSchemaFetch && siteId) {
|
|
3338
|
-
const
|
|
3339
|
-
|
|
4439
|
+
const requestConfig = mergeDynamicConfig(
|
|
4440
|
+
config2,
|
|
4441
|
+
await config2.onSchemaFetch(siteId)
|
|
4442
|
+
);
|
|
4443
|
+
let collection = requestConfig.collections.find(
|
|
4444
|
+
(col) => col.slug === slug
|
|
4445
|
+
);
|
|
3340
4446
|
if (!collection && slug === "media") {
|
|
3341
4447
|
collection = {
|
|
3342
4448
|
slug: "media",
|
|
@@ -3349,19 +4455,25 @@ function registerRoutes(app, config) {
|
|
|
3349
4455
|
if (collection.auth && id) {
|
|
3350
4456
|
const authController = new AuthController(collection);
|
|
3351
4457
|
const method2 = c.req.method;
|
|
3352
|
-
if (method2 === "POST" && id === "login")
|
|
3353
|
-
|
|
4458
|
+
if (method2 === "POST" && id === "login")
|
|
4459
|
+
return authController.login(c);
|
|
4460
|
+
if (method2 === "POST" && id === "logout")
|
|
4461
|
+
return authController.logout(c);
|
|
3354
4462
|
if (method2 === "GET" && id === "me") return authController.me(c);
|
|
3355
|
-
if (method2 === "POST" && id === "refresh-token")
|
|
3356
|
-
|
|
3357
|
-
if (method2 === "POST" && id === "
|
|
4463
|
+
if (method2 === "POST" && id === "refresh-token")
|
|
4464
|
+
return authController.refreshToken(c);
|
|
4465
|
+
if (method2 === "POST" && id === "forgot-password")
|
|
4466
|
+
return authController.forgotPassword(c);
|
|
4467
|
+
if (method2 === "POST" && id === "reset-password")
|
|
4468
|
+
return authController.resetPassword(c);
|
|
3358
4469
|
}
|
|
3359
4470
|
const controller = new CollectionController(collection);
|
|
3360
4471
|
const method = c.req.method;
|
|
3361
4472
|
if (id) {
|
|
3362
4473
|
if (method === "GET") return controller.findOne(c);
|
|
3363
4474
|
if (method === "PATCH") return controller.update(c);
|
|
3364
|
-
if (method === "DELETE" && id === "delete-many")
|
|
4475
|
+
if (method === "DELETE" && id === "delete-many")
|
|
4476
|
+
return controller.deleteMany(c);
|
|
3365
4477
|
if (method === "DELETE") return controller.delete(c);
|
|
3366
4478
|
if (method === "POST" && id === "media") return controller.create(c);
|
|
3367
4479
|
if (method === "POST" && id === "seed") return controller.seed(c);
|
|
@@ -3382,8 +4494,11 @@ function registerRoutes(app, config) {
|
|
|
3382
4494
|
return c.json({ message: "Method Not Allowed" }, 405);
|
|
3383
4495
|
}
|
|
3384
4496
|
if (config2.onSchemaFetch && siteId) {
|
|
3385
|
-
const
|
|
3386
|
-
|
|
4497
|
+
const requestConfig = mergeDynamicConfig(
|
|
4498
|
+
config2,
|
|
4499
|
+
await config2.onSchemaFetch(siteId)
|
|
4500
|
+
);
|
|
4501
|
+
const global = requestConfig.globals.find((glb) => glb.slug === slug);
|
|
3387
4502
|
if (global) {
|
|
3388
4503
|
const controller = new GlobalController(global);
|
|
3389
4504
|
const method = c.req.method;
|
|
@@ -3399,36 +4514,283 @@ function registerRoutes(app, config) {
|
|
|
3399
4514
|
});
|
|
3400
4515
|
}
|
|
3401
4516
|
|
|
4517
|
+
// src/network.ts
|
|
4518
|
+
var DIRECT_IP_HEADERS = [
|
|
4519
|
+
"cf-connecting-ip",
|
|
4520
|
+
"fly-client-ip",
|
|
4521
|
+
"true-client-ip",
|
|
4522
|
+
"x-real-ip",
|
|
4523
|
+
"x-client-ip",
|
|
4524
|
+
"fastly-client-ip"
|
|
4525
|
+
];
|
|
4526
|
+
function toHookRequestContext2(c) {
|
|
4527
|
+
const headers = Object.fromEntries(
|
|
4528
|
+
Object.entries(c.req.header()).map(([key, value]) => [key.toLowerCase(), value])
|
|
4529
|
+
);
|
|
4530
|
+
return {
|
|
4531
|
+
query: c.req.query(),
|
|
4532
|
+
headers,
|
|
4533
|
+
raw: c.req.raw
|
|
4534
|
+
};
|
|
4535
|
+
}
|
|
4536
|
+
function resolveClientIp(req, trustProxy = false) {
|
|
4537
|
+
for (const header of DIRECT_IP_HEADERS) {
|
|
4538
|
+
const value = normalizeIp(req.headers[header]);
|
|
4539
|
+
if (value) return value;
|
|
4540
|
+
}
|
|
4541
|
+
if (trustProxy) {
|
|
4542
|
+
const forwardedFor = pickForwardedFor(req.headers, trustProxy);
|
|
4543
|
+
if (forwardedFor) return forwardedFor;
|
|
4544
|
+
}
|
|
4545
|
+
return "127.0.0.1";
|
|
4546
|
+
}
|
|
4547
|
+
function pickForwardedFor(headers, trustProxy) {
|
|
4548
|
+
const forwardedHeader = headers["x-forwarded-for"];
|
|
4549
|
+
if (forwardedHeader) {
|
|
4550
|
+
const chain2 = forwardedHeader.split(",").map((entry) => normalizeIp(entry)).filter((entry) => !!entry);
|
|
4551
|
+
if (chain2.length > 0) {
|
|
4552
|
+
if (trustProxy === true) return chain2[0];
|
|
4553
|
+
const trustedHops2 = Math.max(1, Math.floor(trustProxy));
|
|
4554
|
+
const candidateIndex2 = Math.max(0, chain2.length - trustedHops2);
|
|
4555
|
+
return chain2[candidateIndex2] ?? chain2[0];
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
const forwarded = headers.forwarded;
|
|
4559
|
+
if (!forwarded) return null;
|
|
4560
|
+
const matches = [...forwarded.matchAll(/for=(?:"?\[?([^;\],"]+)\]?"?)/gi)];
|
|
4561
|
+
if (matches.length === 0) return null;
|
|
4562
|
+
const chain = matches.map((match) => normalizeIp(match[1])).filter((entry) => !!entry);
|
|
4563
|
+
if (chain.length === 0) return null;
|
|
4564
|
+
if (trustProxy === true) return chain[0];
|
|
4565
|
+
const trustedHops = Math.max(1, Math.floor(trustProxy));
|
|
4566
|
+
const candidateIndex = Math.max(0, chain.length - trustedHops);
|
|
4567
|
+
return chain[candidateIndex] ?? chain[0];
|
|
4568
|
+
}
|
|
4569
|
+
function normalizeIp(value) {
|
|
4570
|
+
if (!value) return null;
|
|
4571
|
+
const trimmed = value.trim();
|
|
4572
|
+
if (!trimmed || trimmed.toLowerCase() === "unknown") return null;
|
|
4573
|
+
return trimmed.replace(/^\[(.*)\]$/, "$1");
|
|
4574
|
+
}
|
|
4575
|
+
|
|
4576
|
+
// src/middleware/rate-limit.ts
|
|
4577
|
+
var DEFAULT_WINDOW_MS = 15 * 60 * 1e3;
|
|
4578
|
+
var DEFAULT_MAX_REQUESTS = 500;
|
|
4579
|
+
var DEFAULT_PATHS = ["/api"];
|
|
4580
|
+
function createRateLimitMiddleware(config) {
|
|
4581
|
+
const settings = normalizeRateLimitConfig(config.rateLimit);
|
|
4582
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
4583
|
+
return async (c, next) => {
|
|
4584
|
+
const req = toHookRequestContext2(c);
|
|
4585
|
+
const ip = resolveClientIp(req, settings.trustProxy);
|
|
4586
|
+
c.set("clientIp", ip);
|
|
4587
|
+
if (!settings.enabled) {
|
|
4588
|
+
return next();
|
|
4589
|
+
}
|
|
4590
|
+
const path = c.req.path;
|
|
4591
|
+
if (!matchesLimitedPath(path, settings.paths)) {
|
|
4592
|
+
return next();
|
|
4593
|
+
}
|
|
4594
|
+
if (settings.skip && await settings.skip({
|
|
4595
|
+
ip,
|
|
4596
|
+
path,
|
|
4597
|
+
method: c.req.method,
|
|
4598
|
+
req
|
|
4599
|
+
})) {
|
|
4600
|
+
return next();
|
|
4601
|
+
}
|
|
4602
|
+
const now = Date.now();
|
|
4603
|
+
const key = ip;
|
|
4604
|
+
const existing = buckets.get(key);
|
|
4605
|
+
const bucket = existing && existing.resetAt > now ? existing : { count: 0, resetAt: now + settings.window };
|
|
4606
|
+
bucket.count += 1;
|
|
4607
|
+
buckets.set(key, bucket);
|
|
4608
|
+
pruneExpiredBuckets(buckets, now);
|
|
4609
|
+
const remaining = Math.max(0, settings.max - bucket.count);
|
|
4610
|
+
const resetSeconds = Math.max(1, Math.ceil((bucket.resetAt - now) / 1e3));
|
|
4611
|
+
c.res.headers.set("X-RateLimit-Limit", String(settings.max));
|
|
4612
|
+
c.res.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
4613
|
+
c.res.headers.set("X-RateLimit-Reset", String(Math.ceil(bucket.resetAt / 1e3)));
|
|
4614
|
+
if (bucket.count > settings.max) {
|
|
4615
|
+
c.res.headers.set("Retry-After", String(resetSeconds));
|
|
4616
|
+
return c.json(
|
|
4617
|
+
{
|
|
4618
|
+
error: true,
|
|
4619
|
+
message: "Too many requests. Try again later.",
|
|
4620
|
+
retryAfterSeconds: resetSeconds
|
|
4621
|
+
},
|
|
4622
|
+
429
|
|
4623
|
+
);
|
|
4624
|
+
}
|
|
4625
|
+
await next();
|
|
4626
|
+
};
|
|
4627
|
+
}
|
|
4628
|
+
function normalizeRateLimitConfig(config) {
|
|
4629
|
+
return {
|
|
4630
|
+
enabled: config?.enabled ?? true,
|
|
4631
|
+
window: config?.window ?? DEFAULT_WINDOW_MS,
|
|
4632
|
+
max: config?.max ?? DEFAULT_MAX_REQUESTS,
|
|
4633
|
+
paths: config?.paths?.length ? config.paths : DEFAULT_PATHS,
|
|
4634
|
+
skip: config?.skip,
|
|
4635
|
+
trustProxy: config?.trustProxy ?? false
|
|
4636
|
+
};
|
|
4637
|
+
}
|
|
4638
|
+
function matchesLimitedPath(path, prefixes) {
|
|
4639
|
+
return prefixes.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
|
|
4640
|
+
}
|
|
4641
|
+
function pruneExpiredBuckets(buckets, now) {
|
|
4642
|
+
if (buckets.size < 1e3) return;
|
|
4643
|
+
for (const [key, bucket] of buckets) {
|
|
4644
|
+
if (bucket.resetAt <= now) {
|
|
4645
|
+
buckets.delete(key);
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
|
|
3402
4650
|
// src/app.ts
|
|
3403
4651
|
async function createDyrectedApp(rawConfig) {
|
|
3404
4652
|
const config = normalizeConfig(rawConfig);
|
|
4653
|
+
const observability = createObservabilityRuntime(config);
|
|
4654
|
+
bindObservabilityRuntime(config, observability);
|
|
4655
|
+
config.logger = observability.logger;
|
|
3405
4656
|
const app = new Hono();
|
|
3406
4657
|
if (config.db?.sync) {
|
|
3407
4658
|
await config.db.sync(config.collections, config.globals);
|
|
3408
4659
|
}
|
|
3409
4660
|
app.use("*", requestId());
|
|
3410
|
-
app.use("*", optionalAuth(config));
|
|
3411
|
-
app.use("*", async (c, next) => {
|
|
3412
|
-
const start = Date.now();
|
|
3413
|
-
await next();
|
|
3414
|
-
const ms = Date.now() - start;
|
|
3415
|
-
console.log(`[dyrected/api] ${c.req.method} ${c.req.path} ${c.res.status} - ${ms}ms`);
|
|
3416
|
-
});
|
|
3417
|
-
app.use("*", cors());
|
|
3418
4661
|
app.use("*", async (c, next) => {
|
|
3419
4662
|
c.set("config", config);
|
|
4663
|
+
c.set("observability", observability);
|
|
3420
4664
|
if (!c.get("siteId")) {
|
|
3421
4665
|
c.set("siteId", "default");
|
|
3422
4666
|
}
|
|
4667
|
+
c.set("logger", buildRequestLogger(observability, c.var, c.get("requestId")));
|
|
3423
4668
|
await next();
|
|
3424
4669
|
});
|
|
4670
|
+
app.use("*", async (c, next) => {
|
|
4671
|
+
const start = Date.now();
|
|
4672
|
+
const method = c.req.method;
|
|
4673
|
+
const path = c.req.path;
|
|
4674
|
+
const requestId2 = c.get("requestId");
|
|
4675
|
+
const observabilityConfig = observability.config;
|
|
4676
|
+
const traceSampled = shouldSampleRequest(
|
|
4677
|
+
requestId2,
|
|
4678
|
+
200,
|
|
4679
|
+
observabilityConfig.sampling,
|
|
4680
|
+
"trace"
|
|
4681
|
+
);
|
|
4682
|
+
const requestTrace = traceSampled ? createRequestSpan(observability, `${method} ${path}`, {
|
|
4683
|
+
"http.method": method,
|
|
4684
|
+
"http.route": path,
|
|
4685
|
+
requestId: requestId2,
|
|
4686
|
+
siteId: c.get("siteId"),
|
|
4687
|
+
workspaceId: c.get("workspaceId")
|
|
4688
|
+
}) : void 0;
|
|
4689
|
+
if (requestTrace) {
|
|
4690
|
+
c.set("requestTrace", requestTrace);
|
|
4691
|
+
c.set("logger", buildRequestLogger(observability, c.var, requestId2));
|
|
4692
|
+
}
|
|
4693
|
+
let bodyCapturePromise;
|
|
4694
|
+
if (observabilityConfig.requestLogging.logBodies) {
|
|
4695
|
+
const bodySampled = shouldSampleRequest(
|
|
4696
|
+
requestId2,
|
|
4697
|
+
200,
|
|
4698
|
+
observabilityConfig.sampling,
|
|
4699
|
+
"body"
|
|
4700
|
+
);
|
|
4701
|
+
if (bodySampled) {
|
|
4702
|
+
bodyCapturePromise = captureRequestBody(c.req.raw, observabilityConfig);
|
|
4703
|
+
}
|
|
4704
|
+
}
|
|
4705
|
+
let caughtError;
|
|
4706
|
+
try {
|
|
4707
|
+
await next();
|
|
4708
|
+
} catch (error) {
|
|
4709
|
+
caughtError = error;
|
|
4710
|
+
throw error;
|
|
4711
|
+
} finally {
|
|
4712
|
+
const durationMs = Date.now() - start;
|
|
4713
|
+
const statusCode = c.res.status || (caughtError ? 500 : 200);
|
|
4714
|
+
const sampled = shouldSampleRequest(
|
|
4715
|
+
requestId2,
|
|
4716
|
+
statusCode,
|
|
4717
|
+
observabilityConfig.sampling,
|
|
4718
|
+
"log"
|
|
4719
|
+
);
|
|
4720
|
+
const requestLogger = getRequestLogger(c, "api");
|
|
4721
|
+
const requestHeaders = redactHeaders(
|
|
4722
|
+
Object.fromEntries(c.req.raw.headers.entries()),
|
|
4723
|
+
observabilityConfig
|
|
4724
|
+
);
|
|
4725
|
+
const requestBodyCapture = bodyCapturePromise ? await bodyCapturePromise : void 0;
|
|
4726
|
+
if (requestBodyCapture) {
|
|
4727
|
+
c.set("requestBodyCapture", requestBodyCapture);
|
|
4728
|
+
}
|
|
4729
|
+
attachRequestMetrics(observability, {
|
|
4730
|
+
method,
|
|
4731
|
+
route: path,
|
|
4732
|
+
statusCode,
|
|
4733
|
+
durationMs
|
|
4734
|
+
});
|
|
4735
|
+
if (observabilityConfig.requestLogging.enabled && sampled) {
|
|
4736
|
+
const level = statusCode >= 500 ? "error" : statusCode >= 400 ? "warn" : "info";
|
|
4737
|
+
requestLogger[level]({
|
|
4738
|
+
msg: "HTTP request completed",
|
|
4739
|
+
method,
|
|
4740
|
+
path,
|
|
4741
|
+
statusCode,
|
|
4742
|
+
durationMs,
|
|
4743
|
+
requestId: requestId2,
|
|
4744
|
+
siteId: c.get("siteId"),
|
|
4745
|
+
workspaceId: c.get("workspaceId"),
|
|
4746
|
+
contentType: c.req.header("content-type") ?? void 0,
|
|
4747
|
+
contentLength: c.req.header("content-length") ? Number(c.req.header("content-length")) : void 0,
|
|
4748
|
+
sampled,
|
|
4749
|
+
headers: Object.keys(requestHeaders).length > 0 ? requestHeaders : void 0,
|
|
4750
|
+
body: requestBodyCapture?.body,
|
|
4751
|
+
bodyCapture: requestBodyCapture && requestBodyCapture.body === void 0 ? {
|
|
4752
|
+
attempted: requestBodyCapture.attempted,
|
|
4753
|
+
contentType: requestBodyCapture.contentType,
|
|
4754
|
+
contentLength: requestBodyCapture.contentLength,
|
|
4755
|
+
truncated: requestBodyCapture.truncated,
|
|
4756
|
+
parseFailed: requestBodyCapture.parseFailed
|
|
4757
|
+
} : void 0
|
|
4758
|
+
});
|
|
4759
|
+
}
|
|
4760
|
+
endRequestSpan(requestTrace, statusCode, caughtError);
|
|
4761
|
+
}
|
|
4762
|
+
});
|
|
4763
|
+
app.use("*", createRateLimitMiddleware(config));
|
|
4764
|
+
app.use("*", optionalAuth(config));
|
|
4765
|
+
app.use("*", cors(config.cors ? { origin: config.cors.origins } : {}));
|
|
3425
4766
|
app.get("/health", (c) => c.json({ status: "ok", version: "0.0.1" }));
|
|
4767
|
+
if (observability.config.metrics.enabled && observability.config.metrics.exporter === "prometheus" && observability.prometheusExporter) {
|
|
4768
|
+
app.get(observability.config.metrics.path, async (c) => {
|
|
4769
|
+
const metrics = await renderPrometheusMetrics(
|
|
4770
|
+
observability.prometheusExporter
|
|
4771
|
+
);
|
|
4772
|
+
return new Response(metrics.body, {
|
|
4773
|
+
status: metrics.statusCode,
|
|
4774
|
+
headers: metrics.headers
|
|
4775
|
+
});
|
|
4776
|
+
});
|
|
4777
|
+
}
|
|
3426
4778
|
app.get("/routes", (c) => {
|
|
3427
4779
|
const routes = app.routes.map((r) => ({ method: r.method, path: r.path }));
|
|
3428
4780
|
return c.json({ routes });
|
|
3429
4781
|
});
|
|
3430
4782
|
app.onError((err, c) => {
|
|
3431
|
-
|
|
4783
|
+
const logger = getRequestLogger(c, "core");
|
|
4784
|
+
logger.error({
|
|
4785
|
+
err,
|
|
4786
|
+
msg: "Uncaught error",
|
|
4787
|
+
path: c.req.path,
|
|
4788
|
+
method: c.req.method
|
|
4789
|
+
});
|
|
4790
|
+
observability.recordUncaughtError({
|
|
4791
|
+
path: c.req.path,
|
|
4792
|
+
method: c.req.method
|
|
4793
|
+
});
|
|
3432
4794
|
return c.json({
|
|
3433
4795
|
message: err.message || "Internal Server Error",
|
|
3434
4796
|
stack: process.env.NODE_ENV === "development" ? err.stack : void 0
|