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