@kyro-cms/core 0.8.0 → 0.9.1
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/README.md +57 -589
- package/dist/{WebhookService-118ZTFis.d.ts → WebhookService-CUTb9XOy.d.ts} +1 -1
- package/dist/{WebhookService-AefJfqX0.d.cts → WebhookService-Yg2UEOB4.d.cts} +1 -1
- package/dist/api-handler-graphql.cjs +44 -0
- package/dist/api-handler-graphql.cjs.map +1 -0
- package/dist/api-handler-graphql.d.cts +6 -0
- package/dist/api-handler-graphql.d.ts +6 -0
- package/dist/api-handler-graphql.js +41 -0
- package/dist/api-handler-graphql.js.map +1 -0
- package/dist/api-handler-trpc.cjs +38 -0
- package/dist/api-handler-trpc.cjs.map +1 -0
- package/dist/api-handler-trpc.d.cts +5 -0
- package/dist/api-handler-trpc.d.ts +5 -0
- package/dist/api-handler-trpc.js +36 -0
- package/dist/api-handler-trpc.js.map +1 -0
- package/dist/api-handler.cjs +31 -97
- package/dist/api-handler.cjs.map +1 -1
- package/dist/api-handler.d.cts +2 -1
- package/dist/api-handler.d.ts +2 -1
- package/dist/api-handler.js +19 -95
- package/dist/api-handler.js.map +1 -1
- package/dist/{tenant-B1YB0Jy8.d.ts → base-B71y_EAF.d.cts} +6 -12
- package/dist/{tenant-Cpeveji6.d.cts → base-DaqY2GhA.d.ts} +6 -12
- package/dist/bootstrap-5NLASFOG.cjs +32 -0
- package/dist/{bootstrap-AKAUP6F6.cjs.map → bootstrap-5NLASFOG.cjs.map} +1 -1
- package/dist/bootstrap-T5BK77LD.js +7 -0
- package/dist/{bootstrap-JCML6NFO.js.map → bootstrap-T5BK77LD.js.map} +1 -1
- package/dist/{chunk-35U3FROB.js → chunk-22M4O4ZJ.js} +607 -63
- package/dist/chunk-22M4O4ZJ.js.map +1 -0
- package/dist/chunk-2HZRBATX.cjs +253 -0
- package/dist/chunk-2HZRBATX.cjs.map +1 -0
- package/dist/{chunk-VJT6P4N6.cjs → chunk-3HR772HI.cjs} +199 -32
- package/dist/chunk-3HR772HI.cjs.map +1 -0
- package/dist/chunk-3KTWGODI.cjs +178 -0
- package/dist/chunk-3KTWGODI.cjs.map +1 -0
- package/dist/{chunk-QXIQWPAP.js → chunk-3UK5XBVJ.js} +4 -134
- package/dist/chunk-3UK5XBVJ.js.map +1 -0
- package/dist/{chunk-FXYP2HA6.js → chunk-4AO3A3JM.js} +48 -4
- package/dist/chunk-4AO3A3JM.js.map +1 -0
- package/dist/chunk-4M7X5HAB.cjs +173 -0
- package/dist/chunk-4M7X5HAB.cjs.map +1 -0
- package/dist/chunk-5EPFQUQD.js +3243 -0
- package/dist/chunk-5EPFQUQD.js.map +1 -0
- package/dist/{chunk-Y3N7UUDO.js → chunk-7OGPN7MP.js} +5 -2
- package/dist/chunk-7OGPN7MP.js.map +1 -0
- package/dist/{chunk-WOWUL7ZY.js → chunk-AL5KX63J.js} +4 -3
- package/dist/chunk-AL5KX63J.js.map +1 -0
- package/dist/{chunk-2OL4O2TH.cjs → chunk-C36TMDTY.cjs} +66 -61
- package/dist/chunk-C36TMDTY.cjs.map +1 -0
- package/dist/{chunk-ES5HNFFT.js → chunk-CF7OL6HR.js} +4 -2
- package/dist/chunk-CF7OL6HR.js.map +1 -0
- package/dist/chunk-CJONKRHJ.js +162 -0
- package/dist/chunk-CJONKRHJ.js.map +1 -0
- package/dist/{chunk-2KVHZE6O.cjs → chunk-COIASRDK.cjs} +202 -46
- package/dist/chunk-COIASRDK.cjs.map +1 -0
- package/dist/chunk-DEVFAKCQ.cjs +3291 -0
- package/dist/chunk-DEVFAKCQ.cjs.map +1 -0
- package/dist/{chunk-3ZFYL34R.js → chunk-DYTZ6FQ7.js} +12 -185
- package/dist/chunk-DYTZ6FQ7.js.map +1 -0
- package/dist/{chunk-QPPDLRNR.js → chunk-EJN2PAOE.js} +197 -41
- package/dist/chunk-EJN2PAOE.js.map +1 -0
- package/dist/chunk-FAXU7BMP.js +220 -0
- package/dist/chunk-FAXU7BMP.js.map +1 -0
- package/dist/{chunk-OHVB4AJ7.js → chunk-FOPGUM27.js} +22 -17
- package/dist/chunk-FOPGUM27.js.map +1 -0
- package/dist/chunk-GAOXD3XT.js +175 -0
- package/dist/chunk-GAOXD3XT.js.map +1 -0
- package/dist/{chunk-4DA7QPLA.cjs → chunk-GXFOGU7N.cjs} +5 -2
- package/dist/chunk-GXFOGU7N.cjs.map +1 -0
- package/dist/{chunk-I7HHI6QV.cjs → chunk-IDVRRRAK.cjs} +17 -9
- package/dist/chunk-IDVRRRAK.cjs.map +1 -0
- package/dist/{chunk-WQBRWOQT.cjs → chunk-JOPVMWTM.cjs} +3 -2
- package/dist/chunk-JOPVMWTM.cjs.map +1 -0
- package/dist/chunk-KC2GDBLS.cjs +84 -0
- package/dist/chunk-KC2GDBLS.cjs.map +1 -0
- package/dist/{chunk-K7JPTH3G.cjs → chunk-KNRSROWB.cjs} +132 -74
- package/dist/chunk-KNRSROWB.cjs.map +1 -0
- package/dist/{chunk-3AJE4SEG.js → chunk-KPA4AN4R.js} +125 -67
- package/dist/chunk-KPA4AN4R.js.map +1 -0
- package/dist/{chunk-QUW2RZTM.cjs → chunk-L46ROHUS.cjs} +51 -7
- package/dist/chunk-L46ROHUS.cjs.map +1 -0
- package/dist/chunk-L4EZKIEX.js +185 -0
- package/dist/chunk-L4EZKIEX.js.map +1 -0
- package/dist/{chunk-REK7AYOC.js → chunk-L5UKKZQN.js} +199 -32
- package/dist/chunk-L5UKKZQN.js.map +1 -0
- package/dist/chunk-NKPKR5BW.cjs +188 -0
- package/dist/chunk-NKPKR5BW.cjs.map +1 -0
- package/dist/{chunk-Y3QQN7PN.js → chunk-P2HKJ7P5.js} +13 -4
- package/dist/chunk-P2HKJ7P5.js.map +1 -0
- package/dist/{chunk-SA7NSSIQ.cjs → chunk-PI73NNOK.cjs} +13 -187
- package/dist/chunk-PI73NNOK.cjs.map +1 -0
- package/dist/{chunk-HXRD4B37.js → chunk-PU2Z5VWF.js} +1279 -556
- package/dist/chunk-PU2Z5VWF.js.map +1 -0
- package/dist/{chunk-H727JIG7.js → chunk-Q72BOAPK.js} +16 -8
- package/dist/chunk-Q72BOAPK.js.map +1 -0
- package/dist/{chunk-IBG6V56E.cjs → chunk-QFLB4EIJ.cjs} +2 -139
- package/dist/chunk-QFLB4EIJ.cjs.map +1 -0
- package/dist/{chunk-YVUJBEXE.cjs → chunk-RAMGUDJN.cjs} +16 -7
- package/dist/chunk-RAMGUDJN.cjs.map +1 -0
- package/dist/{chunk-LINKCEG4.cjs → chunk-ROJHKAQ4.cjs} +617 -73
- package/dist/chunk-ROJHKAQ4.cjs.map +1 -0
- package/dist/{chunk-5KVM3WEY.cjs → chunk-RSF3UU7H.cjs} +1330 -602
- package/dist/chunk-RSF3UU7H.cjs.map +1 -0
- package/dist/{chunk-V3LKPM3O.cjs → chunk-SHTTJMLT.cjs} +4 -2
- package/dist/chunk-SHTTJMLT.cjs.map +1 -0
- package/dist/chunk-SPBTLUN6.js +92 -0
- package/dist/chunk-SPBTLUN6.js.map +1 -0
- package/dist/{chunk-57P6MJKC.js → chunk-TXSZFA4G.js} +3 -3
- package/dist/chunk-TXSZFA4G.js.map +1 -0
- package/dist/chunk-UERVXYVK.cjs +99 -0
- package/dist/chunk-UERVXYVK.cjs.map +1 -0
- package/dist/{chunk-PDYFVNUX.cjs → chunk-V2TVSCV5.cjs} +16 -23
- package/dist/chunk-V2TVSCV5.cjs.map +1 -0
- package/dist/{chunk-DXHRBMGB.js → chunk-VO35MNPH.js} +12 -19
- package/dist/chunk-VO35MNPH.js.map +1 -0
- package/dist/{chunk-IA6AU5PI.cjs → chunk-WNCYAKF3.cjs} +3 -3
- package/dist/chunk-WNCYAKF3.cjs.map +1 -0
- package/dist/chunk-XEB7PH2E.js +81 -0
- package/dist/chunk-XEB7PH2E.js.map +1 -0
- package/dist/cli/index.cjs +5 -5
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +5 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/client.cjs +3 -3
- package/dist/client.d.cts +3 -3
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/drizzle/index.cjs +14 -13
- package/dist/drizzle/index.d.cts +9 -7
- package/dist/drizzle/index.d.ts +9 -7
- package/dist/drizzle/index.js +5 -4
- package/dist/fields/index.cjs +21 -37
- package/dist/fields/index.d.cts +2 -22
- package/dist/fields/index.d.ts +2 -22
- package/dist/fields/index.js +1 -1
- package/dist/graphql/index.cjs +5 -4
- package/dist/graphql/index.d.cts +5 -3
- package/dist/graphql/index.d.ts +5 -3
- package/dist/graphql/index.js +3 -2
- package/dist/index-CJXPB_ot.d.ts +276 -0
- package/dist/index-CaTNnLGd.d.cts +276 -0
- package/dist/index.cjs +304 -162
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +129 -205
- package/dist/index.d.ts +129 -205
- package/dist/index.js +172 -33
- package/dist/index.js.map +1 -1
- package/dist/integration.cjs +2 -2
- package/dist/integration.js +1 -1
- package/dist/mongo-auth-adapter-ISOM7FSS.cjs +17 -0
- package/dist/{mongo-auth-adapter-NHHUJHVH.cjs.map → mongo-auth-adapter-ISOM7FSS.cjs.map} +1 -1
- package/dist/mongo-auth-adapter-MO6STCV3.js +4 -0
- package/dist/{mongo-auth-adapter-NJQUUCTP.js.map → mongo-auth-adapter-MO6STCV3.js.map} +1 -1
- package/dist/mongodb/index.cjs +8 -7
- package/dist/mongodb/index.d.cts +5 -7
- package/dist/mongodb/index.d.ts +5 -7
- package/dist/mongodb/index.js +4 -3
- package/dist/postgres-auth-adapter-DWDR7P5G.js +5 -0
- package/dist/{postgres-auth-adapter-3T2NKTSE.js.map → postgres-auth-adapter-DWDR7P5G.js.map} +1 -1
- package/dist/postgres-auth-adapter-WRWSJD4E.cjs +14 -0
- package/dist/{postgres-auth-adapter-7IEENCKQ.cjs.map → postgres-auth-adapter-WRWSJD4E.cjs.map} +1 -1
- package/dist/redis-adapter-HGTPWIGV.js +4 -0
- package/dist/{redis-adapter-VQXD7ESY.js.map → redis-adapter-HGTPWIGV.js.map} +1 -1
- package/dist/redis-adapter-KJ3YOOT6.cjs +13 -0
- package/dist/{redis-adapter-D2E2S3GB.cjs.map → redis-adapter-KJ3YOOT6.cjs.map} +1 -1
- package/dist/rest/index.cjs +15 -14
- package/dist/rest/index.d.cts +4 -4
- package/dist/rest/index.d.ts +4 -4
- package/dist/rest/index.js +13 -12
- package/dist/{schema-5PHL5IVB.js → schema-6I5OFR4Z.js} +3 -3
- package/dist/{schema-5PHL5IVB.js.map → schema-6I5OFR4Z.js.map} +1 -1
- package/dist/{schema-37SE2F4B.cjs → schema-TTFE4467.cjs} +14 -14
- package/dist/{schema-37SE2F4B.cjs.map → schema-TTFE4467.cjs.map} +1 -1
- package/dist/sqlite-adapter-6GEUSVXQ.js +4 -0
- package/dist/{sqlite-adapter-TR3U3W6Q.js.map → sqlite-adapter-6GEUSVXQ.js.map} +1 -1
- package/dist/sqlite-adapter-CSIZE5SX.cjs +13 -0
- package/dist/{sqlite-adapter-LVK5PS4T.cjs.map → sqlite-adapter-CSIZE5SX.cjs.map} +1 -1
- package/dist/templates/index.cjs +133 -31
- package/dist/templates/index.d.cts +52 -9
- package/dist/templates/index.d.ts +52 -9
- package/dist/templates/index.js +3 -1
- package/dist/trpc/index.cjs +13 -12
- package/dist/trpc/index.d.cts +55 -49
- package/dist/trpc/index.d.ts +55 -49
- package/dist/trpc/index.js +4 -3
- package/dist/{types-D6ZLRGbH.d.cts → types-CpjuXbe7.d.cts} +2 -0
- package/dist/{types-D6ZLRGbH.d.ts → types-CpjuXbe7.d.ts} +2 -0
- package/dist/{types-Bs1up4yP.d.ts → types-CyCQ6SAI.d.ts} +28 -2
- package/dist/{types-J3R9nVsZ.d.cts → types-DJxD9394.d.cts} +28 -2
- package/dist/{types-VtjUxIMp.d.cts → types-Z6FBiqa2.d.cts} +35 -14
- package/dist/{types-VtjUxIMp.d.ts → types-Z6FBiqa2.d.ts} +35 -14
- package/package.json +22 -4
- package/dist/bootstrap-AKAUP6F6.cjs +0 -32
- package/dist/bootstrap-JCML6NFO.js +0 -7
- package/dist/chunk-2KVHZE6O.cjs.map +0 -1
- package/dist/chunk-2OL4O2TH.cjs.map +0 -1
- package/dist/chunk-35U3FROB.js.map +0 -1
- package/dist/chunk-3AJE4SEG.js.map +0 -1
- package/dist/chunk-3J4MFTI3.js +0 -3872
- package/dist/chunk-3J4MFTI3.js.map +0 -1
- package/dist/chunk-3ZFYL34R.js.map +0 -1
- package/dist/chunk-4DA7QPLA.cjs.map +0 -1
- package/dist/chunk-57P6MJKC.js.map +0 -1
- package/dist/chunk-5KVM3WEY.cjs.map +0 -1
- package/dist/chunk-6IMPH6WV.cjs +0 -3897
- package/dist/chunk-6IMPH6WV.cjs.map +0 -1
- package/dist/chunk-ATBOUGQP.cjs +0 -513
- package/dist/chunk-ATBOUGQP.cjs.map +0 -1
- package/dist/chunk-DXHRBMGB.js.map +0 -1
- package/dist/chunk-ES5HNFFT.js.map +0 -1
- package/dist/chunk-FXYP2HA6.js.map +0 -1
- package/dist/chunk-H727JIG7.js.map +0 -1
- package/dist/chunk-HXRD4B37.js.map +0 -1
- package/dist/chunk-I7HHI6QV.cjs.map +0 -1
- package/dist/chunk-IA6AU5PI.cjs.map +0 -1
- package/dist/chunk-IBG6V56E.cjs.map +0 -1
- package/dist/chunk-K7JPTH3G.cjs.map +0 -1
- package/dist/chunk-LINKCEG4.cjs.map +0 -1
- package/dist/chunk-OHVB4AJ7.js.map +0 -1
- package/dist/chunk-PDYFVNUX.cjs.map +0 -1
- package/dist/chunk-Q23JB3KL.js +0 -488
- package/dist/chunk-Q23JB3KL.js.map +0 -1
- package/dist/chunk-QPPDLRNR.js.map +0 -1
- package/dist/chunk-QUW2RZTM.cjs.map +0 -1
- package/dist/chunk-QXIQWPAP.js.map +0 -1
- package/dist/chunk-R3XIBBAW.cjs +0 -34
- package/dist/chunk-R3XIBBAW.cjs.map +0 -1
- package/dist/chunk-REK7AYOC.js.map +0 -1
- package/dist/chunk-SA7NSSIQ.cjs.map +0 -1
- package/dist/chunk-SDMNUYVU.js +0 -30
- package/dist/chunk-SDMNUYVU.js.map +0 -1
- package/dist/chunk-V3LKPM3O.cjs.map +0 -1
- package/dist/chunk-VJT6P4N6.cjs.map +0 -1
- package/dist/chunk-WOWUL7ZY.js.map +0 -1
- package/dist/chunk-WQBRWOQT.cjs.map +0 -1
- package/dist/chunk-Y3N7UUDO.js.map +0 -1
- package/dist/chunk-Y3QQN7PN.js.map +0 -1
- package/dist/chunk-YVUJBEXE.cjs.map +0 -1
- package/dist/index-CLp-DRKA.d.ts +0 -64
- package/dist/index-DfO7G4kN.d.cts +0 -64
- package/dist/mongo-auth-adapter-NHHUJHVH.cjs +0 -17
- package/dist/mongo-auth-adapter-NJQUUCTP.js +0 -4
- package/dist/postgres-auth-adapter-3T2NKTSE.js +0 -5
- package/dist/postgres-auth-adapter-7IEENCKQ.cjs +0 -14
- package/dist/redis-adapter-D2E2S3GB.cjs +0 -13
- package/dist/redis-adapter-VQXD7ESY.js +0 -4
- package/dist/sqlite-adapter-LVK5PS4T.cjs +0 -13
- package/dist/sqlite-adapter-TR3U3W6Q.js +0 -4
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { PasswordPolicy, ConfigService, EmailTransport } from './chunk-
|
|
2
|
-
import {
|
|
1
|
+
import { PasswordPolicy, ConfigService, EmailTransport } from './chunk-TXSZFA4G.js';
|
|
2
|
+
import { usersCollection } from './chunk-XEB7PH2E.js';
|
|
3
|
+
import { SQLiteAuthAdapter } from './chunk-Q72BOAPK.js';
|
|
3
4
|
import { createAuditContext } from './chunk-P2YW545G.js';
|
|
4
|
-
import { WEBHOOK_EVENTS
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { hasPermission } from './chunk-3ZFYL34R.js';
|
|
5
|
+
import { WEBHOOK_EVENTS } from './chunk-3UK5XBVJ.js';
|
|
6
|
+
import { API_KEY_COLLECTION, generateApiKey, generateApiKeyPrefix, evaluateAccess, hasApiKeyPermission, extractApiKeyFromRequest, validateApiKey, createApiKeyContext } from './chunk-CJONKRHJ.js';
|
|
7
|
+
import { genId, DrizzleAdapter } from './chunk-EJN2PAOE.js';
|
|
8
|
+
import { PostgresAuthAdapter } from './chunk-FOPGUM27.js';
|
|
9
|
+
import { MongoDBAdapter } from './chunk-VO35MNPH.js';
|
|
10
|
+
import { MongoDBAuthAdapter } from './chunk-7OGPN7MP.js';
|
|
11
|
+
import { hasPermission } from './chunk-L4EZKIEX.js';
|
|
12
12
|
import { __esm, __export, __toCommonJS, __require } from './chunk-Z6ZWNWWR.js';
|
|
13
13
|
import crypto, { randomBytes, createHash } from 'crypto';
|
|
14
14
|
import { createStorage } from 'unstorage';
|
|
@@ -17,11 +17,12 @@ import { Hono } from 'hono';
|
|
|
17
17
|
import path, { join, basename, extname, resolve } from 'path';
|
|
18
18
|
import { existsSync, readFileSync } from 'fs';
|
|
19
19
|
import sharp from 'sharp';
|
|
20
|
-
import { parse, execute } from 'graphql';
|
|
21
20
|
import { mkdir, readdir, stat, rename, unlink, writeFile } from 'fs/promises';
|
|
21
|
+
import process2 from 'process';
|
|
22
22
|
import { S3Client, ListObjectsV2Command, DeleteObjectsCommand, DeleteObjectCommand, PutObjectCommand, HeadObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
|
|
23
23
|
import { Readable } from 'stream';
|
|
24
24
|
import { Client } from 'basic-ftp';
|
|
25
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
25
26
|
|
|
26
27
|
function setDbAdapter(adapter) {
|
|
27
28
|
dbAdapter = adapter;
|
|
@@ -773,7 +774,7 @@ var AuditLogger = class {
|
|
|
773
774
|
serializeLog(log) {
|
|
774
775
|
const result = {
|
|
775
776
|
id: log.id,
|
|
776
|
-
timestamp: log.timestamp.toISOString(),
|
|
777
|
+
timestamp: new Date(log.timestamp).toISOString(),
|
|
777
778
|
action: log.action,
|
|
778
779
|
resource: log.resource,
|
|
779
780
|
success: log.success ? "1" : "0"
|
|
@@ -1168,23 +1169,19 @@ var AuthRoutes = class {
|
|
|
1168
1169
|
}
|
|
1169
1170
|
}
|
|
1170
1171
|
async me(req) {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
return this.errorResponse("Not authenticated", 401);
|
|
1175
|
-
}
|
|
1176
|
-
const user = await this.authAdapter.findUserById(session.userId);
|
|
1177
|
-
if (!user) {
|
|
1178
|
-
return this.errorResponse("User not found", 404);
|
|
1179
|
-
}
|
|
1180
|
-
return this.jsonResponse({
|
|
1181
|
-
success: true,
|
|
1182
|
-
user: this.sanitizeUser(user)
|
|
1183
|
-
});
|
|
1184
|
-
} catch (error) {
|
|
1185
|
-
console.error("[AuthRoutes.me] Authentication failed:", error.message);
|
|
1186
|
-
return this.errorResponse("Authentication failed", 401);
|
|
1172
|
+
const session = await getCurrentUser(req);
|
|
1173
|
+
if (!session) {
|
|
1174
|
+
return this.errorResponse("Not authenticated", 401);
|
|
1187
1175
|
}
|
|
1176
|
+
return this.jsonResponse({
|
|
1177
|
+
success: true,
|
|
1178
|
+
user: {
|
|
1179
|
+
id: session.userId,
|
|
1180
|
+
email: session.email,
|
|
1181
|
+
role: session.role,
|
|
1182
|
+
tenantId: session.tenantId
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1188
1185
|
}
|
|
1189
1186
|
async changePassword(req) {
|
|
1190
1187
|
const session = await getCurrentUser(req);
|
|
@@ -1294,7 +1291,7 @@ var AuthRoutes = class {
|
|
|
1294
1291
|
}
|
|
1295
1292
|
if (this.auditLogger) {
|
|
1296
1293
|
await this.auditLogger.log({
|
|
1297
|
-
action: "
|
|
1294
|
+
action: "password_reset",
|
|
1298
1295
|
userId: user.id,
|
|
1299
1296
|
userEmail: user.email,
|
|
1300
1297
|
resource: "auth",
|
|
@@ -1482,7 +1479,7 @@ var AuthRoutes = class {
|
|
|
1482
1479
|
}
|
|
1483
1480
|
};
|
|
1484
1481
|
function createLocalStorage(config) {
|
|
1485
|
-
const { uploadDir, baseUrl = "/uploads" } = config;
|
|
1482
|
+
const { uploadDir = join(process2.cwd(), "public", "uploads"), baseUrl = "/uploads" } = config;
|
|
1486
1483
|
async function ensureDir(dir) {
|
|
1487
1484
|
if (!existsSync(dir)) {
|
|
1488
1485
|
await mkdir(dir, { recursive: true });
|
|
@@ -1670,6 +1667,467 @@ function createLocalStorage(config) {
|
|
|
1670
1667
|
}
|
|
1671
1668
|
};
|
|
1672
1669
|
}
|
|
1670
|
+
|
|
1671
|
+
// src/storage/imgix.ts
|
|
1672
|
+
function createImgixStorage(config) {
|
|
1673
|
+
const signUrl = (path3, params) => {
|
|
1674
|
+
if (!config.signKey) return path3;
|
|
1675
|
+
const signer = new TextEncoder();
|
|
1676
|
+
const key = signer.encode(config.signKey);
|
|
1677
|
+
const data = signer.encode(path3 + params.toString());
|
|
1678
|
+
let hash = 0;
|
|
1679
|
+
const combined = new Uint8Array(key.length + data.length);
|
|
1680
|
+
combined.set(key);
|
|
1681
|
+
combined.set(data, key.length);
|
|
1682
|
+
for (let i = 0; i < combined.length; i++) {
|
|
1683
|
+
hash = (hash << 5) - hash + combined[i] | 0;
|
|
1684
|
+
}
|
|
1685
|
+
params.set("s", Math.abs(hash).toString(16));
|
|
1686
|
+
return path3;
|
|
1687
|
+
};
|
|
1688
|
+
return {
|
|
1689
|
+
name: "imgix",
|
|
1690
|
+
displayName: "Imgix",
|
|
1691
|
+
supportsDynamicResize: true,
|
|
1692
|
+
async upload(_file, _options) {
|
|
1693
|
+
throw new Error(
|
|
1694
|
+
"Imgix is a transformation service. Use another provider for uploads."
|
|
1695
|
+
);
|
|
1696
|
+
},
|
|
1697
|
+
async uploadFromUrl(url, options) {
|
|
1698
|
+
const filename = options?.filename || url.split("/").pop() || "file";
|
|
1699
|
+
const response = await fetch(url);
|
|
1700
|
+
if (!response.ok) {
|
|
1701
|
+
throw new Error(`Failed to fetch: ${response.statusText}`);
|
|
1702
|
+
}
|
|
1703
|
+
const blob = await response.blob();
|
|
1704
|
+
new File([blob], filename, { type: blob.type });
|
|
1705
|
+
return {
|
|
1706
|
+
id: Buffer.from(url).toString("base64").slice(0, 20),
|
|
1707
|
+
filename,
|
|
1708
|
+
originalName: filename,
|
|
1709
|
+
mimeType: blob.type,
|
|
1710
|
+
size: blob.size,
|
|
1711
|
+
url: this.getImageUrl(url),
|
|
1712
|
+
thumbnailUrl: this.getImageUrl(url, {
|
|
1713
|
+
width: 200,
|
|
1714
|
+
height: 200,
|
|
1715
|
+
fit: "crop"
|
|
1716
|
+
}),
|
|
1717
|
+
provider: "imgix",
|
|
1718
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1719
|
+
};
|
|
1720
|
+
},
|
|
1721
|
+
async delete(_url) {
|
|
1722
|
+
},
|
|
1723
|
+
async rename(_oldUrl, newKey) {
|
|
1724
|
+
return `https://${config.domain}/${newKey}`;
|
|
1725
|
+
},
|
|
1726
|
+
getImageUrl(url, transforms) {
|
|
1727
|
+
const parsed = new URL(url);
|
|
1728
|
+
const params = new URLSearchParams(parsed.search);
|
|
1729
|
+
if (config.defaultParameters) {
|
|
1730
|
+
Object.entries(config.defaultParameters).forEach(([key, value]) => {
|
|
1731
|
+
if (!params.has(key)) {
|
|
1732
|
+
params.set(key, value);
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
if (transforms) {
|
|
1737
|
+
if (transforms.width) params.set("w", String(transforms.width));
|
|
1738
|
+
if (transforms.height) params.set("h", String(transforms.height));
|
|
1739
|
+
if (transforms.quality) params.set("q", String(transforms.quality));
|
|
1740
|
+
if (transforms.format) params.set("fm", transforms.format);
|
|
1741
|
+
if (transforms.fit) params.set("fit", transforms.fit);
|
|
1742
|
+
if (transforms.blur) params.set("blur", String(transforms.blur));
|
|
1743
|
+
if (transforms.sharpen) params.set("sharp", String(transforms.sharpen));
|
|
1744
|
+
}
|
|
1745
|
+
params.set("auto", "compress,format");
|
|
1746
|
+
parsed.pathname + "?" + params.toString();
|
|
1747
|
+
if (config.signKey) {
|
|
1748
|
+
signUrl(parsed.pathname + params.toString(), params);
|
|
1749
|
+
}
|
|
1750
|
+
return `https://${config.domain}${parsed.pathname}${params.toString() ? "?" + params.toString() : ""}`;
|
|
1751
|
+
},
|
|
1752
|
+
async generateThumbnail(file) {
|
|
1753
|
+
return this.getImageUrl(file.url, {
|
|
1754
|
+
width: 200,
|
|
1755
|
+
height: 200,
|
|
1756
|
+
fit: "crop"
|
|
1757
|
+
});
|
|
1758
|
+
},
|
|
1759
|
+
async list() {
|
|
1760
|
+
return [];
|
|
1761
|
+
},
|
|
1762
|
+
async exists(url) {
|
|
1763
|
+
try {
|
|
1764
|
+
const response = await fetch(url, { method: "HEAD" });
|
|
1765
|
+
return response.ok;
|
|
1766
|
+
} catch {
|
|
1767
|
+
return false;
|
|
1768
|
+
}
|
|
1769
|
+
},
|
|
1770
|
+
async createFolder() {
|
|
1771
|
+
},
|
|
1772
|
+
async deleteFolder() {
|
|
1773
|
+
}
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/storage/bunny.ts
|
|
1778
|
+
function getUrl(key, config) {
|
|
1779
|
+
if (config.cdnUrl) {
|
|
1780
|
+
return `${config.cdnUrl.replace(/\/$/, "")}/${key}`;
|
|
1781
|
+
}
|
|
1782
|
+
return `https://${config.storageZone}.b-cdn.net/${key}`;
|
|
1783
|
+
}
|
|
1784
|
+
function getUrlPrefix(config) {
|
|
1785
|
+
if (config.cdnUrl) {
|
|
1786
|
+
return config.cdnUrl.replace(/\/$/, "") + "/";
|
|
1787
|
+
}
|
|
1788
|
+
return `https://${config.storageZone}.b-cdn.net/`;
|
|
1789
|
+
}
|
|
1790
|
+
function createBunnyStorage(config) {
|
|
1791
|
+
const baseUrl = `https://storage.bunnycdn.com/${config.storageZone}`;
|
|
1792
|
+
const getKey = (path3) => {
|
|
1793
|
+
const prefix = config.prefix ? `${config.prefix}/` : "";
|
|
1794
|
+
return `${prefix}${path3}`.replace(/\/+/g, "/");
|
|
1795
|
+
};
|
|
1796
|
+
return {
|
|
1797
|
+
name: "bunny",
|
|
1798
|
+
displayName: "Bunny.net Storage",
|
|
1799
|
+
supportsDynamicResize: true,
|
|
1800
|
+
async upload(file, options) {
|
|
1801
|
+
const key = getKey(
|
|
1802
|
+
`${options?.folder ? `${options.folder}/` : ""}${options?.filename || file.name}`
|
|
1803
|
+
);
|
|
1804
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
1805
|
+
const response = await fetch(`${baseUrl}/${key}`, {
|
|
1806
|
+
method: "PUT",
|
|
1807
|
+
headers: {
|
|
1808
|
+
AccessKey: config.apiKey,
|
|
1809
|
+
"Content-Type": file.type
|
|
1810
|
+
},
|
|
1811
|
+
body: buffer
|
|
1812
|
+
});
|
|
1813
|
+
if (!response.ok) {
|
|
1814
|
+
const errorText = await response.text();
|
|
1815
|
+
throw new Error(
|
|
1816
|
+
`Bunny.net upload failed: ${response.status} ${errorText}`
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
return {
|
|
1820
|
+
id: Buffer.from(key).toString("base64url"),
|
|
1821
|
+
filename: options?.filename || file.name,
|
|
1822
|
+
originalName: file.name,
|
|
1823
|
+
mimeType: file.type,
|
|
1824
|
+
size: buffer.length,
|
|
1825
|
+
url: getUrl(key, config),
|
|
1826
|
+
thumbnailUrl: file.type.startsWith("image/") ? getUrl(key, config) : void 0,
|
|
1827
|
+
folder: options?.folder,
|
|
1828
|
+
provider: "bunny",
|
|
1829
|
+
metadata: options?.metadata,
|
|
1830
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1831
|
+
};
|
|
1832
|
+
},
|
|
1833
|
+
async uploadFromUrl(url) {
|
|
1834
|
+
const response = await fetch(url);
|
|
1835
|
+
if (!response.ok) {
|
|
1836
|
+
throw new Error(`Failed to fetch: ${response.statusText}`);
|
|
1837
|
+
}
|
|
1838
|
+
const blob = await response.blob();
|
|
1839
|
+
const filename = url.split("/").pop() || "file";
|
|
1840
|
+
const file = new File([blob], filename, { type: blob.type });
|
|
1841
|
+
return this.upload(file);
|
|
1842
|
+
},
|
|
1843
|
+
async delete(url) {
|
|
1844
|
+
const key = url.replace(getUrlPrefix(config), "");
|
|
1845
|
+
const response = await fetch(`${baseUrl}/${key}`, {
|
|
1846
|
+
method: "DELETE",
|
|
1847
|
+
headers: {
|
|
1848
|
+
AccessKey: config.apiKey
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1851
|
+
if (!response.ok && response.status !== 404) {
|
|
1852
|
+
const errorText = await response.text();
|
|
1853
|
+
throw new Error(
|
|
1854
|
+
`Bunny.net delete failed: ${response.status} ${errorText}`
|
|
1855
|
+
);
|
|
1856
|
+
}
|
|
1857
|
+
},
|
|
1858
|
+
async rename(oldUrl, newKey) {
|
|
1859
|
+
const oldKey = oldUrl.replace(getUrlPrefix(config), "");
|
|
1860
|
+
const fullPath = config.prefix ? `${config.prefix}/${newKey}` : newKey;
|
|
1861
|
+
const response = await fetch(`${baseUrl}/${oldKey}`, {
|
|
1862
|
+
method: "GET",
|
|
1863
|
+
headers: {
|
|
1864
|
+
AccessKey: config.apiKey
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
if (!response.ok) {
|
|
1868
|
+
throw new Error(`Bunny.net rename failed: could not read old file`);
|
|
1869
|
+
}
|
|
1870
|
+
const content = await response.arrayBuffer();
|
|
1871
|
+
await fetch(`${baseUrl}/${fullPath}`, {
|
|
1872
|
+
method: "PUT",
|
|
1873
|
+
headers: {
|
|
1874
|
+
AccessKey: config.apiKey,
|
|
1875
|
+
"Content-Type": response.headers.get("Content-Type") || "application/octet-stream"
|
|
1876
|
+
},
|
|
1877
|
+
body: content
|
|
1878
|
+
});
|
|
1879
|
+
await this.delete(oldUrl);
|
|
1880
|
+
return getUrl(fullPath, config);
|
|
1881
|
+
},
|
|
1882
|
+
getImageUrl(url, transforms) {
|
|
1883
|
+
if (!transforms || Object.keys(transforms).length === 0) return url;
|
|
1884
|
+
const params = new URLSearchParams({ url });
|
|
1885
|
+
if (transforms.width) params.set("w", String(transforms.width));
|
|
1886
|
+
if (transforms.height) params.set("h", String(transforms.height));
|
|
1887
|
+
if (transforms.quality) params.set("q", String(transforms.quality));
|
|
1888
|
+
if (transforms.format) params.set("f", transforms.format);
|
|
1889
|
+
return `/api/media/resize?${params.toString()}`;
|
|
1890
|
+
},
|
|
1891
|
+
async generateThumbnail(file) {
|
|
1892
|
+
return this.getImageUrl(file.url, { width: 400, height: 400 });
|
|
1893
|
+
},
|
|
1894
|
+
async list(prefix) {
|
|
1895
|
+
const key = getKey(prefix || "");
|
|
1896
|
+
const response = await fetch(`${baseUrl}/${key}`, {
|
|
1897
|
+
method: "GET",
|
|
1898
|
+
headers: {
|
|
1899
|
+
AccessKey: config.apiKey
|
|
1900
|
+
}
|
|
1901
|
+
});
|
|
1902
|
+
if (!response.ok) {
|
|
1903
|
+
const errorText = await response.text();
|
|
1904
|
+
throw new Error(
|
|
1905
|
+
`Bunny.net list failed: ${response.status} ${errorText}`
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
const items = await response.json();
|
|
1909
|
+
return items.map((item) => ({
|
|
1910
|
+
id: Buffer.from(item.ObjectName || "").toString("base64url"),
|
|
1911
|
+
filename: item.ObjectName?.split("/").pop() || "",
|
|
1912
|
+
originalName: item.ObjectName?.split("/").pop() || "",
|
|
1913
|
+
mimeType: "application/octet-stream",
|
|
1914
|
+
size: item.Length || 0,
|
|
1915
|
+
url: getUrl(item.ObjectName || "", config),
|
|
1916
|
+
provider: "bunny",
|
|
1917
|
+
createdAt: item.LastChanged || (/* @__PURE__ */ new Date()).toISOString()
|
|
1918
|
+
}));
|
|
1919
|
+
},
|
|
1920
|
+
async exists(url) {
|
|
1921
|
+
const key = url.replace(getUrlPrefix(config), "");
|
|
1922
|
+
const response = await fetch(`${baseUrl}/${key}`, {
|
|
1923
|
+
method: "HEAD",
|
|
1924
|
+
headers: {
|
|
1925
|
+
AccessKey: config.apiKey
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
return response.ok;
|
|
1929
|
+
},
|
|
1930
|
+
async createFolder(folder) {
|
|
1931
|
+
const key = getKey(`${folder}/`);
|
|
1932
|
+
await fetch(`${baseUrl}/${key}`, {
|
|
1933
|
+
method: "PUT",
|
|
1934
|
+
headers: {
|
|
1935
|
+
AccessKey: config.apiKey,
|
|
1936
|
+
"Content-Length": "0"
|
|
1937
|
+
},
|
|
1938
|
+
body: ""
|
|
1939
|
+
});
|
|
1940
|
+
},
|
|
1941
|
+
async deleteFolder(folder) {
|
|
1942
|
+
const key = getKey(`${folder}/`);
|
|
1943
|
+
await fetch(`${baseUrl}/${key}`, {
|
|
1944
|
+
method: "DELETE",
|
|
1945
|
+
headers: {
|
|
1946
|
+
AccessKey: config.apiKey
|
|
1947
|
+
}
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
var StorageProviderRegistry = class {
|
|
1953
|
+
providers = /* @__PURE__ */ new Map();
|
|
1954
|
+
providerToPlugin = /* @__PURE__ */ new Map();
|
|
1955
|
+
disabledPlugins = /* @__PURE__ */ new Set();
|
|
1956
|
+
constructor() {
|
|
1957
|
+
this.registerLocal();
|
|
1958
|
+
this.registerImgix();
|
|
1959
|
+
this.registerBunny();
|
|
1960
|
+
}
|
|
1961
|
+
registerLocal() {
|
|
1962
|
+
this.register({
|
|
1963
|
+
type: "local",
|
|
1964
|
+
displayName: "Local Server",
|
|
1965
|
+
configKey: "local",
|
|
1966
|
+
configFields: [
|
|
1967
|
+
{
|
|
1968
|
+
name: "uploadDir",
|
|
1969
|
+
type: "text",
|
|
1970
|
+
label: "Upload Directory",
|
|
1971
|
+
defaultValue: "./public/uploads"
|
|
1972
|
+
},
|
|
1973
|
+
{
|
|
1974
|
+
name: "baseUrl",
|
|
1975
|
+
type: "text",
|
|
1976
|
+
label: "Base URL",
|
|
1977
|
+
defaultValue: "/uploads"
|
|
1978
|
+
}
|
|
1979
|
+
],
|
|
1980
|
+
extractConfig: (sc, key) => sc[key] || {},
|
|
1981
|
+
extractRawConfig: (c) => {
|
|
1982
|
+
const localConfig = c?.local || c;
|
|
1983
|
+
const savedUploadDir = (localConfig?.uploadDir || "").trim();
|
|
1984
|
+
let uploadDir;
|
|
1985
|
+
if (savedUploadDir) {
|
|
1986
|
+
if (path.isAbsolute(savedUploadDir)) {
|
|
1987
|
+
uploadDir = savedUploadDir;
|
|
1988
|
+
} else if (savedUploadDir.includes("/") || savedUploadDir.includes("\\")) {
|
|
1989
|
+
uploadDir = path.resolve(process.cwd(), savedUploadDir);
|
|
1990
|
+
} else {
|
|
1991
|
+
uploadDir = path.join(process.cwd(), "public", savedUploadDir);
|
|
1992
|
+
}
|
|
1993
|
+
} else {
|
|
1994
|
+
uploadDir = path.join(process.cwd(), "public", "uploads");
|
|
1995
|
+
}
|
|
1996
|
+
const savedBaseUrl = (localConfig?.baseUrl || "").trim();
|
|
1997
|
+
let baseUrl;
|
|
1998
|
+
if (savedBaseUrl) {
|
|
1999
|
+
baseUrl = savedBaseUrl.startsWith("/") ? savedBaseUrl : `/${savedBaseUrl}`;
|
|
2000
|
+
} else {
|
|
2001
|
+
baseUrl = "/uploads";
|
|
2002
|
+
}
|
|
2003
|
+
return { uploadDir, baseUrl };
|
|
2004
|
+
},
|
|
2005
|
+
factory: (c) => createLocalStorage(c)
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
registerImgix() {
|
|
2009
|
+
this.register({
|
|
2010
|
+
type: "imgix",
|
|
2011
|
+
displayName: "Imgix",
|
|
2012
|
+
configKey: "imgix",
|
|
2013
|
+
configFields: [
|
|
2014
|
+
{ name: "domain", type: "text", label: "Domain", required: true },
|
|
2015
|
+
{ name: "signKey", type: "password", label: "Sign Key" }
|
|
2016
|
+
],
|
|
2017
|
+
extractConfig: (sc, key) => sc[key] || {},
|
|
2018
|
+
extractRawConfig: (c) => c?.imgix || c,
|
|
2019
|
+
factory: (c) => createImgixStorage(c)
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
registerBunny() {
|
|
2023
|
+
this.register({
|
|
2024
|
+
type: "bunny",
|
|
2025
|
+
displayName: "Bunny.net",
|
|
2026
|
+
configKey: "bunny",
|
|
2027
|
+
configFields: [
|
|
2028
|
+
{
|
|
2029
|
+
name: "storageZone",
|
|
2030
|
+
type: "text",
|
|
2031
|
+
label: "Storage Zone",
|
|
2032
|
+
required: true
|
|
2033
|
+
},
|
|
2034
|
+
{ name: "apiKey", type: "password", label: "API Key", required: true },
|
|
2035
|
+
{ name: "cdnUrl", type: "text", label: "CDN URL" },
|
|
2036
|
+
{ name: "prefix", type: "text", label: "Path Prefix" }
|
|
2037
|
+
],
|
|
2038
|
+
extractConfig: (sc, key) => sc[key] || {},
|
|
2039
|
+
extractRawConfig: (c) => c?.bunny || c,
|
|
2040
|
+
factory: (c) => createBunnyStorage(c)
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
register(registration) {
|
|
2044
|
+
if (this.providers.has(registration.type)) {
|
|
2045
|
+
console.warn(
|
|
2046
|
+
`[StorageRegistry] Provider "${registration.type}" already registered, skipping`
|
|
2047
|
+
);
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
if (registration.pluginName) {
|
|
2051
|
+
this.providerToPlugin.set(registration.type, registration.pluginName);
|
|
2052
|
+
}
|
|
2053
|
+
this.providers.set(registration.type, registration);
|
|
2054
|
+
}
|
|
2055
|
+
unregister(type) {
|
|
2056
|
+
this.providers.delete(type);
|
|
2057
|
+
this.providerToPlugin.delete(type);
|
|
2058
|
+
}
|
|
2059
|
+
get(type) {
|
|
2060
|
+
return this.providers.get(type);
|
|
2061
|
+
}
|
|
2062
|
+
getAll() {
|
|
2063
|
+
return Array.from(this.providers.values());
|
|
2064
|
+
}
|
|
2065
|
+
getAllAvailable(isPluginEnabled) {
|
|
2066
|
+
const all = this.getAll();
|
|
2067
|
+
if (!isPluginEnabled) return all;
|
|
2068
|
+
return all.filter((p) => {
|
|
2069
|
+
if (!p.pluginName) return true;
|
|
2070
|
+
return isPluginEnabled(p.pluginName);
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
has(type) {
|
|
2074
|
+
return this.providers.has(type);
|
|
2075
|
+
}
|
|
2076
|
+
async resolve(type, storageConfig) {
|
|
2077
|
+
const reg = this.providers.get(type);
|
|
2078
|
+
if (!reg) {
|
|
2079
|
+
throw new Error(
|
|
2080
|
+
`Unknown storage provider type: "${type}". Is the plugin that provides it enabled?`
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
if (reg.pluginName && this.disabledPlugins.has(reg.pluginName)) {
|
|
2084
|
+
throw new Error(
|
|
2085
|
+
`Storage provider "${type}" is not available (plugin "${reg.pluginName}" is disabled)`
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
const configKey = reg.configKey || type;
|
|
2089
|
+
const config = reg.extractConfig(storageConfig, configKey);
|
|
2090
|
+
return reg.factory(config);
|
|
2091
|
+
}
|
|
2092
|
+
async resolveWithConfig(type, config) {
|
|
2093
|
+
const reg = this.providers.get(type);
|
|
2094
|
+
if (!reg) {
|
|
2095
|
+
throw new Error(
|
|
2096
|
+
`Unknown storage provider type: "${type}". Is the plugin that provides it enabled?`
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
if (reg.pluginName && this.disabledPlugins.has(reg.pluginName)) {
|
|
2100
|
+
throw new Error(
|
|
2101
|
+
`Storage provider "${type}" is not available (plugin "${reg.pluginName}" is disabled)`
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
const providerConfig = reg.extractRawConfig(config);
|
|
2105
|
+
return reg.factory(providerConfig);
|
|
2106
|
+
}
|
|
2107
|
+
getProviderPluginName(type) {
|
|
2108
|
+
return this.providerToPlugin.get(type);
|
|
2109
|
+
}
|
|
2110
|
+
getAllPluginNames() {
|
|
2111
|
+
return Array.from(new Set(this.providerToPlugin.values()));
|
|
2112
|
+
}
|
|
2113
|
+
setPluginEnabled(name, enabled) {
|
|
2114
|
+
if (enabled) {
|
|
2115
|
+
this.disabledPlugins.delete(name);
|
|
2116
|
+
} else {
|
|
2117
|
+
this.disabledPlugins.add(name);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
isPluginEnabled(name) {
|
|
2121
|
+
return !this.disabledPlugins.has(name);
|
|
2122
|
+
}
|
|
2123
|
+
};
|
|
2124
|
+
var instance = null;
|
|
2125
|
+
function getDefaultRegistry() {
|
|
2126
|
+
if (!instance) {
|
|
2127
|
+
instance = new StorageProviderRegistry();
|
|
2128
|
+
}
|
|
2129
|
+
return instance;
|
|
2130
|
+
}
|
|
1673
2131
|
function extractPublicDevUrlId(url) {
|
|
1674
2132
|
if (!url) return "";
|
|
1675
2133
|
if (url.startsWith("pub-")) return url;
|
|
@@ -1703,7 +2161,7 @@ function getPublicUrl(key, config) {
|
|
|
1703
2161
|
return `https://${config.bucket}.s3.${config.region}.amazonaws.com/${normalizedKey}`;
|
|
1704
2162
|
}
|
|
1705
2163
|
}
|
|
1706
|
-
function
|
|
2164
|
+
function getUrlPrefix2(config) {
|
|
1707
2165
|
if (config.cdnUrl) {
|
|
1708
2166
|
return config.cdnUrl.replace(/\/$/, "") + "/";
|
|
1709
2167
|
}
|
|
@@ -1771,11 +2229,11 @@ function createS3Storage(config) {
|
|
|
1771
2229
|
})
|
|
1772
2230
|
}
|
|
1773
2231
|
});
|
|
1774
|
-
const getKey = (
|
|
2232
|
+
const getKey = (path3) => {
|
|
1775
2233
|
const prefix = config.prefix ? `${config.prefix}/` : "";
|
|
1776
|
-
return `${prefix}${
|
|
2234
|
+
return `${prefix}${path3}`.replace(/\/+/g, "/");
|
|
1777
2235
|
};
|
|
1778
|
-
const
|
|
2236
|
+
const getUrl2 = (key) => getPublicUrl(key, config);
|
|
1779
2237
|
return {
|
|
1780
2238
|
name: config.provider,
|
|
1781
2239
|
displayName: getDisplayName(config.provider),
|
|
@@ -1806,8 +2264,8 @@ function createS3Storage(config) {
|
|
|
1806
2264
|
originalName: file.name,
|
|
1807
2265
|
mimeType: file.type,
|
|
1808
2266
|
size: buffer.length,
|
|
1809
|
-
url:
|
|
1810
|
-
thumbnailUrl: file.type.startsWith("image/") ?
|
|
2267
|
+
url: getUrl2(key),
|
|
2268
|
+
thumbnailUrl: file.type.startsWith("image/") ? getUrl2(key) : void 0,
|
|
1811
2269
|
folder: options?.folder,
|
|
1812
2270
|
provider: config.provider,
|
|
1813
2271
|
metadata: {
|
|
@@ -1828,7 +2286,7 @@ function createS3Storage(config) {
|
|
|
1828
2286
|
return this.upload(file);
|
|
1829
2287
|
},
|
|
1830
2288
|
async delete(url) {
|
|
1831
|
-
const key = url.replace(
|
|
2289
|
+
const key = url.replace(getUrlPrefix2(config), "");
|
|
1832
2290
|
await client.send(
|
|
1833
2291
|
new DeleteObjectCommand({
|
|
1834
2292
|
Bucket: config.bucket,
|
|
@@ -1837,7 +2295,7 @@ function createS3Storage(config) {
|
|
|
1837
2295
|
);
|
|
1838
2296
|
},
|
|
1839
2297
|
async rename(oldUrl, newKey) {
|
|
1840
|
-
const oldKey = oldUrl.replace(
|
|
2298
|
+
const oldKey = oldUrl.replace(getUrlPrefix2(config), "");
|
|
1841
2299
|
const newKeyWithPrefix = config.prefix ? `${config.prefix}/${newKey}` : newKey;
|
|
1842
2300
|
await client.send(
|
|
1843
2301
|
new CopyObjectCommand({
|
|
@@ -1852,7 +2310,7 @@ function createS3Storage(config) {
|
|
|
1852
2310
|
Key: oldKey
|
|
1853
2311
|
})
|
|
1854
2312
|
);
|
|
1855
|
-
return
|
|
2313
|
+
return getUrl2(newKeyWithPrefix);
|
|
1856
2314
|
},
|
|
1857
2315
|
getImageUrl(url, transforms) {
|
|
1858
2316
|
if (!transforms || Object.keys(transforms).length === 0) return url;
|
|
@@ -1880,14 +2338,14 @@ function createS3Storage(config) {
|
|
|
1880
2338
|
originalName: item.Key?.split("/").pop() || "",
|
|
1881
2339
|
mimeType: "application/octet-stream",
|
|
1882
2340
|
size: item.Size || 0,
|
|
1883
|
-
url:
|
|
2341
|
+
url: getUrl2(item.Key || ""),
|
|
1884
2342
|
provider: config.provider,
|
|
1885
2343
|
createdAt: item.LastModified?.toISOString() || (/* @__PURE__ */ new Date()).toISOString()
|
|
1886
2344
|
}));
|
|
1887
2345
|
},
|
|
1888
2346
|
async exists(url) {
|
|
1889
2347
|
try {
|
|
1890
|
-
const key = url.replace(
|
|
2348
|
+
const key = url.replace(getUrlPrefix2(config), "");
|
|
1891
2349
|
await client.send(
|
|
1892
2350
|
new HeadObjectCommand({
|
|
1893
2351
|
Bucket: config.bucket,
|
|
@@ -1947,9 +2405,9 @@ function createS3Storage(config) {
|
|
|
1947
2405
|
function createCloudinaryStorage(config) {
|
|
1948
2406
|
const getBaseUrl = () => `https://api.cloudinary.com/v1_1/${config.cloudName}/upload`;
|
|
1949
2407
|
const generateSignature = async (params) => {
|
|
1950
|
-
const
|
|
2408
|
+
const crypto4 = await import('crypto');
|
|
1951
2409
|
const sortedParams = Object.keys(params).sort().map((key) => `${key}=${params[key]}`).join("&");
|
|
1952
|
-
return
|
|
2410
|
+
return crypto4.createHash("sha256").update(sortedParams + config.apiSecret).digest("hex");
|
|
1953
2411
|
};
|
|
1954
2412
|
return {
|
|
1955
2413
|
name: "cloudinary",
|
|
@@ -2051,8 +2509,8 @@ function createCloudinaryStorage(config) {
|
|
|
2051
2509
|
public_id: publicId
|
|
2052
2510
|
};
|
|
2053
2511
|
const sortedParams = Object.keys(signatureParams).sort().map((key) => `${key}=${signatureParams[key]}`).join("&");
|
|
2054
|
-
const
|
|
2055
|
-
const signature =
|
|
2512
|
+
const crypto4 = await import('crypto');
|
|
2513
|
+
const signature = crypto4.createHash("sha256").update(sortedParams + config.apiSecret).digest("hex");
|
|
2056
2514
|
const deleteUrl = `https://api.cloudinary.com/v1_1/${config.cloudName}/image/destroy`;
|
|
2057
2515
|
const formData = new FormData();
|
|
2058
2516
|
formData.append("public_id", publicId);
|
|
@@ -2103,8 +2561,8 @@ function createCloudinaryStorage(config) {
|
|
|
2103
2561
|
timestamp: String(timestamp)
|
|
2104
2562
|
};
|
|
2105
2563
|
const sortedParams = Object.keys(signatureParams).sort().map((key) => `${key}=${signatureParams[key]}`).join("&");
|
|
2106
|
-
const
|
|
2107
|
-
const signature =
|
|
2564
|
+
const crypto4 = await import('crypto');
|
|
2565
|
+
const signature = crypto4.createHash("sha256").update(sortedParams + config.apiSecret).digest("hex");
|
|
2108
2566
|
const formData = new FormData();
|
|
2109
2567
|
formData.append("from_public_id", publicIdWithoutVersion);
|
|
2110
2568
|
formData.append("to_public_id", newPublicId);
|
|
@@ -2175,112 +2633,6 @@ function createCloudinaryStorage(config) {
|
|
|
2175
2633
|
}
|
|
2176
2634
|
};
|
|
2177
2635
|
}
|
|
2178
|
-
|
|
2179
|
-
// src/storage/imgix.ts
|
|
2180
|
-
function createImgixStorage(config) {
|
|
2181
|
-
const signUrl = (path2, params) => {
|
|
2182
|
-
if (!config.signKey) return path2;
|
|
2183
|
-
const signer = new TextEncoder();
|
|
2184
|
-
const key = signer.encode(config.signKey);
|
|
2185
|
-
const data = signer.encode(path2 + params.toString());
|
|
2186
|
-
let hash = 0;
|
|
2187
|
-
const combined = new Uint8Array(key.length + data.length);
|
|
2188
|
-
combined.set(key);
|
|
2189
|
-
combined.set(data, key.length);
|
|
2190
|
-
for (let i = 0; i < combined.length; i++) {
|
|
2191
|
-
hash = (hash << 5) - hash + combined[i] | 0;
|
|
2192
|
-
}
|
|
2193
|
-
params.set("s", Math.abs(hash).toString(16));
|
|
2194
|
-
return path2;
|
|
2195
|
-
};
|
|
2196
|
-
return {
|
|
2197
|
-
name: "imgix",
|
|
2198
|
-
displayName: "Imgix",
|
|
2199
|
-
supportsDynamicResize: true,
|
|
2200
|
-
async upload(_file, _options) {
|
|
2201
|
-
throw new Error(
|
|
2202
|
-
"Imgix is a transformation service. Use another provider for uploads."
|
|
2203
|
-
);
|
|
2204
|
-
},
|
|
2205
|
-
async uploadFromUrl(url, options) {
|
|
2206
|
-
const filename = options?.filename || url.split("/").pop() || "file";
|
|
2207
|
-
const response = await fetch(url);
|
|
2208
|
-
if (!response.ok) {
|
|
2209
|
-
throw new Error(`Failed to fetch: ${response.statusText}`);
|
|
2210
|
-
}
|
|
2211
|
-
const blob = await response.blob();
|
|
2212
|
-
new File([blob], filename, { type: blob.type });
|
|
2213
|
-
return {
|
|
2214
|
-
id: Buffer.from(url).toString("base64").slice(0, 20),
|
|
2215
|
-
filename,
|
|
2216
|
-
originalName: filename,
|
|
2217
|
-
mimeType: blob.type,
|
|
2218
|
-
size: blob.size,
|
|
2219
|
-
url: this.getImageUrl(url),
|
|
2220
|
-
thumbnailUrl: this.getImageUrl(url, {
|
|
2221
|
-
width: 200,
|
|
2222
|
-
height: 200,
|
|
2223
|
-
fit: "crop"
|
|
2224
|
-
}),
|
|
2225
|
-
provider: "imgix",
|
|
2226
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2227
|
-
};
|
|
2228
|
-
},
|
|
2229
|
-
async delete(_url) {
|
|
2230
|
-
},
|
|
2231
|
-
async rename(_oldUrl, newKey) {
|
|
2232
|
-
return `https://${config.domain}/${newKey}`;
|
|
2233
|
-
},
|
|
2234
|
-
getImageUrl(url, transforms) {
|
|
2235
|
-
const parsed = new URL(url);
|
|
2236
|
-
const params = new URLSearchParams(parsed.search);
|
|
2237
|
-
if (config.defaultParameters) {
|
|
2238
|
-
Object.entries(config.defaultParameters).forEach(([key, value]) => {
|
|
2239
|
-
if (!params.has(key)) {
|
|
2240
|
-
params.set(key, value);
|
|
2241
|
-
}
|
|
2242
|
-
});
|
|
2243
|
-
}
|
|
2244
|
-
if (transforms) {
|
|
2245
|
-
if (transforms.width) params.set("w", String(transforms.width));
|
|
2246
|
-
if (transforms.height) params.set("h", String(transforms.height));
|
|
2247
|
-
if (transforms.quality) params.set("q", String(transforms.quality));
|
|
2248
|
-
if (transforms.format) params.set("fm", transforms.format);
|
|
2249
|
-
if (transforms.fit) params.set("fit", transforms.fit);
|
|
2250
|
-
if (transforms.blur) params.set("blur", String(transforms.blur));
|
|
2251
|
-
if (transforms.sharpen) params.set("sharp", String(transforms.sharpen));
|
|
2252
|
-
}
|
|
2253
|
-
params.set("auto", "compress,format");
|
|
2254
|
-
parsed.pathname + "?" + params.toString();
|
|
2255
|
-
if (config.signKey) {
|
|
2256
|
-
signUrl(parsed.pathname + params.toString(), params);
|
|
2257
|
-
}
|
|
2258
|
-
return `https://${config.domain}${parsed.pathname}${params.toString() ? "?" + params.toString() : ""}`;
|
|
2259
|
-
},
|
|
2260
|
-
async generateThumbnail(file) {
|
|
2261
|
-
return this.getImageUrl(file.url, {
|
|
2262
|
-
width: 200,
|
|
2263
|
-
height: 200,
|
|
2264
|
-
fit: "crop"
|
|
2265
|
-
});
|
|
2266
|
-
},
|
|
2267
|
-
async list() {
|
|
2268
|
-
return [];
|
|
2269
|
-
},
|
|
2270
|
-
async exists(url) {
|
|
2271
|
-
try {
|
|
2272
|
-
const response = await fetch(url, { method: "HEAD" });
|
|
2273
|
-
return response.ok;
|
|
2274
|
-
} catch {
|
|
2275
|
-
return false;
|
|
2276
|
-
}
|
|
2277
|
-
},
|
|
2278
|
-
async createFolder() {
|
|
2279
|
-
},
|
|
2280
|
-
async deleteFolder() {
|
|
2281
|
-
}
|
|
2282
|
-
};
|
|
2283
|
-
}
|
|
2284
2636
|
function createFtpStorage(config) {
|
|
2285
2637
|
let client = null;
|
|
2286
2638
|
async function getClient() {
|
|
@@ -2298,15 +2650,15 @@ function createFtpStorage(config) {
|
|
|
2298
2650
|
}
|
|
2299
2651
|
return client;
|
|
2300
2652
|
}
|
|
2301
|
-
const getKey = (
|
|
2653
|
+
const getKey = (path3) => {
|
|
2302
2654
|
const prefix = config.prefix ? `${config.prefix}/` : "";
|
|
2303
|
-
return `${prefix}${
|
|
2655
|
+
return `${prefix}${path3}`.replace(/\/+/g, "/");
|
|
2304
2656
|
};
|
|
2305
|
-
const
|
|
2657
|
+
const getUrl2 = (key) => {
|
|
2306
2658
|
const base = config.baseUrl.replace(/\/$/, "");
|
|
2307
2659
|
return `${base}/${key}`;
|
|
2308
2660
|
};
|
|
2309
|
-
const
|
|
2661
|
+
const getUrlPrefix3 = () => {
|
|
2310
2662
|
const base = config.baseUrl.replace(/\/$/, "");
|
|
2311
2663
|
return base + "/";
|
|
2312
2664
|
};
|
|
@@ -2337,8 +2689,8 @@ function createFtpStorage(config) {
|
|
|
2337
2689
|
originalName: file.name,
|
|
2338
2690
|
mimeType: file.type,
|
|
2339
2691
|
size: buffer.length,
|
|
2340
|
-
url:
|
|
2341
|
-
thumbnailUrl: file.type.startsWith("image/") ?
|
|
2692
|
+
url: getUrl2(key),
|
|
2693
|
+
thumbnailUrl: file.type.startsWith("image/") ? getUrl2(key) : void 0,
|
|
2342
2694
|
folder: options?.folder,
|
|
2343
2695
|
provider: config.type,
|
|
2344
2696
|
metadata: options?.metadata,
|
|
@@ -2357,15 +2709,15 @@ function createFtpStorage(config) {
|
|
|
2357
2709
|
},
|
|
2358
2710
|
async delete(url) {
|
|
2359
2711
|
const ftp = await getClient();
|
|
2360
|
-
const key = url.replace(
|
|
2712
|
+
const key = url.replace(getUrlPrefix3(), "");
|
|
2361
2713
|
await ftp.remove(key);
|
|
2362
2714
|
},
|
|
2363
2715
|
async rename(oldUrl, newKey) {
|
|
2364
2716
|
const ftp = await getClient();
|
|
2365
|
-
const oldKey = oldUrl.replace(
|
|
2717
|
+
const oldKey = oldUrl.replace(getUrlPrefix3(), "");
|
|
2366
2718
|
const fullPath = config.prefix ? `${config.prefix}/${newKey}` : newKey;
|
|
2367
2719
|
await ftp.rename(oldKey, fullPath);
|
|
2368
|
-
return
|
|
2720
|
+
return getUrl2(fullPath);
|
|
2369
2721
|
},
|
|
2370
2722
|
getImageUrl(url, transforms) {
|
|
2371
2723
|
if (!transforms || Object.keys(transforms).length === 0) return url;
|
|
@@ -2394,14 +2746,14 @@ function createFtpStorage(config) {
|
|
|
2394
2746
|
originalName: item.name,
|
|
2395
2747
|
mimeType: "application/octet-stream",
|
|
2396
2748
|
size: Number(item.size) || 0,
|
|
2397
|
-
url:
|
|
2749
|
+
url: getUrl2(`${key}/${item.name}`.replace(/\/+/g, "/")),
|
|
2398
2750
|
provider: config.type,
|
|
2399
2751
|
createdAt: item.modifiedAt ? item.modifiedAt.toISOString() : (/* @__PURE__ */ new Date()).toISOString()
|
|
2400
2752
|
}));
|
|
2401
2753
|
},
|
|
2402
2754
|
async exists(url) {
|
|
2403
2755
|
const ftp = await getClient();
|
|
2404
|
-
const key = url.replace(
|
|
2756
|
+
const key = url.replace(getUrlPrefix3(), "");
|
|
2405
2757
|
try {
|
|
2406
2758
|
await ftp.size(key);
|
|
2407
2759
|
return true;
|
|
@@ -2425,105 +2777,17 @@ function createFtpStorage(config) {
|
|
|
2425
2777
|
// src/storage/index.ts
|
|
2426
2778
|
async function resolveProvider(configService) {
|
|
2427
2779
|
const config = configService.getStorageConfig();
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
});
|
|
2440
|
-
case "r2":
|
|
2441
|
-
return createS3Storage({
|
|
2442
|
-
provider: "r2",
|
|
2443
|
-
bucket: config.r2.bucket || "",
|
|
2444
|
-
region: "auto",
|
|
2445
|
-
accessKeyId: config.r2.accessKeyId || "",
|
|
2446
|
-
secretAccessKey: config.r2.secretAccessKey || "",
|
|
2447
|
-
accountId: config.r2.accountId || "",
|
|
2448
|
-
publicDevUrl: config.r2.publicDevUrl,
|
|
2449
|
-
endpoint: `https://${config.r2.accountId || ""}.r2.cloudflarestorage.com`,
|
|
2450
|
-
cdnUrl: config.r2.cdnUrl,
|
|
2451
|
-
prefix: config.r2.prefix
|
|
2452
|
-
});
|
|
2453
|
-
case "gcs":
|
|
2454
|
-
return createS3Storage({
|
|
2455
|
-
provider: "gcs",
|
|
2456
|
-
bucket: config.gcs.bucket || "",
|
|
2457
|
-
region: config.gcs.projectId || "auto",
|
|
2458
|
-
accessKeyId: config.gcs.clientEmail || "",
|
|
2459
|
-
secretAccessKey: config.gcs.privateKey || "",
|
|
2460
|
-
cdnUrl: config.gcs.cdnUrl,
|
|
2461
|
-
prefix: config.gcs.prefix
|
|
2462
|
-
});
|
|
2463
|
-
case "digitalocean":
|
|
2464
|
-
return createS3Storage({
|
|
2465
|
-
provider: "digitalocean",
|
|
2466
|
-
bucket: config.digitalocean.bucket || "",
|
|
2467
|
-
region: config.digitalocean.region || "nyc3",
|
|
2468
|
-
accessKeyId: config.digitalocean.accessKeyId || "",
|
|
2469
|
-
secretAccessKey: config.digitalocean.secretAccessKey || "",
|
|
2470
|
-
endpoint: `https://${config.digitalocean.region || "nyc3"}.digitaloceanspaces.com`,
|
|
2471
|
-
cdnUrl: config.digitalocean.cdnUrl,
|
|
2472
|
-
prefix: config.digitalocean.prefix
|
|
2473
|
-
});
|
|
2474
|
-
case "backblaze":
|
|
2475
|
-
return createS3Storage({
|
|
2476
|
-
provider: "backblaze",
|
|
2477
|
-
bucket: config.backblaze.bucket || "",
|
|
2478
|
-
region: "auto",
|
|
2479
|
-
accessKeyId: config.backblaze.applicationKeyId || "",
|
|
2480
|
-
secretAccessKey: config.backblaze.applicationKey || "",
|
|
2481
|
-
accountId: config.backblaze.accountId || "",
|
|
2482
|
-
endpoint: `https://s3.backblazeb2.com`,
|
|
2483
|
-
cdnUrl: config.backblaze.cdnUrl,
|
|
2484
|
-
prefix: config.backblaze.prefix
|
|
2485
|
-
});
|
|
2486
|
-
case "wasabi":
|
|
2487
|
-
return createS3Storage({
|
|
2488
|
-
provider: "wasabi",
|
|
2489
|
-
bucket: config.wasabi.bucket || "",
|
|
2490
|
-
region: config.wasabi.region || "us-east-1",
|
|
2491
|
-
accessKeyId: config.wasabi.accessKeyId || "",
|
|
2492
|
-
secretAccessKey: config.wasabi.secretAccessKey || "",
|
|
2493
|
-
endpoint: `https://s3.${config.wasabi.region || "us-east-1"}.wasabisys.com`,
|
|
2494
|
-
cdnUrl: config.wasabi.cdnUrl,
|
|
2495
|
-
prefix: config.wasabi.prefix
|
|
2496
|
-
});
|
|
2497
|
-
case "ftp":
|
|
2498
|
-
case "sftp":
|
|
2499
|
-
return createFtpStorage({
|
|
2500
|
-
host: config.ftp?.host || "",
|
|
2501
|
-
port: config.ftp?.port || 21,
|
|
2502
|
-
user: config.ftp?.user || "",
|
|
2503
|
-
password: config.ftp?.password || "",
|
|
2504
|
-
secure: config.ftp?.secure || false,
|
|
2505
|
-
baseUrl: config.ftp?.baseUrl || "",
|
|
2506
|
-
prefix: config.ftp?.prefix,
|
|
2507
|
-
type: "ftp"
|
|
2508
|
-
});
|
|
2509
|
-
case "cloudinary":
|
|
2510
|
-
return createCloudinaryStorage({
|
|
2511
|
-
cloudName: config.cloudinary.cloudName || "",
|
|
2512
|
-
apiKey: config.cloudinary.apiKey || "",
|
|
2513
|
-
apiSecret: config.cloudinary.apiSecret || "",
|
|
2514
|
-
folder: config.cloudinary.folder
|
|
2515
|
-
});
|
|
2516
|
-
case "imgix":
|
|
2517
|
-
return createImgixStorage({
|
|
2518
|
-
domain: config.imgix.domain || "",
|
|
2519
|
-
signKey: config.imgix.signKey
|
|
2520
|
-
});
|
|
2521
|
-
case "local":
|
|
2522
|
-
default:
|
|
2523
|
-
return createLocalStorage({
|
|
2524
|
-
uploadDir: config.local.uploadDir || path.join(process.cwd(), "public", "uploads"),
|
|
2525
|
-
baseUrl: config.local.baseUrl || "/uploads"
|
|
2526
|
-
});
|
|
2780
|
+
const registry = getDefaultRegistry();
|
|
2781
|
+
try {
|
|
2782
|
+
return await registry.resolve(config.type, config);
|
|
2783
|
+
} catch (err) {
|
|
2784
|
+
console.warn(
|
|
2785
|
+
`[resolveProvider] ${err.message} \u2014 falling back to local storage`
|
|
2786
|
+
);
|
|
2787
|
+
return createLocalStorage({
|
|
2788
|
+
uploadDir: path.join(process.cwd(), "public", "uploads"),
|
|
2789
|
+
baseUrl: "/uploads"
|
|
2790
|
+
});
|
|
2527
2791
|
}
|
|
2528
2792
|
}
|
|
2529
2793
|
async function resolveProviderWithConfig(config) {
|
|
@@ -2534,121 +2798,18 @@ async function resolveProviderWithConfig(config) {
|
|
|
2534
2798
|
baseUrl: "/uploads"
|
|
2535
2799
|
});
|
|
2536
2800
|
}
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
});
|
|
2550
|
-
case "r2":
|
|
2551
|
-
return createS3Storage({
|
|
2552
|
-
provider: "r2",
|
|
2553
|
-
bucket: config.r2?.bucket || "",
|
|
2554
|
-
region: "auto",
|
|
2555
|
-
accessKeyId: config.r2?.accessKeyId || "",
|
|
2556
|
-
secretAccessKey: config.r2?.secretAccessKey || "",
|
|
2557
|
-
accountId: config.r2?.accountId || "",
|
|
2558
|
-
publicDevUrl: config.r2?.publicDevUrl,
|
|
2559
|
-
endpoint: `https://${config.r2?.accountId || ""}.r2.cloudflarestorage.com`,
|
|
2560
|
-
cdnUrl: config.r2?.cdnUrl,
|
|
2561
|
-
prefix: config.r2?.prefix
|
|
2562
|
-
});
|
|
2563
|
-
case "gcs":
|
|
2564
|
-
return createS3Storage({
|
|
2565
|
-
provider: "gcs",
|
|
2566
|
-
bucket: config.gcs?.bucket || "",
|
|
2567
|
-
region: config.gcs?.projectId || "auto",
|
|
2568
|
-
accessKeyId: config.gcs?.clientEmail || "",
|
|
2569
|
-
secretAccessKey: config.gcs?.privateKey || "",
|
|
2570
|
-
cdnUrl: config.gcs?.cdnUrl,
|
|
2571
|
-
prefix: config.gcs?.prefix
|
|
2572
|
-
});
|
|
2573
|
-
case "digitalocean":
|
|
2574
|
-
return createS3Storage({
|
|
2575
|
-
provider: "digitalocean",
|
|
2576
|
-
bucket: config.digitalocean?.bucket || "",
|
|
2577
|
-
region: config.digitalocean?.region || "nyc3",
|
|
2578
|
-
accessKeyId: config.digitalocean?.accessKeyId || "",
|
|
2579
|
-
secretAccessKey: config.digitalocean?.secretAccessKey || "",
|
|
2580
|
-
cdnUrl: config.digitalocean?.cdnUrl,
|
|
2581
|
-
prefix: config.digitalocean?.prefix
|
|
2582
|
-
});
|
|
2583
|
-
case "backblaze":
|
|
2584
|
-
return createS3Storage({
|
|
2585
|
-
provider: "backblaze",
|
|
2586
|
-
bucket: config.backblaze?.bucket || "",
|
|
2587
|
-
region: "auto",
|
|
2588
|
-
accessKeyId: config.backblaze?.applicationKeyId || "",
|
|
2589
|
-
secretAccessKey: config.backblaze?.applicationKey || "",
|
|
2590
|
-
cdnUrl: config.backblaze?.cdnUrl,
|
|
2591
|
-
prefix: config.backblaze?.prefix
|
|
2592
|
-
});
|
|
2593
|
-
case "wasabi":
|
|
2594
|
-
return createS3Storage({
|
|
2595
|
-
provider: "wasabi",
|
|
2596
|
-
bucket: config.wasabi?.bucket || "",
|
|
2597
|
-
region: config.wasabi?.region || "us-east-1",
|
|
2598
|
-
accessKeyId: config.wasabi?.accessKeyId || "",
|
|
2599
|
-
secretAccessKey: config.wasabi?.secretAccessKey || "",
|
|
2600
|
-
cdnUrl: config.wasabi?.cdnUrl,
|
|
2601
|
-
prefix: config.wasabi?.prefix
|
|
2602
|
-
});
|
|
2603
|
-
case "cloudinary":
|
|
2604
|
-
return createCloudinaryStorage({
|
|
2605
|
-
cloudName: config.cloudinary?.cloudName || "",
|
|
2606
|
-
apiKey: config.cloudinary?.apiKey || "",
|
|
2607
|
-
apiSecret: config.cloudinary?.apiSecret || "",
|
|
2608
|
-
folder: config.cloudinary?.folder
|
|
2609
|
-
});
|
|
2610
|
-
case "ftp":
|
|
2611
|
-
case "sftp": {
|
|
2612
|
-
const ftpConf = config.ftp || config;
|
|
2613
|
-
return createFtpStorage({
|
|
2614
|
-
type: "ftp",
|
|
2615
|
-
host: ftpConf.host || "",
|
|
2616
|
-
port: ftpConf.port || 21,
|
|
2617
|
-
user: ftpConf.user || "",
|
|
2618
|
-
password: ftpConf.password || "",
|
|
2619
|
-
secure: ftpConf.secure || false,
|
|
2620
|
-
baseUrl: ftpConf.baseUrl || "",
|
|
2621
|
-
prefix: ftpConf.prefix
|
|
2622
|
-
});
|
|
2623
|
-
}
|
|
2624
|
-
case "local":
|
|
2625
|
-
default: {
|
|
2626
|
-
const localConfig = config.local || {
|
|
2627
|
-
uploadDir: config["local.uploadDir"],
|
|
2628
|
-
baseUrl: config["local.baseUrl"]
|
|
2629
|
-
};
|
|
2630
|
-
const savedUploadDir = (localConfig?.uploadDir || "").trim();
|
|
2631
|
-
let uploadDir;
|
|
2632
|
-
if (savedUploadDir) {
|
|
2633
|
-
if (path.isAbsolute(savedUploadDir)) {
|
|
2634
|
-
uploadDir = savedUploadDir;
|
|
2635
|
-
} else if (savedUploadDir.includes("/") || savedUploadDir.includes("\\")) {
|
|
2636
|
-
uploadDir = path.resolve(process.cwd(), savedUploadDir);
|
|
2637
|
-
} else {
|
|
2638
|
-
uploadDir = path.join(process.cwd(), "public", savedUploadDir);
|
|
2639
|
-
}
|
|
2640
|
-
} else {
|
|
2641
|
-
uploadDir = path.join(process.cwd(), "public", "uploads");
|
|
2642
|
-
}
|
|
2643
|
-
const savedBaseUrl = (localConfig?.baseUrl || "").trim();
|
|
2644
|
-
let baseUrl;
|
|
2645
|
-
if (savedBaseUrl) {
|
|
2646
|
-
baseUrl = savedBaseUrl.startsWith("/") ? savedBaseUrl : `/${savedBaseUrl}`;
|
|
2647
|
-
} else {
|
|
2648
|
-
baseUrl = "/uploads";
|
|
2649
|
-
}
|
|
2650
|
-
return createLocalStorage({ uploadDir, baseUrl });
|
|
2651
|
-
}
|
|
2801
|
+
const type = config.type || "local";
|
|
2802
|
+
const registry = getDefaultRegistry();
|
|
2803
|
+
try {
|
|
2804
|
+
return await registry.resolveWithConfig(type, config);
|
|
2805
|
+
} catch (err) {
|
|
2806
|
+
console.warn(
|
|
2807
|
+
`[resolveProviderWithConfig] ${err.message} \u2014 falling back to local storage`
|
|
2808
|
+
);
|
|
2809
|
+
return createLocalStorage({
|
|
2810
|
+
uploadDir: path.join(process.cwd(), "public", "uploads"),
|
|
2811
|
+
baseUrl: "/uploads"
|
|
2812
|
+
});
|
|
2652
2813
|
}
|
|
2653
2814
|
}
|
|
2654
2815
|
async function processImage(buffer) {
|
|
@@ -3252,8 +3413,33 @@ var MediaService = class _MediaService {
|
|
|
3252
3413
|
}
|
|
3253
3414
|
}
|
|
3254
3415
|
};
|
|
3255
|
-
|
|
3256
|
-
|
|
3416
|
+
function formatZodErrors(errors) {
|
|
3417
|
+
return errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
|
|
3418
|
+
}
|
|
3419
|
+
function convertRichtextFields(fields, data) {
|
|
3420
|
+
if (!data || typeof data !== "object") return;
|
|
3421
|
+
for (const field of fields) {
|
|
3422
|
+
if (field.type === "richtext" && field.name) {
|
|
3423
|
+
const val = data[field.name];
|
|
3424
|
+
if (typeof val === "string") {
|
|
3425
|
+
data[field.name] = [{ type: "paragraph", children: [{ text: val }] }];
|
|
3426
|
+
} else if (val && typeof val === "object" && !Array.isArray(val) && val.type === "doc" && Array.isArray(val.content)) {
|
|
3427
|
+
data[field.name] = val.content;
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
if (field.type === "tabs" && field.name && Array.isArray(field.tabs) && data[field.name] && typeof data[field.name] === "object") {
|
|
3431
|
+
for (const tab of field.tabs) {
|
|
3432
|
+
if (Array.isArray(tab.fields)) convertRichtextFields(tab.fields, data[field.name]);
|
|
3433
|
+
}
|
|
3434
|
+
} else if ((field.type === "group" || field.type === "collapsible") && field.name && Array.isArray(field.fields) && data[field.name] && typeof data[field.name] === "object") {
|
|
3435
|
+
convertRichtextFields(field.fields, data[field.name]);
|
|
3436
|
+
} else if (field.type === "array" && field.name && Array.isArray(field.fields) && Array.isArray(data[field.name])) {
|
|
3437
|
+
for (const item of data[field.name]) {
|
|
3438
|
+
if (item && typeof item === "object") convertRichtextFields(field.fields, item);
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3257
3443
|
var COLLECTION_EVENT_MAP = {
|
|
3258
3444
|
_media: {
|
|
3259
3445
|
create: WEBHOOK_EVENTS.MEDIA_UPLOAD,
|
|
@@ -3406,31 +3592,30 @@ async function checkGlobalAccess(global, operation, req, ctxUser, ctxTenantID, e
|
|
|
3406
3592
|
return { allowed: true };
|
|
3407
3593
|
}
|
|
3408
3594
|
async function resolveAuthContext(req, authMw, staticUser, staticTenantID) {
|
|
3409
|
-
if (
|
|
3595
|
+
if (staticUser) {
|
|
3410
3596
|
return {
|
|
3411
3597
|
user: staticUser,
|
|
3412
3598
|
tenantID: staticTenantID,
|
|
3413
|
-
apiKeyContext: void 0
|
|
3599
|
+
apiKeyContext: void 0,
|
|
3600
|
+
authType: "static"
|
|
3414
3601
|
};
|
|
3415
3602
|
}
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
if (result.status === 401) {
|
|
3427
|
-
return { user: void 0, tenantID: void 0, apiKeyContext: void 0 };
|
|
3603
|
+
if (authMw) {
|
|
3604
|
+
const res = await authMw(req);
|
|
3605
|
+
if (res.status === 200 && res.user) {
|
|
3606
|
+
return {
|
|
3607
|
+
user: res.user,
|
|
3608
|
+
tenantID: res.tenantContext?.tenantId,
|
|
3609
|
+
apiKeyContext: res.apiKeyContext,
|
|
3610
|
+
authType: res.authType
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3428
3613
|
}
|
|
3429
3614
|
return {
|
|
3430
|
-
user:
|
|
3431
|
-
tenantID:
|
|
3432
|
-
apiKeyContext:
|
|
3433
|
-
authType:
|
|
3615
|
+
user: void 0,
|
|
3616
|
+
tenantID: void 0,
|
|
3617
|
+
apiKeyContext: void 0,
|
|
3618
|
+
authType: void 0
|
|
3434
3619
|
};
|
|
3435
3620
|
}
|
|
3436
3621
|
function createDefaultAuthAdapter(db, rootDir) {
|
|
@@ -3545,78 +3730,15 @@ function createHonoApp(options) {
|
|
|
3545
3730
|
rateLimiter
|
|
3546
3731
|
});
|
|
3547
3732
|
app.post("/api/auth/login", async (c) => authRoutes.login(c.req.raw));
|
|
3548
|
-
app.post("/api/auth/register", async (c) => authRoutes.register(c.req.raw));
|
|
3549
|
-
app.post("/api/auth/logout", async (c) => authRoutes.logout(c.req.raw));
|
|
3550
|
-
app.post("/api/auth/refresh", async (c) => authRoutes.refresh(c.req.raw));
|
|
3551
|
-
app.get("/api/auth/me", async (c) => authRoutes.me(c.req.raw));
|
|
3552
|
-
app.get("/api/auth/sessions", async (c) => authRoutes.listSessions(c.req.raw));
|
|
3553
|
-
app.post("/api/auth/sessions/refresh", async (c) => authRoutes.refreshSession(c.req.raw));
|
|
3554
|
-
app.delete("/api/auth/sessions", async (c) => authRoutes.revokeOtherSessions(c.req.raw));
|
|
3555
|
-
app.delete("/api/auth/sessions/:id", async (c) => authRoutes.revokeSession(c.req.raw, c.req.param("id")));
|
|
3556
|
-
app.put("/api/auth/sessions/:id/name", async (c) => authRoutes.renameSession(c.req.raw, c.req.param("id")));
|
|
3557
|
-
app.post("/api/graphql", async (c) => {
|
|
3558
|
-
try {
|
|
3559
|
-
const req = c.req.raw;
|
|
3560
|
-
const apiKeyRaw = extractApiKeyFromRequest(req);
|
|
3561
|
-
if (apiKeyRaw && db) {
|
|
3562
|
-
const apiKeyResult = await validateApiKey(apiKeyRaw, db);
|
|
3563
|
-
if (!apiKeyResult.valid) {
|
|
3564
|
-
return c.json({ errors: [{ message: apiKeyResult.error || "Invalid API key" }] }, 401);
|
|
3565
|
-
}
|
|
3566
|
-
const apiKeyId = apiKeyResult.apiKeyId || "";
|
|
3567
|
-
await sessionAuthAdapter?.createAuditLog({
|
|
3568
|
-
action: "api_key_request",
|
|
3569
|
-
userId: apiKeyResult.userId || "",
|
|
3570
|
-
resource: "api_key",
|
|
3571
|
-
resourceId: apiKeyId,
|
|
3572
|
-
success: true,
|
|
3573
|
-
metadata: {
|
|
3574
|
-
endpoint: "/api/graphql",
|
|
3575
|
-
method: "POST",
|
|
3576
|
-
ip: extractIp(req)
|
|
3577
|
-
}
|
|
3578
|
-
});
|
|
3579
|
-
}
|
|
3580
|
-
const body = await req.json().catch(() => ({}));
|
|
3581
|
-
const { query, variables } = body;
|
|
3582
|
-
if (!query) {
|
|
3583
|
-
return c.json({ errors: [{ message: "No query provided" }] }, 400);
|
|
3584
|
-
}
|
|
3585
|
-
let gqlUser;
|
|
3586
|
-
let apiKeyCtx;
|
|
3587
|
-
if (apiKeyRaw && db) {
|
|
3588
|
-
const apiKeyResult = await validateApiKey(apiKeyRaw, db);
|
|
3589
|
-
if (apiKeyResult.valid && apiKeyResult.user) {
|
|
3590
|
-
gqlUser = apiKeyResult.user;
|
|
3591
|
-
apiKeyCtx = createApiKeyContext(apiKeyResult);
|
|
3592
|
-
}
|
|
3593
|
-
}
|
|
3594
|
-
const schema = buildGraphQLSchema({
|
|
3595
|
-
registry,
|
|
3596
|
-
db,
|
|
3597
|
-
user: gqlUser,
|
|
3598
|
-
req,
|
|
3599
|
-
settings
|
|
3600
|
-
});
|
|
3601
|
-
const document = parse(query);
|
|
3602
|
-
const result = await execute({
|
|
3603
|
-
schema,
|
|
3604
|
-
document,
|
|
3605
|
-
variableValues: variables,
|
|
3606
|
-
contextValue: { user: gqlUser, apiKeyContext: apiKeyCtx, req, db }
|
|
3607
|
-
});
|
|
3608
|
-
return c.json(result);
|
|
3609
|
-
} catch (error) {
|
|
3610
|
-
if (error.message?.includes("GraphQL is disabled")) {
|
|
3611
|
-
return c.json({ errors: [{ message: "GraphQL API is disabled" }] }, 403);
|
|
3612
|
-
}
|
|
3613
|
-
if (error instanceof SyntaxError) {
|
|
3614
|
-
return c.json({ errors: [{ message: "Invalid request body" }] }, 400);
|
|
3615
|
-
}
|
|
3616
|
-
console.error("[GraphQL] execution error:", error);
|
|
3617
|
-
return c.json({ errors: [{ message: error.message || "GraphQL execution failed" }] }, 500);
|
|
3618
|
-
}
|
|
3619
|
-
});
|
|
3733
|
+
app.post("/api/auth/register", async (c) => authRoutes.register(c.req.raw));
|
|
3734
|
+
app.post("/api/auth/logout", async (c) => authRoutes.logout(c.req.raw));
|
|
3735
|
+
app.post("/api/auth/refresh", async (c) => authRoutes.refresh(c.req.raw));
|
|
3736
|
+
app.get("/api/auth/me", async (c) => authRoutes.me(c.req.raw));
|
|
3737
|
+
app.get("/api/auth/sessions", async (c) => authRoutes.listSessions(c.req.raw));
|
|
3738
|
+
app.post("/api/auth/sessions/refresh", async (c) => authRoutes.refreshSession(c.req.raw));
|
|
3739
|
+
app.delete("/api/auth/sessions", async (c) => authRoutes.revokeOtherSessions(c.req.raw));
|
|
3740
|
+
app.delete("/api/auth/sessions/:id", async (c) => authRoutes.revokeSession(c.req.raw, c.req.param("id")));
|
|
3741
|
+
app.put("/api/auth/sessions/:id/name", async (c) => authRoutes.renameSession(c.req.raw, c.req.param("id")));
|
|
3620
3742
|
app.get("/api/auth/access", async (c) => {
|
|
3621
3743
|
try {
|
|
3622
3744
|
const { user: ctxUser, tenantID: ctxTenantID } = await resolveAuthContext(
|
|
@@ -3701,7 +3823,13 @@ function createHonoApp(options) {
|
|
|
3701
3823
|
return c.json({ error: error.message }, 500);
|
|
3702
3824
|
}
|
|
3703
3825
|
});
|
|
3704
|
-
const
|
|
3826
|
+
const usersCollection2 = typeof registry.hasCollection === "function" && registry.hasCollection("users") ? registry.getCollection("users") : (() => {
|
|
3827
|
+
try {
|
|
3828
|
+
return registry.getCollection("users");
|
|
3829
|
+
} catch {
|
|
3830
|
+
return usersCollection;
|
|
3831
|
+
}
|
|
3832
|
+
})();
|
|
3705
3833
|
app.get("/api/users", async (c) => {
|
|
3706
3834
|
try {
|
|
3707
3835
|
const { user: ctxUser, tenantID: ctxTenantID } = await resolveAuthContext(
|
|
@@ -3711,7 +3839,7 @@ function createHonoApp(options) {
|
|
|
3711
3839
|
tenantID
|
|
3712
3840
|
);
|
|
3713
3841
|
const access = await checkCollectionAccess(
|
|
3714
|
-
|
|
3842
|
+
usersCollection2,
|
|
3715
3843
|
"read",
|
|
3716
3844
|
c.req.raw,
|
|
3717
3845
|
ctxUser,
|
|
@@ -3756,19 +3884,6 @@ function createHonoApp(options) {
|
|
|
3756
3884
|
user,
|
|
3757
3885
|
tenantID
|
|
3758
3886
|
);
|
|
3759
|
-
const access = await checkCollectionAccess(
|
|
3760
|
-
usersCollection,
|
|
3761
|
-
"read",
|
|
3762
|
-
c.req.raw,
|
|
3763
|
-
ctxUser,
|
|
3764
|
-
ctxTenantID,
|
|
3765
|
-
void 0,
|
|
3766
|
-
enablePublicAccess,
|
|
3767
|
-
defaultCollectionAccess
|
|
3768
|
-
);
|
|
3769
|
-
if (!access.allowed) {
|
|
3770
|
-
return c.json({ error: access.error }, access.status || 403);
|
|
3771
|
-
}
|
|
3772
3887
|
const id = c.req.param("id");
|
|
3773
3888
|
const found = await sessionAuthAdapter.findUserById(id);
|
|
3774
3889
|
if (!found) {
|
|
@@ -3788,7 +3903,7 @@ function createHonoApp(options) {
|
|
|
3788
3903
|
tenantID
|
|
3789
3904
|
);
|
|
3790
3905
|
const access = await checkCollectionAccess(
|
|
3791
|
-
|
|
3906
|
+
usersCollection2,
|
|
3792
3907
|
"create",
|
|
3793
3908
|
c.req.raw,
|
|
3794
3909
|
ctxUser,
|
|
@@ -3813,8 +3928,18 @@ function createHonoApp(options) {
|
|
|
3813
3928
|
password: body.password,
|
|
3814
3929
|
name: body.name,
|
|
3815
3930
|
role: body.role || "customer",
|
|
3931
|
+
avatar: body.avatar,
|
|
3816
3932
|
tenantId: body.tenantId
|
|
3817
3933
|
});
|
|
3934
|
+
if (ctxUser) {
|
|
3935
|
+
sessionAuthAdapter?.createAuditLog({
|
|
3936
|
+
action: "user_create",
|
|
3937
|
+
userId: ctxUser.id,
|
|
3938
|
+
resource: "users",
|
|
3939
|
+
resourceId: created.id,
|
|
3940
|
+
success: true
|
|
3941
|
+
});
|
|
3942
|
+
}
|
|
3818
3943
|
return c.json(
|
|
3819
3944
|
{ data: created, message: "User created successfully" },
|
|
3820
3945
|
201
|
|
@@ -3832,7 +3957,7 @@ function createHonoApp(options) {
|
|
|
3832
3957
|
tenantID
|
|
3833
3958
|
);
|
|
3834
3959
|
const access = await checkCollectionAccess(
|
|
3835
|
-
|
|
3960
|
+
usersCollection2,
|
|
3836
3961
|
"update",
|
|
3837
3962
|
c.req.raw,
|
|
3838
3963
|
ctxUser,
|
|
@@ -3854,6 +3979,9 @@ function createHonoApp(options) {
|
|
|
3854
3979
|
if (body.name !== void 0) updateData.name = body.name;
|
|
3855
3980
|
if (body.email !== void 0) updateData.email = body.email;
|
|
3856
3981
|
if (body.role !== void 0) updateData.role = body.role;
|
|
3982
|
+
if (body.avatar !== void 0) {
|
|
3983
|
+
updateData.avatar = typeof body.avatar === "object" && body.avatar !== null ? body.avatar.id || String(body.avatar) : body.avatar;
|
|
3984
|
+
}
|
|
3857
3985
|
if (body.tenantId !== void 0) updateData.tenantId = body.tenantId;
|
|
3858
3986
|
if (body.emailVerified !== void 0)
|
|
3859
3987
|
updateData.emailVerified = body.emailVerified;
|
|
@@ -3865,6 +3993,25 @@ function createHonoApp(options) {
|
|
|
3865
3993
|
if (!updated) {
|
|
3866
3994
|
return c.json({ error: "User update failed" }, 500);
|
|
3867
3995
|
}
|
|
3996
|
+
if (ctxUser) {
|
|
3997
|
+
sessionAuthAdapter?.createAuditLog({
|
|
3998
|
+
action: "user_update",
|
|
3999
|
+
userId: ctxUser.id,
|
|
4000
|
+
resource: "users",
|
|
4001
|
+
resourceId: id,
|
|
4002
|
+
success: true
|
|
4003
|
+
});
|
|
4004
|
+
if (body.role && existing.role !== body.role) {
|
|
4005
|
+
sessionAuthAdapter?.createAuditLog({
|
|
4006
|
+
action: "role_change",
|
|
4007
|
+
userId: ctxUser.id,
|
|
4008
|
+
resource: "users",
|
|
4009
|
+
resourceId: id,
|
|
4010
|
+
success: true,
|
|
4011
|
+
metadata: { oldRole: existing.role, newRole: body.role }
|
|
4012
|
+
});
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
3868
4015
|
return c.json({ data: updated, message: "User updated successfully" });
|
|
3869
4016
|
} catch (error) {
|
|
3870
4017
|
return c.json({ error: error.message }, 500);
|
|
@@ -3879,7 +4026,7 @@ function createHonoApp(options) {
|
|
|
3879
4026
|
tenantID
|
|
3880
4027
|
);
|
|
3881
4028
|
const access = await checkCollectionAccess(
|
|
3882
|
-
|
|
4029
|
+
usersCollection2,
|
|
3883
4030
|
"delete",
|
|
3884
4031
|
c.req.raw,
|
|
3885
4032
|
ctxUser,
|
|
@@ -3903,6 +4050,15 @@ function createHonoApp(options) {
|
|
|
3903
4050
|
if (!deleted) {
|
|
3904
4051
|
return c.json({ error: "User deletion failed" }, 500);
|
|
3905
4052
|
}
|
|
4053
|
+
if (ctxUser) {
|
|
4054
|
+
sessionAuthAdapter?.createAuditLog({
|
|
4055
|
+
action: "user_delete",
|
|
4056
|
+
userId: ctxUser.id,
|
|
4057
|
+
resource: "users",
|
|
4058
|
+
resourceId: id,
|
|
4059
|
+
success: true
|
|
4060
|
+
});
|
|
4061
|
+
}
|
|
3906
4062
|
return c.json({ data: existing, message: "User deleted successfully" });
|
|
3907
4063
|
} catch (error) {
|
|
3908
4064
|
return c.json({ error: error.message }, 500);
|
|
@@ -4059,11 +4215,11 @@ function createHonoApp(options) {
|
|
|
4059
4215
|
const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
|
|
4060
4216
|
if (!ctxUser) return c.json({ error: "Authentication required" }, 401);
|
|
4061
4217
|
const service = await getMedia();
|
|
4062
|
-
const
|
|
4063
|
-
if (!
|
|
4218
|
+
const path3 = c.req.query("path");
|
|
4219
|
+
if (!path3) {
|
|
4064
4220
|
return c.json({ error: "Path is required" }, 400);
|
|
4065
4221
|
}
|
|
4066
|
-
await service.deleteFolder(
|
|
4222
|
+
await service.deleteFolder(path3);
|
|
4067
4223
|
return c.json({ message: "Folder deleted" });
|
|
4068
4224
|
} catch (error) {
|
|
4069
4225
|
console.error("[Media] delete folder error:", error);
|
|
@@ -4216,6 +4372,119 @@ function createHonoApp(options) {
|
|
|
4216
4372
|
return c.json({ error: error.message }, 500);
|
|
4217
4373
|
}
|
|
4218
4374
|
});
|
|
4375
|
+
app.get("/api/plugins", async (c) => {
|
|
4376
|
+
try {
|
|
4377
|
+
const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
|
|
4378
|
+
if (!ctxUser) return c.json({ error: "Authentication required" }, 401);
|
|
4379
|
+
const plugins = registry.getPlugins();
|
|
4380
|
+
const storageRegistry = registry.storageProviders;
|
|
4381
|
+
const pluginList = await Promise.all(
|
|
4382
|
+
plugins.map(async (p) => {
|
|
4383
|
+
let enabled = true;
|
|
4384
|
+
const pluginName = p.name;
|
|
4385
|
+
let states = {};
|
|
4386
|
+
try {
|
|
4387
|
+
const doc = await db.findOne({
|
|
4388
|
+
collection: "_globals_plugin-settings",
|
|
4389
|
+
where: {}
|
|
4390
|
+
});
|
|
4391
|
+
if (doc && doc.states) states = doc.states;
|
|
4392
|
+
} catch {
|
|
4393
|
+
}
|
|
4394
|
+
if (states[pluginName] !== void 0) {
|
|
4395
|
+
enabled = states[pluginName];
|
|
4396
|
+
}
|
|
4397
|
+
storageRegistry.setPluginEnabled(pluginName, enabled);
|
|
4398
|
+
return {
|
|
4399
|
+
id: pluginName,
|
|
4400
|
+
name: pluginName,
|
|
4401
|
+
version: p.version || "1.0.0",
|
|
4402
|
+
description: p.description || "",
|
|
4403
|
+
enabled,
|
|
4404
|
+
status: enabled ? "active" : "disabled"
|
|
4405
|
+
};
|
|
4406
|
+
})
|
|
4407
|
+
);
|
|
4408
|
+
return c.json(pluginList);
|
|
4409
|
+
} catch (error) {
|
|
4410
|
+
console.error("[Plugins] list error:", error);
|
|
4411
|
+
return c.json({ error: error.message }, 500);
|
|
4412
|
+
}
|
|
4413
|
+
});
|
|
4414
|
+
app.put("/api/plugins/:name/toggle", async (c) => {
|
|
4415
|
+
try {
|
|
4416
|
+
const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
|
|
4417
|
+
if (!ctxUser) return c.json({ error: "Authentication required" }, 401);
|
|
4418
|
+
const pluginName = c.req.param("name");
|
|
4419
|
+
const storageRegistry = registry.storageProviders;
|
|
4420
|
+
const currentEnabled = storageRegistry.isPluginEnabled(pluginName);
|
|
4421
|
+
const newEnabled = !currentEnabled;
|
|
4422
|
+
const affectedProviders = [];
|
|
4423
|
+
if (!newEnabled) {
|
|
4424
|
+
for (const p of storageRegistry.getAll()) {
|
|
4425
|
+
if (p.pluginName === pluginName) {
|
|
4426
|
+
affectedProviders.push(p.type);
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
const force = c.req.query("force") === "1";
|
|
4431
|
+
if (!force && !newEnabled && affectedProviders.length > 0) {
|
|
4432
|
+
let activeProvider = "local";
|
|
4433
|
+
try {
|
|
4434
|
+
const row = db?.prepare?.(`SELECT provider FROM "_globals_storage-settings" LIMIT 1`)?.get();
|
|
4435
|
+
if (row?.provider) activeProvider = row.provider;
|
|
4436
|
+
} catch {
|
|
4437
|
+
try {
|
|
4438
|
+
const result = await db?.findOne?.({
|
|
4439
|
+
collection: "_globals_storage-settings",
|
|
4440
|
+
where: {}
|
|
4441
|
+
});
|
|
4442
|
+
if (result?.provider) activeProvider = result.provider;
|
|
4443
|
+
} catch {
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
if (affectedProviders.includes(activeProvider)) {
|
|
4447
|
+
return c.json({
|
|
4448
|
+
error: `Cannot disable "${pluginName}" \u2014 storage provider "${activeProvider}" is currently active. Switch to Local storage first.`,
|
|
4449
|
+
requiresAction: true,
|
|
4450
|
+
activeProvider
|
|
4451
|
+
}, 409);
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
try {
|
|
4455
|
+
let states = {};
|
|
4456
|
+
let docId = "global";
|
|
4457
|
+
const doc = await db.findOne({
|
|
4458
|
+
collection: "_globals_plugin-settings",
|
|
4459
|
+
where: {}
|
|
4460
|
+
});
|
|
4461
|
+
if (doc) {
|
|
4462
|
+
states = doc.states || {};
|
|
4463
|
+
docId = doc.id;
|
|
4464
|
+
}
|
|
4465
|
+
states[pluginName] = newEnabled;
|
|
4466
|
+
if (doc) {
|
|
4467
|
+
await db.update({
|
|
4468
|
+
collection: "_globals_plugin-settings",
|
|
4469
|
+
id: docId,
|
|
4470
|
+
data: { states }
|
|
4471
|
+
});
|
|
4472
|
+
} else {
|
|
4473
|
+
await db.create({
|
|
4474
|
+
collection: "_globals_plugin-settings",
|
|
4475
|
+
data: { states, id: "global" }
|
|
4476
|
+
});
|
|
4477
|
+
}
|
|
4478
|
+
} catch (e) {
|
|
4479
|
+
console.warn(`[Plugins] Could not persist state for "${pluginName}":`, e);
|
|
4480
|
+
}
|
|
4481
|
+
storageRegistry.setPluginEnabled(pluginName, newEnabled);
|
|
4482
|
+
return c.json({ name: pluginName, enabled: newEnabled });
|
|
4483
|
+
} catch (error) {
|
|
4484
|
+
console.error("[Plugins] toggle error:", error);
|
|
4485
|
+
return c.json({ error: error.message }, 500);
|
|
4486
|
+
}
|
|
4487
|
+
});
|
|
4219
4488
|
app.get("/api/health", (c) => {
|
|
4220
4489
|
return c.json({
|
|
4221
4490
|
status: "ok",
|
|
@@ -4224,6 +4493,102 @@ function createHonoApp(options) {
|
|
|
4224
4493
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4225
4494
|
});
|
|
4226
4495
|
});
|
|
4496
|
+
app.get("/api/kyro/schema", async (c) => {
|
|
4497
|
+
const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
|
|
4498
|
+
if (!ctxUser) return c.json({ error: "Authentication required" }, 401);
|
|
4499
|
+
const extractFields = (fields) => fields.map((f) => {
|
|
4500
|
+
const meta = {
|
|
4501
|
+
name: f.name,
|
|
4502
|
+
type: f.type,
|
|
4503
|
+
label: f.label,
|
|
4504
|
+
required: f.required ?? false,
|
|
4505
|
+
unique: f.unique ?? false,
|
|
4506
|
+
indexed: f.indexed ?? false,
|
|
4507
|
+
defaultValue: f.defaultValue,
|
|
4508
|
+
admin: f.admin ? { ...f.admin } : void 0
|
|
4509
|
+
};
|
|
4510
|
+
if (f.minLength !== void 0) meta.minLength = f.minLength;
|
|
4511
|
+
if (f.maxLength !== void 0) meta.maxLength = f.maxLength;
|
|
4512
|
+
if (f.pattern !== void 0) meta.pattern = f.pattern;
|
|
4513
|
+
if (f.variant !== void 0) meta.variant = f.variant;
|
|
4514
|
+
if (f.hasMany !== void 0) meta.hasMany = f.hasMany;
|
|
4515
|
+
if (f.min !== void 0) meta.min = f.min;
|
|
4516
|
+
if (f.max !== void 0) meta.max = f.max;
|
|
4517
|
+
if (f.step !== void 0) meta.step = f.step;
|
|
4518
|
+
if (f.integer !== void 0) meta.integer = f.integer;
|
|
4519
|
+
if (f.options) meta.options = f.options;
|
|
4520
|
+
if (f.relationTo) meta.relationTo = f.relationTo;
|
|
4521
|
+
if (f.maxDepth !== void 0) meta.maxDepth = f.maxDepth;
|
|
4522
|
+
if (f.minRows !== void 0) meta.minRows = f.minRows;
|
|
4523
|
+
if (f.maxRows !== void 0) meta.maxRows = f.maxRows;
|
|
4524
|
+
if (f.localized !== void 0) meta.localized = f.localized;
|
|
4525
|
+
if (f.language) meta.language = f.language;
|
|
4526
|
+
if (f.format) meta.format = f.format;
|
|
4527
|
+
if (f.allowedTypes) meta.allowedTypes = f.allowedTypes;
|
|
4528
|
+
if (f.maxSize) meta.maxSize = f.maxSize;
|
|
4529
|
+
if (f.type === "group" || f.type === "row" || f.type === "collapsible" && f.fields) {
|
|
4530
|
+
meta.fields = extractFields(f.fields);
|
|
4531
|
+
}
|
|
4532
|
+
if (f.type === "array" && f.fields) {
|
|
4533
|
+
meta.fields = extractFields(f.fields);
|
|
4534
|
+
}
|
|
4535
|
+
if (f.type === "blocks" && f.blocks) {
|
|
4536
|
+
meta.blocks = f.blocks.map((b) => ({
|
|
4537
|
+
slug: b.slug,
|
|
4538
|
+
label: b.label,
|
|
4539
|
+
fields: extractFields(b.fields)
|
|
4540
|
+
}));
|
|
4541
|
+
}
|
|
4542
|
+
if (f.type === "tabs" && f.tabs) {
|
|
4543
|
+
meta.tabs = f.tabs.map((t) => ({
|
|
4544
|
+
label: t.label,
|
|
4545
|
+
name: t.name,
|
|
4546
|
+
fields: extractFields(t.fields)
|
|
4547
|
+
}));
|
|
4548
|
+
}
|
|
4549
|
+
return meta;
|
|
4550
|
+
});
|
|
4551
|
+
const data = { collections: {}, globals: {} };
|
|
4552
|
+
for (const col of registry.getCollections()) {
|
|
4553
|
+
const slug = col.slug;
|
|
4554
|
+
try {
|
|
4555
|
+
data.collections[slug] = {
|
|
4556
|
+
slug,
|
|
4557
|
+
label: col.label || slug,
|
|
4558
|
+
fields: extractFields(col.fields),
|
|
4559
|
+
jsonSchema: zodToJsonSchema(registry.getZodSchema(slug), { target: "openApi3" }),
|
|
4560
|
+
createSchema: zodToJsonSchema(registry.getCreateZodSchema(slug), { target: "openApi3" }),
|
|
4561
|
+
updateSchema: zodToJsonSchema(registry.getUpdateZodSchema(slug), { target: "openApi3" }),
|
|
4562
|
+
procedures: {
|
|
4563
|
+
find: { collection: slug, where: "Record<string,any>", sort: "string", limit: "number", page: "number", depth: "number", select: "string[]", draft: "boolean" },
|
|
4564
|
+
findByID: { collection: slug, id: "string", depth: "number", select: "string[]", draft: "boolean" },
|
|
4565
|
+
create: { collection: slug, data: "Record<string,any>", depth: "number", select: "string[]" },
|
|
4566
|
+
update: { collection: slug, id: "string", data: "Record<string,any>", depth: "number", select: "string[]", baseUpdatedAt: "string" },
|
|
4567
|
+
delete: { collection: slug, id: "string" },
|
|
4568
|
+
count: { collection: slug, where: "Record<string,any>" }
|
|
4569
|
+
}
|
|
4570
|
+
};
|
|
4571
|
+
} catch {
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
for (const global of registry.getGlobals()) {
|
|
4575
|
+
const slug = global.slug;
|
|
4576
|
+
try {
|
|
4577
|
+
data.globals[slug] = {
|
|
4578
|
+
slug,
|
|
4579
|
+
label: global.label || slug,
|
|
4580
|
+
fields: extractFields(global.fields),
|
|
4581
|
+
jsonSchema: zodToJsonSchema(registry.getZodSchema(slug), { target: "openApi3" }),
|
|
4582
|
+
procedures: {
|
|
4583
|
+
get: {},
|
|
4584
|
+
update: { data: "Record<string,any>" }
|
|
4585
|
+
}
|
|
4586
|
+
};
|
|
4587
|
+
} catch {
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
return c.json(data);
|
|
4591
|
+
});
|
|
4227
4592
|
app.get("/api/collections", async (c) => {
|
|
4228
4593
|
const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
|
|
4229
4594
|
if (!ctxUser) return c.json({ error: "Authentication required" }, 401);
|
|
@@ -4239,6 +4604,129 @@ function createHonoApp(options) {
|
|
|
4239
4604
|
}));
|
|
4240
4605
|
return c.json(collections2);
|
|
4241
4606
|
});
|
|
4607
|
+
function resolveDocField(fields, doc, fieldName) {
|
|
4608
|
+
if (fieldName in doc) return doc[fieldName];
|
|
4609
|
+
for (const field of fields) {
|
|
4610
|
+
if (!field.name) continue;
|
|
4611
|
+
if (field.type === "tabs" && field.tabs) {
|
|
4612
|
+
const data = doc[field.name];
|
|
4613
|
+
if (data && typeof data === "object" && fieldName in data) return data[fieldName];
|
|
4614
|
+
}
|
|
4615
|
+
if ((field.type === "group" || field.type === "collapsible") && field.fields) {
|
|
4616
|
+
const data = doc[field.name];
|
|
4617
|
+
if (data && typeof data === "object") {
|
|
4618
|
+
if (fieldName in data) return data[fieldName];
|
|
4619
|
+
const nested = resolveDocField(field.fields, data, fieldName);
|
|
4620
|
+
if (nested !== void 0) return nested;
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
}
|
|
4624
|
+
return void 0;
|
|
4625
|
+
}
|
|
4626
|
+
function flattenRelationshipFields(fields) {
|
|
4627
|
+
const relFields = [];
|
|
4628
|
+
for (const field of fields) {
|
|
4629
|
+
if (field.type === "relationship") {
|
|
4630
|
+
relFields.push(field);
|
|
4631
|
+
} else if (field.type === "tabs" && field.tabs) {
|
|
4632
|
+
for (const tab of field.tabs) {
|
|
4633
|
+
relFields.push(...flattenRelationshipFields(tab.fields || []));
|
|
4634
|
+
}
|
|
4635
|
+
} else if ((field.type === "group" || field.type === "collapsible") && field.fields) {
|
|
4636
|
+
relFields.push(...flattenRelationshipFields(field.fields || []));
|
|
4637
|
+
}
|
|
4638
|
+
}
|
|
4639
|
+
return relFields;
|
|
4640
|
+
}
|
|
4641
|
+
function extractRelValue(value) {
|
|
4642
|
+
if (!value) return [];
|
|
4643
|
+
if (typeof value === "string") return [value];
|
|
4644
|
+
if (Array.isArray(value)) return value.map((v) => typeof v === "object" ? v.value ?? v : v);
|
|
4645
|
+
if (typeof value === "object") {
|
|
4646
|
+
const v = value.value ?? value;
|
|
4647
|
+
return typeof v === "string" ? [v] : Array.isArray(v) ? v : [];
|
|
4648
|
+
}
|
|
4649
|
+
return [];
|
|
4650
|
+
}
|
|
4651
|
+
async function populateRelationships(docs, collection, db2, registry2) {
|
|
4652
|
+
if (docs.length === 0) return;
|
|
4653
|
+
const relFields = flattenRelationshipFields(collection.fields);
|
|
4654
|
+
if (relFields.length === 0) return;
|
|
4655
|
+
for (const relField of relFields) {
|
|
4656
|
+
const targetSlugs = Array.isArray(relField.relationTo) ? relField.relationTo : [relField.relationTo];
|
|
4657
|
+
const idsBySlug = {};
|
|
4658
|
+
for (const slug of targetSlugs) {
|
|
4659
|
+
idsBySlug[slug] = /* @__PURE__ */ new Set();
|
|
4660
|
+
}
|
|
4661
|
+
for (const doc of docs) {
|
|
4662
|
+
const raw = resolveDocField(collection.fields, doc, relField.name);
|
|
4663
|
+
const ids = extractRelValue(raw);
|
|
4664
|
+
for (const id of ids) {
|
|
4665
|
+
if (!id) continue;
|
|
4666
|
+
if (targetSlugs.length === 1) {
|
|
4667
|
+
idsBySlug[targetSlugs[0]].add(id);
|
|
4668
|
+
} else {
|
|
4669
|
+
for (const slug of targetSlugs) {
|
|
4670
|
+
idsBySlug[slug].add(id);
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
for (const [targetSlug, idSet] of Object.entries(idsBySlug)) {
|
|
4676
|
+
if (idSet.size === 0) continue;
|
|
4677
|
+
const targetCollection = registry2.getCollection(targetSlug);
|
|
4678
|
+
if (!targetCollection) continue;
|
|
4679
|
+
const titleField = targetCollection.admin?.useAsTitle || "title";
|
|
4680
|
+
const idArr = Array.from(idSet);
|
|
4681
|
+
const relatedDocs = [];
|
|
4682
|
+
for (const id of idArr) {
|
|
4683
|
+
try {
|
|
4684
|
+
const relDoc = await db2.findByID({ collection: targetSlug, id, draft: true });
|
|
4685
|
+
if (relDoc) {
|
|
4686
|
+
const title = resolveDocField(targetCollection.fields, relDoc, titleField) ?? id;
|
|
4687
|
+
relatedDocs.push({ id, title: String(title) });
|
|
4688
|
+
}
|
|
4689
|
+
} catch {
|
|
4690
|
+
relatedDocs.push({ id, title: id });
|
|
4691
|
+
}
|
|
4692
|
+
}
|
|
4693
|
+
const titleMap = new Map(relatedDocs.map((d) => [d.id, d.title]));
|
|
4694
|
+
for (const doc of docs) {
|
|
4695
|
+
const raw = resolveDocField(collection.fields, doc, relField.name);
|
|
4696
|
+
if (!raw) continue;
|
|
4697
|
+
const setValue = (val) => {
|
|
4698
|
+
if (relField.name in doc) {
|
|
4699
|
+
doc[relField.name] = val;
|
|
4700
|
+
} else {
|
|
4701
|
+
for (const f of collection.fields) {
|
|
4702
|
+
if (f.type === "tabs" && f.tabs) {
|
|
4703
|
+
for (const tab of f.tabs) {
|
|
4704
|
+
if (tab.fields?.some((tf) => tf.name === relField.name)) {
|
|
4705
|
+
const tabData = doc[f.name];
|
|
4706
|
+
if (tabData && typeof tabData === "object") {
|
|
4707
|
+
tabData[relField.name] = val;
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
};
|
|
4715
|
+
if (typeof raw === "string") {
|
|
4716
|
+
setValue({ id: raw, title: titleMap.get(raw) || raw });
|
|
4717
|
+
} else if (Array.isArray(raw)) {
|
|
4718
|
+
setValue(raw.map((v) => {
|
|
4719
|
+
const id = typeof v === "object" ? v.value ?? v.id : v;
|
|
4720
|
+
return { id, title: titleMap.get(id) || id };
|
|
4721
|
+
}));
|
|
4722
|
+
} else if (typeof raw === "object") {
|
|
4723
|
+
const id = raw.value ?? raw.id;
|
|
4724
|
+
setValue({ id, title: titleMap.get(id) || id });
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4242
4730
|
app.get("/api/search", async (c) => {
|
|
4243
4731
|
try {
|
|
4244
4732
|
const query = c.req.query("q") || "";
|
|
@@ -4290,11 +4778,12 @@ function createHonoApp(options) {
|
|
|
4290
4778
|
limit,
|
|
4291
4779
|
tenantID: ctxTenantID
|
|
4292
4780
|
});
|
|
4781
|
+
await populateRelationships(searchResult.docs, collection, db, registry);
|
|
4293
4782
|
for (const doc of searchResult.docs) {
|
|
4294
4783
|
const titleField = collection.admin?.useAsTitle || searchableFields.find(
|
|
4295
4784
|
(f) => f === "title" || f === "name" || f === "heading" || f === "slug"
|
|
4296
4785
|
);
|
|
4297
|
-
const title = titleField ? doc
|
|
4786
|
+
const title = titleField ? resolveDocField(collection.fields, doc, titleField) ?? doc.id : doc.id;
|
|
4298
4787
|
results.push({
|
|
4299
4788
|
collection: collection.slug,
|
|
4300
4789
|
label: collection.label || collection.slug,
|
|
@@ -4634,37 +5123,31 @@ function createHonoApp(options) {
|
|
|
4634
5123
|
if (!access.allowed) {
|
|
4635
5124
|
return c.json({ error: access.error }, access.status || 403);
|
|
4636
5125
|
}
|
|
4637
|
-
|
|
5126
|
+
if (ctxTenantID) {
|
|
5127
|
+
db.setTenantContext({ tenantId: ctxTenantID, userId: ctxUser?.id ?? "", role: ctxUser?.role, isSuperAdmin: ctxUser?.role === "super_admin" });
|
|
5128
|
+
}
|
|
4638
5129
|
const url = new URL(c.req.url);
|
|
4639
5130
|
const page = parseInt(url.searchParams.get("page") || "1");
|
|
4640
|
-
const limit = Math.min(
|
|
4641
|
-
parseInt(url.searchParams.get("limit") || "10"),
|
|
4642
|
-
100
|
|
4643
|
-
);
|
|
5131
|
+
const limit = Math.min(parseInt(url.searchParams.get("limit") || "10"), 100);
|
|
4644
5132
|
const sort = url.searchParams.get("sort") || void 0;
|
|
4645
5133
|
const depth = parseInt(url.searchParams.get("depth") || "0");
|
|
4646
5134
|
const select = url.searchParams.get("select")?.split(",") || void 0;
|
|
4647
|
-
|
|
4648
|
-
const whereParam = url.searchParams.get("where");
|
|
4649
|
-
if (whereParam) {
|
|
4650
|
-
try {
|
|
4651
|
-
where = JSON.parse(whereParam);
|
|
4652
|
-
} catch {
|
|
4653
|
-
}
|
|
4654
|
-
}
|
|
5135
|
+
const isDraftRequest = !!ctxUser;
|
|
4655
5136
|
const result = await db.find({
|
|
4656
5137
|
collection: slug,
|
|
4657
|
-
where,
|
|
5138
|
+
where: {},
|
|
4658
5139
|
sort,
|
|
4659
5140
|
limit,
|
|
4660
5141
|
page,
|
|
4661
5142
|
depth,
|
|
4662
5143
|
tenantID: ctxTenantID,
|
|
4663
|
-
select
|
|
5144
|
+
select,
|
|
5145
|
+
draft: isDraftRequest
|
|
4664
5146
|
});
|
|
5147
|
+
await populateRelationships(result.docs, collection, db, registry);
|
|
4665
5148
|
return c.json(result);
|
|
4666
5149
|
} catch (error) {
|
|
4667
|
-
console.error(
|
|
5150
|
+
console.error("[API] list error:", error);
|
|
4668
5151
|
return c.json({ error: error.message }, 500);
|
|
4669
5152
|
}
|
|
4670
5153
|
});
|
|
@@ -4689,6 +5172,9 @@ function createHonoApp(options) {
|
|
|
4689
5172
|
return c.json({ error: access.error }, access.status || 403);
|
|
4690
5173
|
}
|
|
4691
5174
|
const id = c.req.param("id");
|
|
5175
|
+
if (!id) {
|
|
5176
|
+
return c.json({ error: "Missing document ID" }, 400);
|
|
5177
|
+
}
|
|
4692
5178
|
const url = new URL(c.req.url);
|
|
4693
5179
|
const compareA = url.searchParams.get("compareA");
|
|
4694
5180
|
const compareB = url.searchParams.get("compareB");
|
|
@@ -4772,23 +5258,39 @@ function createHonoApp(options) {
|
|
|
4772
5258
|
const id = c.req.param("id");
|
|
4773
5259
|
const body = await c.req.json();
|
|
4774
5260
|
const baseUpdatedAt = readBaseUpdatedAt(body);
|
|
4775
|
-
const data = body.data ?? omitRevisionFields(body);
|
|
4776
5261
|
const originalDoc = await db.findByID({
|
|
4777
5262
|
collection: slug,
|
|
4778
5263
|
id,
|
|
4779
|
-
tenantID: ctxTenantID
|
|
5264
|
+
tenantID: ctxTenantID,
|
|
5265
|
+
draft: true
|
|
4780
5266
|
});
|
|
4781
5267
|
if (!originalDoc) {
|
|
4782
5268
|
return c.json({ error: "Document not found" }, 404);
|
|
4783
5269
|
}
|
|
5270
|
+
let finalData;
|
|
5271
|
+
if (body.delta) {
|
|
5272
|
+
finalData = { ...originalDoc, ...body.delta };
|
|
5273
|
+
} else {
|
|
5274
|
+
finalData = body.data ?? omitRevisionFields(body);
|
|
5275
|
+
}
|
|
4784
5276
|
const draft = await db.upsertDraft({
|
|
4785
5277
|
collection: slug,
|
|
4786
5278
|
documentId: id,
|
|
4787
5279
|
tenantID: ctxTenantID,
|
|
4788
|
-
data,
|
|
5280
|
+
data: finalData,
|
|
4789
5281
|
baseUpdatedAt,
|
|
4790
5282
|
draftUpdatedAt: body.draftUpdatedAt
|
|
4791
5283
|
});
|
|
5284
|
+
if (ctxUser) {
|
|
5285
|
+
sessionAuthAdapter?.createAuditLog({
|
|
5286
|
+
action: "document_update",
|
|
5287
|
+
userId: ctxUser.id,
|
|
5288
|
+
resource: slug,
|
|
5289
|
+
resourceId: id,
|
|
5290
|
+
success: true,
|
|
5291
|
+
metadata: { type: "draft_save" }
|
|
5292
|
+
});
|
|
5293
|
+
}
|
|
4792
5294
|
return c.json({ data: draft, message: "Draft saved successfully" });
|
|
4793
5295
|
} catch (error) {
|
|
4794
5296
|
return c.json({ error: error.message }, 500);
|
|
@@ -4819,6 +5321,16 @@ function createHonoApp(options) {
|
|
|
4819
5321
|
documentId: c.req.param("id"),
|
|
4820
5322
|
tenantID: ctxTenantID
|
|
4821
5323
|
});
|
|
5324
|
+
if (ctxUser) {
|
|
5325
|
+
sessionAuthAdapter?.createAuditLog({
|
|
5326
|
+
action: "document_update",
|
|
5327
|
+
userId: ctxUser.id,
|
|
5328
|
+
resource: slug,
|
|
5329
|
+
resourceId: c.req.param("id"),
|
|
5330
|
+
success: true,
|
|
5331
|
+
metadata: { type: "draft_discard" }
|
|
5332
|
+
});
|
|
5333
|
+
}
|
|
4822
5334
|
return c.json({ message: "Draft discarded successfully" });
|
|
4823
5335
|
} catch (error) {
|
|
4824
5336
|
return c.json({ error: error.message }, 500);
|
|
@@ -4861,12 +5373,14 @@ function createHonoApp(options) {
|
|
|
4861
5373
|
doc = await db.findOne({
|
|
4862
5374
|
collection: slug,
|
|
4863
5375
|
where: { slug: id },
|
|
4864
|
-
tenantID: ctxTenantID
|
|
5376
|
+
tenantID: ctxTenantID,
|
|
5377
|
+
draft: isDraftRequest
|
|
4865
5378
|
});
|
|
4866
5379
|
}
|
|
4867
5380
|
if (!doc) {
|
|
4868
5381
|
return c.json({ error: "Document not found" }, 404);
|
|
4869
5382
|
}
|
|
5383
|
+
await populateRelationships([doc], collection, db, registry);
|
|
4870
5384
|
return c.json({ data: doc });
|
|
4871
5385
|
} catch (error) {
|
|
4872
5386
|
return c.json({ error: error.message }, 500);
|
|
@@ -4892,23 +5406,80 @@ function createHonoApp(options) {
|
|
|
4892
5406
|
if (!access.allowed) {
|
|
4893
5407
|
return c.json({ error: access.error }, access.status || 403);
|
|
4894
5408
|
}
|
|
5409
|
+
if (ctxTenantID) {
|
|
5410
|
+
db.setTenantContext({ tenantId: ctxTenantID, userId: ctxUser?.id ?? "", role: ctxUser?.role, isSuperAdmin: ctxUser?.role === "super_admin" });
|
|
5411
|
+
}
|
|
4895
5412
|
auditApiKeyUsage(sessionAuthAdapter, apiKeyContext, basePath, "POST", c.req.raw);
|
|
4896
5413
|
const body = await c.req.json();
|
|
5414
|
+
let validated = body;
|
|
5415
|
+
for (const field of collection.fields) {
|
|
5416
|
+
if (field.name && validated[field.name] === "") {
|
|
5417
|
+
const isTextual = field.type === "text" || field.type === "textarea" || field.type === "code" || field.type === "markdown" || field.type === "email" || field.type === "password" || field.type === "color";
|
|
5418
|
+
if (!isTextual) {
|
|
5419
|
+
validated[field.name] = null;
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
5422
|
+
}
|
|
5423
|
+
convertRichtextFields(collection.fields, validated);
|
|
5424
|
+
const hookReq = c.req.raw;
|
|
5425
|
+
if (collection.hooks?.beforeValidate) {
|
|
5426
|
+
for (const hook of collection.hooks.beforeValidate) {
|
|
5427
|
+
const hookResult = await hook({
|
|
5428
|
+
collection: slug,
|
|
5429
|
+
data: validated,
|
|
5430
|
+
req: hookReq,
|
|
5431
|
+
user: ctxUser,
|
|
5432
|
+
tenantID: ctxTenantID,
|
|
5433
|
+
operation: "create"
|
|
5434
|
+
});
|
|
5435
|
+
if (hookResult) Object.assign(validated, hookResult);
|
|
5436
|
+
}
|
|
5437
|
+
}
|
|
4897
5438
|
const schema = registry.getCreateZodSchema(slug);
|
|
4898
|
-
let validated;
|
|
4899
5439
|
try {
|
|
4900
|
-
validated = schema.parse(
|
|
5440
|
+
validated = schema.parse(validated);
|
|
4901
5441
|
} catch (zodErr) {
|
|
4902
|
-
return c.json({ error:
|
|
5442
|
+
return c.json({ error: `Validation failed: ${formatZodErrors(zodErr.errors)}`, details: zodErr.errors }, 400);
|
|
4903
5443
|
}
|
|
4904
5444
|
if (collection.tenantScoped && ctxTenantID) {
|
|
4905
5445
|
validated.tenantID = ctxTenantID;
|
|
4906
5446
|
}
|
|
5447
|
+
const isDraftEnabled = collection.versions?.drafts === true;
|
|
5448
|
+
if (isDraftEnabled) {
|
|
5449
|
+
validated.publishStatus = "draft";
|
|
5450
|
+
validated.hasDraft = false;
|
|
5451
|
+
}
|
|
5452
|
+
if (collection.hooks?.beforeChange) {
|
|
5453
|
+
for (const hook of collection.hooks.beforeChange) {
|
|
5454
|
+
const hookResult = await hook({
|
|
5455
|
+
collection: slug,
|
|
5456
|
+
data: validated,
|
|
5457
|
+
req: hookReq,
|
|
5458
|
+
user: ctxUser,
|
|
5459
|
+
tenantID: ctxTenantID,
|
|
5460
|
+
operation: "create"
|
|
5461
|
+
});
|
|
5462
|
+
if (hookResult) Object.assign(validated, hookResult);
|
|
5463
|
+
}
|
|
5464
|
+
}
|
|
4907
5465
|
const doc = await db.create({
|
|
4908
5466
|
collection: slug,
|
|
4909
5467
|
data: validated,
|
|
4910
5468
|
tenantID: ctxTenantID
|
|
4911
5469
|
});
|
|
5470
|
+
if (collection.hooks?.afterChange) {
|
|
5471
|
+
for (const hook of collection.hooks.afterChange) {
|
|
5472
|
+
await hook({
|
|
5473
|
+
collection: slug,
|
|
5474
|
+
doc,
|
|
5475
|
+
data: validated,
|
|
5476
|
+
req: hookReq,
|
|
5477
|
+
user: ctxUser,
|
|
5478
|
+
tenantID: ctxTenantID,
|
|
5479
|
+
operation: "create"
|
|
5480
|
+
});
|
|
5481
|
+
}
|
|
5482
|
+
}
|
|
4912
5483
|
if (webhookService) {
|
|
4913
5484
|
webhookService.trigger(getWebhookEvent(slug, "create"), {
|
|
4914
5485
|
collection: slug,
|
|
@@ -4918,11 +5489,20 @@ function createHonoApp(options) {
|
|
|
4918
5489
|
tenantId: ctxTenantID
|
|
4919
5490
|
}).catch((err) => console.error(`[Webhook] Failed to trigger:`, err));
|
|
4920
5491
|
}
|
|
5492
|
+
if (ctxUser) {
|
|
5493
|
+
sessionAuthAdapter?.createAuditLog({
|
|
5494
|
+
action: "document_create",
|
|
5495
|
+
userId: ctxUser.id,
|
|
5496
|
+
resource: slug,
|
|
5497
|
+
resourceId: doc.id,
|
|
5498
|
+
success: true
|
|
5499
|
+
});
|
|
5500
|
+
}
|
|
4921
5501
|
return c.json({ data: doc, message: "Created successfully" }, 201);
|
|
4922
5502
|
} catch (error) {
|
|
4923
5503
|
if (error.name === "ZodError") {
|
|
4924
5504
|
return c.json(
|
|
4925
|
-
{ error:
|
|
5505
|
+
{ error: `Validation failed: ${formatZodErrors(error.errors)}`, details: error.errors },
|
|
4926
5506
|
400
|
|
4927
5507
|
);
|
|
4928
5508
|
}
|
|
@@ -4957,32 +5537,66 @@ function createHonoApp(options) {
|
|
|
4957
5537
|
bodyKeys: Object.keys(body),
|
|
4958
5538
|
tenantID: ctxTenantID
|
|
4959
5539
|
});
|
|
4960
|
-
const cleaned = Object.fromEntries(
|
|
4961
|
-
Object.entries(omitRevisionFields(body)).filter(
|
|
4962
|
-
([_, v]) => v !== "null" && v !== void 0
|
|
4963
|
-
)
|
|
4964
|
-
);
|
|
4965
|
-
const schema = registry.getUpdateZodSchema(slug);
|
|
4966
|
-
const validated = schema.parse(cleaned);
|
|
4967
|
-
console.log(`[PATCH] Validated data:`, Object.keys(validated));
|
|
4968
5540
|
const originalDoc = await db.findByID({
|
|
4969
5541
|
collection: slug,
|
|
4970
5542
|
id,
|
|
4971
5543
|
tenantID: ctxTenantID,
|
|
4972
5544
|
draft: true
|
|
4973
|
-
// Always fetch current doc regardless of status
|
|
4974
5545
|
});
|
|
4975
|
-
if (originalDoc) {
|
|
4976
|
-
console.log(`[PATCH] Original doc updatedAt:`, originalDoc.updatedAt);
|
|
4977
|
-
}
|
|
4978
5546
|
if (!originalDoc) {
|
|
4979
5547
|
return c.json({ error: "Document not found" }, 404);
|
|
4980
5548
|
}
|
|
4981
5549
|
if (baseUpdatedAt && originalDoc.updatedAt && baseUpdatedAt !== originalDoc.updatedAt) {
|
|
4982
5550
|
return c.json(buildConflictResponse(baseUpdatedAt, originalDoc), 409);
|
|
4983
5551
|
}
|
|
5552
|
+
let validated = Object.fromEntries(
|
|
5553
|
+
Object.entries(omitRevisionFields(body)).filter(
|
|
5554
|
+
([_, v]) => v !== "null" && v !== void 0
|
|
5555
|
+
)
|
|
5556
|
+
);
|
|
5557
|
+
for (const field of collection.fields) {
|
|
5558
|
+
if (field.name && validated[field.name] === "") {
|
|
5559
|
+
const isTextual = field.type === "text" || field.type === "textarea" || field.type === "code" || field.type === "markdown" || field.type === "email" || field.type === "password" || field.type === "color";
|
|
5560
|
+
if (!isTextual) {
|
|
5561
|
+
validated[field.name] = null;
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5564
|
+
}
|
|
5565
|
+
convertRichtextFields(collection.fields, validated);
|
|
5566
|
+
const hookReq = c.req.raw;
|
|
5567
|
+
if (collection.hooks?.beforeValidate) {
|
|
5568
|
+
for (const hook of collection.hooks.beforeValidate) {
|
|
5569
|
+
const hookResult = await hook({
|
|
5570
|
+
collection: slug,
|
|
5571
|
+
data: validated,
|
|
5572
|
+
originalDoc,
|
|
5573
|
+
req: hookReq,
|
|
5574
|
+
user: ctxUser,
|
|
5575
|
+
tenantID: ctxTenantID,
|
|
5576
|
+
operation: "update"
|
|
5577
|
+
});
|
|
5578
|
+
if (hookResult) Object.assign(validated, hookResult);
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
const schema = registry.getUpdateZodSchema(slug);
|
|
5582
|
+
validated = schema.parse(validated);
|
|
5583
|
+
if (collection.hooks?.beforeChange) {
|
|
5584
|
+
for (const hook of collection.hooks.beforeChange) {
|
|
5585
|
+
const hookResult = await hook({
|
|
5586
|
+
collection: slug,
|
|
5587
|
+
data: validated,
|
|
5588
|
+
originalDoc,
|
|
5589
|
+
req: hookReq,
|
|
5590
|
+
user: ctxUser,
|
|
5591
|
+
tenantID: ctxTenantID,
|
|
5592
|
+
operation: "update"
|
|
5593
|
+
});
|
|
5594
|
+
if (hookResult) Object.assign(validated, hookResult);
|
|
5595
|
+
}
|
|
5596
|
+
}
|
|
5597
|
+
console.log(`[PATCH] Validated data:`, Object.keys(validated));
|
|
4984
5598
|
const isDraftEnabled = collection.versions?.drafts === true;
|
|
4985
|
-
const isAlreadyPublished = originalDoc.
|
|
5599
|
+
const isAlreadyPublished = originalDoc.publishStatus === "published";
|
|
4986
5600
|
let doc;
|
|
4987
5601
|
if (isDraftEnabled && isAlreadyPublished) {
|
|
4988
5602
|
await db.createVersion({
|
|
@@ -4997,18 +5611,20 @@ function createHonoApp(options) {
|
|
|
4997
5611
|
await db.update({
|
|
4998
5612
|
collection: slug,
|
|
4999
5613
|
id,
|
|
5000
|
-
data: {
|
|
5614
|
+
data: { hasDraft: true },
|
|
5001
5615
|
// Keep old data, just set flag
|
|
5002
5616
|
tenantID: ctxTenantID
|
|
5003
5617
|
});
|
|
5004
5618
|
} else {
|
|
5005
|
-
const saveData = isDraftEnabled ? { ...validated,
|
|
5619
|
+
const saveData = isDraftEnabled ? { ...validated, publishStatus: "draft", hasDraft: false } : validated;
|
|
5620
|
+
console.log(`[PATCH] About to call db.update for ${slug}/${id} with keys:`, Object.keys(saveData));
|
|
5006
5621
|
await db.update({
|
|
5007
5622
|
collection: slug,
|
|
5008
5623
|
id,
|
|
5009
5624
|
data: saveData,
|
|
5010
5625
|
tenantID: ctxTenantID
|
|
5011
5626
|
});
|
|
5627
|
+
console.log(`[PATCH] db.update SUCCEEDED for ${slug}/${id}`);
|
|
5012
5628
|
}
|
|
5013
5629
|
doc = await db.findByID({
|
|
5014
5630
|
collection: slug,
|
|
@@ -5035,6 +5651,20 @@ function createHonoApp(options) {
|
|
|
5035
5651
|
tenantID: ctxTenantID
|
|
5036
5652
|
});
|
|
5037
5653
|
}
|
|
5654
|
+
if (collection.hooks?.afterChange) {
|
|
5655
|
+
for (const hook of collection.hooks.afterChange) {
|
|
5656
|
+
await hook({
|
|
5657
|
+
collection: slug,
|
|
5658
|
+
doc,
|
|
5659
|
+
data: validated,
|
|
5660
|
+
originalDoc,
|
|
5661
|
+
req: hookReq,
|
|
5662
|
+
user: ctxUser,
|
|
5663
|
+
tenantID: ctxTenantID,
|
|
5664
|
+
operation: "update"
|
|
5665
|
+
});
|
|
5666
|
+
}
|
|
5667
|
+
}
|
|
5038
5668
|
if (webhookService) {
|
|
5039
5669
|
webhookService.trigger(getWebhookEvent(slug, "update"), {
|
|
5040
5670
|
collection: slug,
|
|
@@ -5045,16 +5675,26 @@ function createHonoApp(options) {
|
|
|
5045
5675
|
tenantId: ctxTenantID
|
|
5046
5676
|
}).catch((err) => console.error(`[Webhook] Failed to trigger:`, err));
|
|
5047
5677
|
}
|
|
5048
|
-
|
|
5678
|
+
auditApiKeyUsage(sessionAuthAdapter, apiKeyContext, `${basePath}/${id}`, "PATCH", c.req.raw);
|
|
5679
|
+
if (ctxUser) {
|
|
5680
|
+
sessionAuthAdapter?.createAuditLog({
|
|
5681
|
+
action: "document_update",
|
|
5682
|
+
userId: ctxUser.id,
|
|
5683
|
+
resource: slug,
|
|
5684
|
+
resourceId: id,
|
|
5685
|
+
success: true
|
|
5686
|
+
});
|
|
5687
|
+
}
|
|
5049
5688
|
return c.json({ data: doc, message: isDraftEnabled ? "Draft saved" : "Updated successfully" });
|
|
5050
5689
|
} catch (error) {
|
|
5051
5690
|
if (error.name === "ZodError") {
|
|
5052
5691
|
console.error(`[PATCH ${basePath}/:id] Validation failed:`, error.errors);
|
|
5053
5692
|
return c.json(
|
|
5054
|
-
{ error:
|
|
5693
|
+
{ error: `Validation failed: ${formatZodErrors(error.errors)}`, details: error.errors },
|
|
5055
5694
|
400
|
|
5056
5695
|
);
|
|
5057
5696
|
}
|
|
5697
|
+
console.error(`[PATCH ${basePath}/:id] ERROR:`, error.message, `CAUSE:`, error.cause?.message || error.cause, `QUERY:`, error.query);
|
|
5058
5698
|
return c.json({ error: error.message }, 500);
|
|
5059
5699
|
}
|
|
5060
5700
|
});
|
|
@@ -5078,13 +5718,33 @@ function createHonoApp(options) {
|
|
|
5078
5718
|
if (!access.allowed) {
|
|
5079
5719
|
return c.json({ error: access.error }, access.status || 403);
|
|
5080
5720
|
}
|
|
5721
|
+
if (ctxTenantID) {
|
|
5722
|
+
db.setTenantContext({ tenantId: ctxTenantID, userId: ctxUser?.id ?? "", role: ctxUser?.role, isSuperAdmin: ctxUser?.role === "super_admin" });
|
|
5723
|
+
}
|
|
5081
5724
|
const id = c.req.param("id");
|
|
5082
5725
|
console.log(`[DELETE] Deleting ${slug}/${id}`);
|
|
5726
|
+
const hookReq = c.req.raw;
|
|
5083
5727
|
const originalDoc = await db.findByID({
|
|
5084
5728
|
collection: slug,
|
|
5085
5729
|
id,
|
|
5086
|
-
tenantID: ctxTenantID
|
|
5730
|
+
tenantID: ctxTenantID,
|
|
5731
|
+
draft: true
|
|
5087
5732
|
});
|
|
5733
|
+
if (!originalDoc) {
|
|
5734
|
+
return c.json({ error: "Document not found" }, 404);
|
|
5735
|
+
}
|
|
5736
|
+
if (collection.hooks?.beforeDelete) {
|
|
5737
|
+
for (const hook of collection.hooks.beforeDelete) {
|
|
5738
|
+
await hook({
|
|
5739
|
+
collection: slug,
|
|
5740
|
+
doc: originalDoc,
|
|
5741
|
+
req: hookReq,
|
|
5742
|
+
user: ctxUser,
|
|
5743
|
+
tenantID: ctxTenantID,
|
|
5744
|
+
operation: "delete"
|
|
5745
|
+
});
|
|
5746
|
+
}
|
|
5747
|
+
}
|
|
5088
5748
|
const doc = await db.delete({
|
|
5089
5749
|
collection: slug,
|
|
5090
5750
|
id,
|
|
@@ -5095,6 +5755,19 @@ function createHonoApp(options) {
|
|
|
5095
5755
|
documentId: id,
|
|
5096
5756
|
tenantID: ctxTenantID
|
|
5097
5757
|
});
|
|
5758
|
+
if (collection.hooks?.afterDelete) {
|
|
5759
|
+
for (const hook of collection.hooks.afterDelete) {
|
|
5760
|
+
await hook({
|
|
5761
|
+
collection: slug,
|
|
5762
|
+
doc,
|
|
5763
|
+
originalDoc,
|
|
5764
|
+
req: hookReq,
|
|
5765
|
+
user: ctxUser,
|
|
5766
|
+
tenantID: ctxTenantID,
|
|
5767
|
+
operation: "delete"
|
|
5768
|
+
});
|
|
5769
|
+
}
|
|
5770
|
+
}
|
|
5098
5771
|
if (webhookService) {
|
|
5099
5772
|
webhookService.trigger(getWebhookEvent(slug, "delete"), {
|
|
5100
5773
|
collection: slug,
|
|
@@ -5105,6 +5778,16 @@ function createHonoApp(options) {
|
|
|
5105
5778
|
tenantId: ctxTenantID
|
|
5106
5779
|
}).catch((err) => console.error(`[Webhook] Failed to trigger:`, err));
|
|
5107
5780
|
}
|
|
5781
|
+
auditApiKeyUsage(sessionAuthAdapter, apiKeyContext, `${basePath}/${id}`, "DELETE", c.req.raw);
|
|
5782
|
+
if (ctxUser) {
|
|
5783
|
+
sessionAuthAdapter?.createAuditLog({
|
|
5784
|
+
action: "document_delete",
|
|
5785
|
+
userId: ctxUser.id,
|
|
5786
|
+
resource: slug,
|
|
5787
|
+
resourceId: id,
|
|
5788
|
+
success: true
|
|
5789
|
+
});
|
|
5790
|
+
}
|
|
5108
5791
|
return c.json({ data: doc, message: "Deleted successfully" });
|
|
5109
5792
|
} catch (error) {
|
|
5110
5793
|
console.error(`[DELETE] Error deleting ${slug}:`, error);
|
|
@@ -5138,7 +5821,8 @@ function createHonoApp(options) {
|
|
|
5138
5821
|
const originalDoc = await db.findByID({
|
|
5139
5822
|
collection: slug,
|
|
5140
5823
|
id,
|
|
5141
|
-
tenantID: ctxTenantID
|
|
5824
|
+
tenantID: ctxTenantID,
|
|
5825
|
+
draft: true
|
|
5142
5826
|
});
|
|
5143
5827
|
if (!originalDoc) {
|
|
5144
5828
|
console.log("[Duplicate] Document not found");
|
|
@@ -5198,7 +5882,7 @@ function createHonoApp(options) {
|
|
|
5198
5882
|
const doc = await db.update({
|
|
5199
5883
|
collection: slug,
|
|
5200
5884
|
id,
|
|
5201
|
-
data: { ...version.data,
|
|
5885
|
+
data: { ...version.data, publishStatus: "draft", hasDraft: false },
|
|
5202
5886
|
tenantID: ctxTenantID
|
|
5203
5887
|
});
|
|
5204
5888
|
return c.json({
|
|
@@ -5243,7 +5927,7 @@ function createHonoApp(options) {
|
|
|
5243
5927
|
const doc = await db.update({
|
|
5244
5928
|
collection: slug,
|
|
5245
5929
|
id: c.req.param("id"),
|
|
5246
|
-
data: { ...version.data,
|
|
5930
|
+
data: { ...version.data, publishStatus: "draft", hasDraft: false },
|
|
5247
5931
|
tenantID: ctxTenantID
|
|
5248
5932
|
});
|
|
5249
5933
|
return c.json({
|
|
@@ -5276,6 +5960,9 @@ function createHonoApp(options) {
|
|
|
5276
5960
|
if (!access.allowed) {
|
|
5277
5961
|
return c.json({ error: access.error }, access.status || 403);
|
|
5278
5962
|
}
|
|
5963
|
+
if (ctxTenantID) {
|
|
5964
|
+
db.setTenantContext({ tenantId: ctxTenantID, userId: ctxUser?.id ?? "", role: ctxUser?.role, isSuperAdmin: ctxUser?.role === "super_admin" });
|
|
5965
|
+
}
|
|
5279
5966
|
const id = c.req.param("id");
|
|
5280
5967
|
const body = await c.req.json().catch(() => ({}));
|
|
5281
5968
|
const baseUpdatedAt = readBaseUpdatedAt(body);
|
|
@@ -5291,9 +5978,9 @@ function createHonoApp(options) {
|
|
|
5291
5978
|
if (baseUpdatedAt && originalDoc.updatedAt && baseUpdatedAt !== originalDoc.updatedAt) {
|
|
5292
5979
|
return c.json(buildConflictResponse(baseUpdatedAt, originalDoc), 409);
|
|
5293
5980
|
}
|
|
5294
|
-
let publishData = {
|
|
5981
|
+
let publishData = { publishStatus: "published", hasDraft: false };
|
|
5295
5982
|
let finalContent = originalDoc;
|
|
5296
|
-
if (originalDoc.
|
|
5983
|
+
if (originalDoc.hasDraft) {
|
|
5297
5984
|
const versions = await db.findVersions({
|
|
5298
5985
|
collection: slug,
|
|
5299
5986
|
documentId: id,
|
|
@@ -5316,7 +6003,7 @@ function createHonoApp(options) {
|
|
|
5316
6003
|
await db.createVersion({
|
|
5317
6004
|
collection: slug,
|
|
5318
6005
|
documentId: id,
|
|
5319
|
-
data: { ...finalContent,
|
|
6006
|
+
data: { ...finalContent, publishStatus: "published" },
|
|
5320
6007
|
status: "published",
|
|
5321
6008
|
createdBy: ctxUser?.id,
|
|
5322
6009
|
changeDescription: "Published",
|
|
@@ -5375,7 +6062,7 @@ function createHonoApp(options) {
|
|
|
5375
6062
|
const doc = await db.update({
|
|
5376
6063
|
collection: slug,
|
|
5377
6064
|
id,
|
|
5378
|
-
data: {
|
|
6065
|
+
data: { publishStatus: "draft" },
|
|
5379
6066
|
tenantID: ctxTenantID
|
|
5380
6067
|
});
|
|
5381
6068
|
if (webhookService) {
|
|
@@ -5411,12 +6098,30 @@ function createHonoApp(options) {
|
|
|
5411
6098
|
return c.json({ error: access.error }, access.status || 403);
|
|
5412
6099
|
}
|
|
5413
6100
|
const isDraftRequest = c.req.query("draft") === "true" && !!ctxUser;
|
|
5414
|
-
|
|
6101
|
+
let doc = await db.findOne({
|
|
5415
6102
|
collection: `_globals_${slug}`,
|
|
5416
6103
|
where: {},
|
|
5417
6104
|
tenantID: ctxTenantID,
|
|
5418
6105
|
draft: isDraftRequest
|
|
5419
6106
|
});
|
|
6107
|
+
if (slug === "system") {
|
|
6108
|
+
const newSecret = crypto.randomBytes(32).toString("hex");
|
|
6109
|
+
if (!doc) {
|
|
6110
|
+
doc = await db.create({
|
|
6111
|
+
collection: `_globals_${slug}`,
|
|
6112
|
+
data: { id: slug, appSecret: newSecret },
|
|
6113
|
+
tenantID: ctxTenantID
|
|
6114
|
+
});
|
|
6115
|
+
} else if (!doc.appSecret) {
|
|
6116
|
+
await db.update({
|
|
6117
|
+
collection: `_globals_${slug}`,
|
|
6118
|
+
id: slug,
|
|
6119
|
+
data: { appSecret: newSecret },
|
|
6120
|
+
tenantID: ctxTenantID
|
|
6121
|
+
});
|
|
6122
|
+
doc.appSecret = newSecret;
|
|
6123
|
+
}
|
|
6124
|
+
}
|
|
5420
6125
|
return c.json({ data: doc || {} });
|
|
5421
6126
|
} catch (error) {
|
|
5422
6127
|
return c.json({ error: error.message }, 500);
|
|
@@ -5436,18 +6141,27 @@ function createHonoApp(options) {
|
|
|
5436
6141
|
if (!access.allowed) {
|
|
5437
6142
|
return c.json({ error: access.error }, access.status || 403);
|
|
5438
6143
|
}
|
|
5439
|
-
const body = await c.req.json();
|
|
6144
|
+
const body = omitRevisionFields(await c.req.json());
|
|
5440
6145
|
const cleaned = Object.fromEntries(
|
|
5441
6146
|
Object.entries(body).filter(([_, v]) => v !== null && v !== "null" && v !== void 0)
|
|
5442
6147
|
);
|
|
6148
|
+
for (const field of globalConfig.fields) {
|
|
6149
|
+
if (field.name && cleaned[field.name] === "") {
|
|
6150
|
+
const isTextual = field.type === "text" || field.type === "textarea" || field.type === "code" || field.type === "markdown" || field.type === "email" || field.type === "password" || field.type === "color";
|
|
6151
|
+
if (!isTextual) {
|
|
6152
|
+
cleaned[field.name] = null;
|
|
6153
|
+
}
|
|
6154
|
+
}
|
|
6155
|
+
}
|
|
6156
|
+
convertRichtextFields(globalConfig.fields, cleaned);
|
|
5443
6157
|
const schema = registry.getZodSchema(slug);
|
|
5444
6158
|
let validated;
|
|
5445
6159
|
try {
|
|
5446
6160
|
validated = schema.parse(cleaned);
|
|
5447
6161
|
} catch (zodErr) {
|
|
5448
|
-
return c.json({ error:
|
|
6162
|
+
return c.json({ error: `Validation failed: ${formatZodErrors(zodErr.errors)}`, details: zodErr.errors }, 400);
|
|
5449
6163
|
}
|
|
5450
|
-
const SYSTEM_FIELDS = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt", "
|
|
6164
|
+
const SYSTEM_FIELDS = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt", "publishStatus", "hasDraft", "baseUpdatedAt", "_baseUpdatedAt"]);
|
|
5451
6165
|
const userData = Object.fromEntries(
|
|
5452
6166
|
Object.entries(validated).filter(([k]) => !SYSTEM_FIELDS.has(k))
|
|
5453
6167
|
);
|
|
@@ -5459,7 +6173,7 @@ function createHonoApp(options) {
|
|
|
5459
6173
|
draft: true
|
|
5460
6174
|
});
|
|
5461
6175
|
const isDraftEnabled = globalConfig.versions?.drafts === true;
|
|
5462
|
-
const isAlreadyPublished = originalDoc?.
|
|
6176
|
+
const isAlreadyPublished = originalDoc?.publishStatus === "published";
|
|
5463
6177
|
let doc;
|
|
5464
6178
|
if (isDraftEnabled && isAlreadyPublished) {
|
|
5465
6179
|
await db.createVersion({
|
|
@@ -5474,11 +6188,11 @@ function createHonoApp(options) {
|
|
|
5474
6188
|
doc = await db.update({
|
|
5475
6189
|
collection: collectionSlug,
|
|
5476
6190
|
id: slug,
|
|
5477
|
-
data: {
|
|
6191
|
+
data: { hasDraft: true },
|
|
5478
6192
|
tenantID: ctxTenantID
|
|
5479
6193
|
});
|
|
5480
6194
|
} else {
|
|
5481
|
-
const saveData = isDraftEnabled ? { ...userData,
|
|
6195
|
+
const saveData = isDraftEnabled ? { ...userData, publishStatus: "draft", hasDraft: false } : { ...userData, publishStatus: "published", hasDraft: false };
|
|
5482
6196
|
if (originalDoc) {
|
|
5483
6197
|
doc = await db.update({
|
|
5484
6198
|
collection: collectionSlug,
|
|
@@ -5509,6 +6223,15 @@ function createHonoApp(options) {
|
|
|
5509
6223
|
mediaService = null;
|
|
5510
6224
|
mediaServiceInitError = null;
|
|
5511
6225
|
}
|
|
6226
|
+
if (ctxUser) {
|
|
6227
|
+
sessionAuthAdapter?.createAuditLog({
|
|
6228
|
+
action: "settings_change",
|
|
6229
|
+
userId: ctxUser.id,
|
|
6230
|
+
resource: `global:${slug}`,
|
|
6231
|
+
resourceId: slug,
|
|
6232
|
+
success: true
|
|
6233
|
+
});
|
|
6234
|
+
}
|
|
5512
6235
|
return c.json({ data: doc, message: "Updated successfully" });
|
|
5513
6236
|
} catch (error) {
|
|
5514
6237
|
console.error(`[API] Save global "${slug}" failed:`, error);
|
|
@@ -5533,9 +6256,9 @@ function createHonoApp(options) {
|
|
|
5533
6256
|
draft: true
|
|
5534
6257
|
});
|
|
5535
6258
|
if (!originalDoc) return c.json({ error: "Global not found" }, 404);
|
|
5536
|
-
let publishData = {
|
|
6259
|
+
let publishData = { publishStatus: "published", hasDraft: false };
|
|
5537
6260
|
let finalContent = originalDoc;
|
|
5538
|
-
if (originalDoc.
|
|
6261
|
+
if (originalDoc.hasDraft) {
|
|
5539
6262
|
const versions = await db.findVersions({
|
|
5540
6263
|
collection: collectionSlug,
|
|
5541
6264
|
documentId: slug,
|
|
@@ -5558,7 +6281,7 @@ function createHonoApp(options) {
|
|
|
5558
6281
|
await db.createVersion({
|
|
5559
6282
|
collection: collectionSlug,
|
|
5560
6283
|
documentId: slug,
|
|
5561
|
-
data: { ...finalContent,
|
|
6284
|
+
data: { ...finalContent, publishStatus: "published" },
|
|
5562
6285
|
status: "published",
|
|
5563
6286
|
createdBy: ctxUser?.id,
|
|
5564
6287
|
changeDescription: "Published",
|
|
@@ -5578,7 +6301,7 @@ function createHonoApp(options) {
|
|
|
5578
6301
|
const doc = await db.update({
|
|
5579
6302
|
collection: `_globals_${slug}`,
|
|
5580
6303
|
id: slug,
|
|
5581
|
-
data: {
|
|
6304
|
+
data: { publishStatus: "draft", hasDraft: false },
|
|
5582
6305
|
tenantID: ctxTenantID
|
|
5583
6306
|
});
|
|
5584
6307
|
return c.json({ data: doc, message: "Unpublished successfully" });
|
|
@@ -5638,7 +6361,7 @@ function createHonoApp(options) {
|
|
|
5638
6361
|
const doc = await db.update({
|
|
5639
6362
|
collection: collectionSlug,
|
|
5640
6363
|
id: slug,
|
|
5641
|
-
data: { ...version.data,
|
|
6364
|
+
data: { ...version.data, publishStatus: "draft", hasDraft: false },
|
|
5642
6365
|
tenantID: ctxTenantID
|
|
5643
6366
|
});
|
|
5644
6367
|
return c.json({ data: doc, message: "Restored successfully" });
|
|
@@ -5742,6 +6465,6 @@ function createRESTAPI(registry, db, options) {
|
|
|
5742
6465
|
});
|
|
5743
6466
|
}
|
|
5744
6467
|
|
|
5745
|
-
export { AuditLogger, AuthRoutes, InMemoryAuditLogger, InMemoryRateLimiter, MediaService, createAuditContext2 as createAuditContext, createHonoApp, createLocalStorage, createRESTAPI, getAppSecret, getEncryptionKey, getSessionConfig, init_secret, loadSecrets, resolveProvider, setDbAdapter };
|
|
5746
|
-
//# sourceMappingURL=chunk-
|
|
5747
|
-
//# sourceMappingURL=chunk-
|
|
6468
|
+
export { AuditLogger, AuthRoutes, InMemoryAuditLogger, InMemoryRateLimiter, MediaService, createAuditContext2 as createAuditContext, createCloudinaryStorage, createFtpStorage, createHonoApp, createLocalStorage, createRESTAPI, createS3Storage, getAppSecret, getDefaultRegistry, getEncryptionKey, getSessionConfig, init_secret, loadSecrets, resolveProvider, setDbAdapter };
|
|
6469
|
+
//# sourceMappingURL=chunk-PU2Z5VWF.js.map
|
|
6470
|
+
//# sourceMappingURL=chunk-PU2Z5VWF.js.map
|