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