@kyro-cms/core 0.9.0 → 0.9.2
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 +55 -593
- package/dist/{WebhookService-AefJfqX0.d.cts → WebhookService-BKszZlG0.d.cts} +1 -1
- package/dist/{WebhookService-118ZTFis.d.ts → WebhookService-Ccf1j-IN.d.ts} +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 +33 -99
- 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 +21 -97
- package/dist/api-handler.js.map +1 -1
- package/dist/{tenant-B1YB0Jy8.d.ts → base-CIuXkrH4.d.cts} +7 -15
- package/dist/{tenant-Cpeveji6.d.cts → base-fFo4lqER.d.ts} +7 -15
- package/dist/bootstrap-3PV3GJ3S.js +7 -0
- package/dist/{bootstrap-JCML6NFO.js.map → bootstrap-3PV3GJ3S.js.map} +1 -1
- package/dist/bootstrap-4CELFLJO.cjs +32 -0
- package/dist/{bootstrap-AKAUP6F6.cjs.map → bootstrap-4CELFLJO.cjs.map} +1 -1
- 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-Z6ZWNWWR.js → chunk-4CV4JOE5.js} +3 -9
- package/dist/{chunk-Z6ZWNWWR.js.map → chunk-4CV4JOE5.js.map} +1 -1
- package/dist/chunk-4M7X5HAB.cjs +173 -0
- package/dist/chunk-4M7X5HAB.cjs.map +1 -0
- package/dist/chunk-53NYVYVX.js +3243 -0
- package/dist/chunk-53NYVYVX.js.map +1 -0
- package/dist/{chunk-35U3FROB.js → chunk-5H3MWQJS.js} +714 -184
- package/dist/chunk-5H3MWQJS.js.map +1 -0
- package/dist/{chunk-YVUJBEXE.cjs → chunk-5PMQQFRE.cjs} +16 -7
- package/dist/chunk-5PMQQFRE.cjs.map +1 -0
- package/dist/{chunk-57P6MJKC.js → chunk-6UNONDW7.js} +94 -10
- package/dist/chunk-6UNONDW7.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-2OL4O2TH.cjs → chunk-7OS7TX2Q.cjs} +68 -62
- package/dist/chunk-7OS7TX2Q.cjs.map +1 -0
- package/dist/{chunk-3TPQ2BU6.js → chunk-BYBMTIMT.js} +2 -6
- package/dist/chunk-BYBMTIMT.js.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-OHVB4AJ7.js → chunk-CJX74IYK.js} +24 -18
- package/dist/chunk-CJX74IYK.js.map +1 -0
- package/dist/{chunk-5KVM3WEY.cjs → chunk-CNKT4PME.cjs} +1592 -868
- package/dist/chunk-CNKT4PME.cjs.map +1 -0
- package/dist/{chunk-G7VZBCD6.cjs → chunk-CZLDE2OZ.cjs} +2 -9
- package/dist/{chunk-G7VZBCD6.cjs.map → chunk-CZLDE2OZ.cjs.map} +1 -1
- package/dist/{chunk-WQBRWOQT.cjs → chunk-DPA3KWPY.cjs} +4 -3
- package/dist/chunk-DPA3KWPY.cjs.map +1 -0
- package/dist/{chunk-LINKCEG4.cjs → chunk-E2763JUP.cjs} +726 -196
- package/dist/chunk-E2763JUP.cjs.map +1 -0
- package/dist/chunk-E5UJBLQ7.js +220 -0
- package/dist/chunk-E5UJBLQ7.js.map +1 -0
- package/dist/{chunk-DVD5P72E.cjs → chunk-EEJUFDMF.cjs} +2 -6
- package/dist/chunk-EEJUFDMF.cjs.map +1 -0
- package/dist/chunk-FSKONGCX.cjs +253 -0
- package/dist/chunk-FSKONGCX.cjs.map +1 -0
- package/dist/{chunk-Y3QQN7PN.js → chunk-GAAHG2Z4.js} +13 -4
- package/dist/chunk-GAAHG2Z4.js.map +1 -0
- package/dist/chunk-GAOXD3XT.js +175 -0
- package/dist/chunk-GAOXD3XT.js.map +1 -0
- package/dist/{chunk-SA7NSSIQ.cjs → chunk-GUUB5EAG.cjs} +13 -187
- package/dist/chunk-GUUB5EAG.cjs.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-HXRD4B37.js → chunk-IPTZM3VE.js} +1423 -704
- package/dist/chunk-IPTZM3VE.js.map +1 -0
- package/dist/chunk-KC2GDBLS.cjs +84 -0
- package/dist/chunk-KC2GDBLS.cjs.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-NWUEVLQT.cjs +99 -0
- package/dist/chunk-NWUEVLQT.cjs.map +1 -0
- package/dist/{chunk-3AJE4SEG.js → chunk-OHC6UHFY.js} +208 -76
- package/dist/chunk-OHC6UHFY.js.map +1 -0
- package/dist/chunk-PHJRNPHY.cjs +3291 -0
- package/dist/chunk-PHJRNPHY.cjs.map +1 -0
- package/dist/{chunk-DXHRBMGB.js → chunk-PQ72Z6WC.js} +67 -112
- package/dist/chunk-PQ72Z6WC.js.map +1 -0
- package/dist/{chunk-K7JPTH3G.cjs → chunk-PV2I2KMI.cjs} +214 -82
- package/dist/chunk-PV2I2KMI.cjs.map +1 -0
- package/dist/{chunk-PDYFVNUX.cjs → chunk-Q23GAMLE.cjs} +71 -116
- package/dist/chunk-Q23GAMLE.cjs.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-2KVHZE6O.cjs → chunk-RFFSZSCL.cjs} +282 -190
- package/dist/chunk-RFFSZSCL.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-WOWUL7ZY.js → chunk-UUDTPZX6.js} +5 -4
- package/dist/chunk-UUDTPZX6.js.map +1 -0
- package/dist/{chunk-QPPDLRNR.js → chunk-V7KZQIZ6.js} +277 -185
- package/dist/chunk-V7KZQIZ6.js.map +1 -0
- package/dist/{chunk-3ZFYL34R.js → chunk-WXVB364T.js} +12 -185
- package/dist/chunk-WXVB364T.js.map +1 -0
- package/dist/chunk-XEB7PH2E.js +81 -0
- package/dist/chunk-XEB7PH2E.js.map +1 -0
- package/dist/{chunk-IA6AU5PI.cjs → chunk-Y7AQK4R4.cjs} +94 -10
- package/dist/chunk-Y7AQK4R4.cjs.map +1 -0
- package/dist/chunk-YFAVQQTU.js +92 -0
- package/dist/chunk-YFAVQQTU.js.map +1 -0
- package/dist/cli/index.cjs +6 -6
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +6 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/client.cjs +4 -4
- package/dist/client.d.cts +3 -3
- package/dist/client.d.ts +3 -3
- package/dist/client.js +2 -2
- package/dist/drizzle/index.cjs +15 -14
- package/dist/drizzle/index.d.cts +10 -14
- package/dist/drizzle/index.d.ts +10 -14
- package/dist/drizzle/index.js +6 -5
- package/dist/fields/index.cjs +22 -38
- package/dist/fields/index.d.cts +2 -22
- package/dist/fields/index.d.ts +2 -22
- package/dist/fields/index.js +2 -2
- package/dist/graphql/index.cjs +6 -5
- package/dist/graphql/index.d.cts +5 -3
- package/dist/graphql/index.d.ts +5 -3
- package/dist/graphql/index.js +4 -3
- package/dist/index-BKta3cBH.d.cts +277 -0
- package/dist/index-ClOqnkTO.d.ts +277 -0
- package/dist/index.cjs +310 -168
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +130 -211
- package/dist/index.d.ts +130 -211
- package/dist/index.js +174 -35
- package/dist/index.js.map +1 -1
- package/dist/integration.cjs +3 -3
- package/dist/integration.js +2 -2
- package/dist/media-7WDX4BDJ.js +4 -0
- package/dist/{media-GPPTZ43E.js.map → media-7WDX4BDJ.js.map} +1 -1
- package/dist/{media-XNTUFJZR.cjs → media-TUSLVRQ6.cjs} +3 -3
- package/dist/{media-XNTUFJZR.cjs.map → media-TUSLVRQ6.cjs.map} +1 -1
- package/dist/mongo-auth-adapter-GT4S7SCU.cjs +17 -0
- package/dist/{mongo-auth-adapter-NHHUJHVH.cjs.map → mongo-auth-adapter-GT4S7SCU.cjs.map} +1 -1
- package/dist/mongo-auth-adapter-M7VV4LNB.js +4 -0
- package/dist/{mongo-auth-adapter-NJQUUCTP.js.map → mongo-auth-adapter-M7VV4LNB.js.map} +1 -1
- package/dist/mongodb/index.cjs +9 -8
- package/dist/mongodb/index.d.cts +6 -13
- package/dist/mongodb/index.d.ts +6 -13
- package/dist/mongodb/index.js +5 -4
- package/dist/postgres-auth-adapter-AFAPISH7.js +5 -0
- package/dist/{postgres-auth-adapter-3T2NKTSE.js.map → postgres-auth-adapter-AFAPISH7.js.map} +1 -1
- package/dist/postgres-auth-adapter-SFDTLONT.cjs +14 -0
- package/dist/{postgres-auth-adapter-7IEENCKQ.cjs.map → postgres-auth-adapter-SFDTLONT.cjs.map} +1 -1
- package/dist/redis-adapter-UQX4EE3B.cjs +13 -0
- package/dist/{redis-adapter-D2E2S3GB.cjs.map → redis-adapter-UQX4EE3B.cjs.map} +1 -1
- package/dist/redis-adapter-XALOGWY3.js +4 -0
- package/dist/{redis-adapter-VQXD7ESY.js.map → redis-adapter-XALOGWY3.js.map} +1 -1
- package/dist/rest/index.cjs +16 -15
- package/dist/rest/index.d.cts +4 -4
- package/dist/rest/index.d.ts +4 -4
- package/dist/rest/index.js +14 -13
- package/dist/{schema-37SE2F4B.cjs → schema-6QL3USNB.cjs} +15 -15
- package/dist/{schema-37SE2F4B.cjs.map → schema-6QL3USNB.cjs.map} +1 -1
- package/dist/{schema-5PHL5IVB.js → schema-FNNWEAAW.js} +4 -4
- package/dist/{schema-5PHL5IVB.js.map → schema-FNNWEAAW.js.map} +1 -1
- package/dist/sqlite-adapter-AQB5TCGV.cjs +13 -0
- package/dist/{sqlite-adapter-LVK5PS4T.cjs.map → sqlite-adapter-AQB5TCGV.cjs.map} +1 -1
- package/dist/sqlite-adapter-N5H6IM2X.js +4 -0
- package/dist/{sqlite-adapter-TR3U3W6Q.js.map → sqlite-adapter-N5H6IM2X.js.map} +1 -1
- package/dist/templates/index.cjs +134 -32
- package/dist/templates/index.d.cts +52 -9
- package/dist/templates/index.d.ts +52 -9
- package/dist/templates/index.js +4 -2
- package/dist/trpc/index.cjs +14 -13
- package/dist/trpc/index.d.cts +55 -49
- package/dist/trpc/index.d.ts +55 -49
- package/dist/trpc/index.js +5 -4
- 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-VtjUxIMp.d.cts → types-DeSApf9T.d.cts} +36 -14
- package/dist/{types-VtjUxIMp.d.ts → types-DeSApf9T.d.ts} +36 -14
- package/dist/{types-J3R9nVsZ.d.cts → types-Dgzlftb7.d.ts} +32 -28
- package/dist/{types-Bs1up4yP.d.ts → types-Ds0tCA3L.d.cts} +32 -28
- package/dist/ws/index.cjs +6 -6
- package/dist/ws/index.js +2 -2
- 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-3TPQ2BU6.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-DVD5P72E.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/media-GPPTZ43E.js +0 -4
- 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,15 +1,15 @@
|
|
|
1
|
-
import { PasswordPolicy, ConfigService, EmailTransport } from './chunk-
|
|
2
|
-
import {
|
|
1
|
+
import { PasswordPolicy, ConfigService, EmailTransport } from './chunk-6UNONDW7.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 {
|
|
12
|
-
import { __esm, __export, __toCommonJS, __require } from './chunk-Z6ZWNWWR.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-V7KZQIZ6.js';
|
|
8
|
+
import { PostgresAuthAdapter } from './chunk-CJX74IYK.js';
|
|
9
|
+
import { MongoDBAdapter } from './chunk-PQ72Z6WC.js';
|
|
10
|
+
import { MongoDBAuthAdapter } from './chunk-7OGPN7MP.js';
|
|
11
|
+
import { hasPermission } from './chunk-L4EZKIEX.js';
|
|
12
|
+
import { __esm, __export, __toCommonJS } from './chunk-4CV4JOE5.js';
|
|
13
13
|
import crypto, { randomBytes, createHash } from 'crypto';
|
|
14
14
|
import { createStorage } from 'unstorage';
|
|
15
15
|
import fsDriver from 'unstorage/drivers/fs';
|
|
@@ -17,11 +17,13 @@ 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
|
+
import { NodeHttpHandler } from '@smithy/node-http-handler';
|
|
23
24
|
import { Readable } from 'stream';
|
|
24
25
|
import { Client } from 'basic-ftp';
|
|
26
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
25
27
|
|
|
26
28
|
function setDbAdapter(adapter) {
|
|
27
29
|
dbAdapter = adapter;
|
|
@@ -30,8 +32,8 @@ async function loadSecrets() {
|
|
|
30
32
|
if (dbAdapter) {
|
|
31
33
|
try {
|
|
32
34
|
const result = await dbAdapter.findOne({
|
|
33
|
-
collection: "
|
|
34
|
-
where: {
|
|
35
|
+
collection: "_globals_system",
|
|
36
|
+
where: {}
|
|
35
37
|
});
|
|
36
38
|
if (result) {
|
|
37
39
|
cachedSecrets = {
|
|
@@ -441,6 +443,9 @@ function createTenantContextFromUser(user) {
|
|
|
441
443
|
};
|
|
442
444
|
}
|
|
443
445
|
|
|
446
|
+
// src/api/rest/hono-app.ts
|
|
447
|
+
init_secret();
|
|
448
|
+
|
|
444
449
|
// src/auth/security/in-memory-rate-limit.ts
|
|
445
450
|
var InMemoryRateLimiter = class {
|
|
446
451
|
storage = /* @__PURE__ */ new Map();
|
|
@@ -773,7 +778,7 @@ var AuditLogger = class {
|
|
|
773
778
|
serializeLog(log) {
|
|
774
779
|
const result = {
|
|
775
780
|
id: log.id,
|
|
776
|
-
timestamp: log.timestamp.toISOString(),
|
|
781
|
+
timestamp: new Date(log.timestamp).toISOString(),
|
|
777
782
|
action: log.action,
|
|
778
783
|
resource: log.resource,
|
|
779
784
|
success: log.success ? "1" : "0"
|
|
@@ -1168,23 +1173,19 @@ var AuthRoutes = class {
|
|
|
1168
1173
|
}
|
|
1169
1174
|
}
|
|
1170
1175
|
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);
|
|
1176
|
+
const session = await getCurrentUser(req);
|
|
1177
|
+
if (!session) {
|
|
1178
|
+
return this.errorResponse("Not authenticated", 401);
|
|
1187
1179
|
}
|
|
1180
|
+
return this.jsonResponse({
|
|
1181
|
+
success: true,
|
|
1182
|
+
user: {
|
|
1183
|
+
id: session.userId,
|
|
1184
|
+
email: session.email,
|
|
1185
|
+
role: session.role,
|
|
1186
|
+
tenantId: session.tenantId
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1188
1189
|
}
|
|
1189
1190
|
async changePassword(req) {
|
|
1190
1191
|
const session = await getCurrentUser(req);
|
|
@@ -1294,7 +1295,7 @@ var AuthRoutes = class {
|
|
|
1294
1295
|
}
|
|
1295
1296
|
if (this.auditLogger) {
|
|
1296
1297
|
await this.auditLogger.log({
|
|
1297
|
-
action: "
|
|
1298
|
+
action: "password_reset",
|
|
1298
1299
|
userId: user.id,
|
|
1299
1300
|
userEmail: user.email,
|
|
1300
1301
|
resource: "auth",
|
|
@@ -1482,7 +1483,7 @@ var AuthRoutes = class {
|
|
|
1482
1483
|
}
|
|
1483
1484
|
};
|
|
1484
1485
|
function createLocalStorage(config) {
|
|
1485
|
-
const { uploadDir, baseUrl = "/uploads" } = config;
|
|
1486
|
+
const { uploadDir = join(process2.cwd(), "public", "uploads"), baseUrl = "/uploads" } = config;
|
|
1486
1487
|
async function ensureDir(dir) {
|
|
1487
1488
|
if (!existsSync(dir)) {
|
|
1488
1489
|
await mkdir(dir, { recursive: true });
|
|
@@ -1670,6 +1671,467 @@ function createLocalStorage(config) {
|
|
|
1670
1671
|
}
|
|
1671
1672
|
};
|
|
1672
1673
|
}
|
|
1674
|
+
|
|
1675
|
+
// src/storage/imgix.ts
|
|
1676
|
+
function createImgixStorage(config) {
|
|
1677
|
+
const signUrl = (path3, params) => {
|
|
1678
|
+
if (!config.signKey) return path3;
|
|
1679
|
+
const signer = new TextEncoder();
|
|
1680
|
+
const key = signer.encode(config.signKey);
|
|
1681
|
+
const data = signer.encode(path3 + params.toString());
|
|
1682
|
+
let hash = 0;
|
|
1683
|
+
const combined = new Uint8Array(key.length + data.length);
|
|
1684
|
+
combined.set(key);
|
|
1685
|
+
combined.set(data, key.length);
|
|
1686
|
+
for (let i = 0; i < combined.length; i++) {
|
|
1687
|
+
hash = (hash << 5) - hash + combined[i] | 0;
|
|
1688
|
+
}
|
|
1689
|
+
params.set("s", Math.abs(hash).toString(16));
|
|
1690
|
+
return path3;
|
|
1691
|
+
};
|
|
1692
|
+
return {
|
|
1693
|
+
name: "imgix",
|
|
1694
|
+
displayName: "Imgix",
|
|
1695
|
+
supportsDynamicResize: true,
|
|
1696
|
+
async upload(_file, _options) {
|
|
1697
|
+
throw new Error(
|
|
1698
|
+
"Imgix is a transformation service. Use another provider for uploads."
|
|
1699
|
+
);
|
|
1700
|
+
},
|
|
1701
|
+
async uploadFromUrl(url, options) {
|
|
1702
|
+
const filename = options?.filename || url.split("/").pop() || "file";
|
|
1703
|
+
const response = await fetch(url);
|
|
1704
|
+
if (!response.ok) {
|
|
1705
|
+
throw new Error(`Failed to fetch: ${response.statusText}`);
|
|
1706
|
+
}
|
|
1707
|
+
const blob = await response.blob();
|
|
1708
|
+
new File([blob], filename, { type: blob.type });
|
|
1709
|
+
return {
|
|
1710
|
+
id: Buffer.from(url).toString("base64").slice(0, 20),
|
|
1711
|
+
filename,
|
|
1712
|
+
originalName: filename,
|
|
1713
|
+
mimeType: blob.type,
|
|
1714
|
+
size: blob.size,
|
|
1715
|
+
url: this.getImageUrl(url),
|
|
1716
|
+
thumbnailUrl: this.getImageUrl(url, {
|
|
1717
|
+
width: 200,
|
|
1718
|
+
height: 200,
|
|
1719
|
+
fit: "crop"
|
|
1720
|
+
}),
|
|
1721
|
+
provider: "imgix",
|
|
1722
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1723
|
+
};
|
|
1724
|
+
},
|
|
1725
|
+
async delete(_url) {
|
|
1726
|
+
},
|
|
1727
|
+
async rename(_oldUrl, newKey) {
|
|
1728
|
+
return `https://${config.domain}/${newKey}`;
|
|
1729
|
+
},
|
|
1730
|
+
getImageUrl(url, transforms) {
|
|
1731
|
+
const parsed = new URL(url);
|
|
1732
|
+
const params = new URLSearchParams(parsed.search);
|
|
1733
|
+
if (config.defaultParameters) {
|
|
1734
|
+
Object.entries(config.defaultParameters).forEach(([key, value]) => {
|
|
1735
|
+
if (!params.has(key)) {
|
|
1736
|
+
params.set(key, value);
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
if (transforms) {
|
|
1741
|
+
if (transforms.width) params.set("w", String(transforms.width));
|
|
1742
|
+
if (transforms.height) params.set("h", String(transforms.height));
|
|
1743
|
+
if (transforms.quality) params.set("q", String(transforms.quality));
|
|
1744
|
+
if (transforms.format) params.set("fm", transforms.format);
|
|
1745
|
+
if (transforms.fit) params.set("fit", transforms.fit);
|
|
1746
|
+
if (transforms.blur) params.set("blur", String(transforms.blur));
|
|
1747
|
+
if (transforms.sharpen) params.set("sharp", String(transforms.sharpen));
|
|
1748
|
+
}
|
|
1749
|
+
params.set("auto", "compress,format");
|
|
1750
|
+
parsed.pathname + "?" + params.toString();
|
|
1751
|
+
if (config.signKey) {
|
|
1752
|
+
signUrl(parsed.pathname + params.toString(), params);
|
|
1753
|
+
}
|
|
1754
|
+
return `https://${config.domain}${parsed.pathname}${params.toString() ? "?" + params.toString() : ""}`;
|
|
1755
|
+
},
|
|
1756
|
+
async generateThumbnail(file) {
|
|
1757
|
+
return this.getImageUrl(file.url, {
|
|
1758
|
+
width: 200,
|
|
1759
|
+
height: 200,
|
|
1760
|
+
fit: "crop"
|
|
1761
|
+
});
|
|
1762
|
+
},
|
|
1763
|
+
async list() {
|
|
1764
|
+
return [];
|
|
1765
|
+
},
|
|
1766
|
+
async exists(url) {
|
|
1767
|
+
try {
|
|
1768
|
+
const response = await fetch(url, { method: "HEAD" });
|
|
1769
|
+
return response.ok;
|
|
1770
|
+
} catch {
|
|
1771
|
+
return false;
|
|
1772
|
+
}
|
|
1773
|
+
},
|
|
1774
|
+
async createFolder() {
|
|
1775
|
+
},
|
|
1776
|
+
async deleteFolder() {
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// src/storage/bunny.ts
|
|
1782
|
+
function getUrl(key, config) {
|
|
1783
|
+
if (config.cdnUrl) {
|
|
1784
|
+
return `${config.cdnUrl.replace(/\/$/, "")}/${key}`;
|
|
1785
|
+
}
|
|
1786
|
+
return `https://${config.storageZone}.b-cdn.net/${key}`;
|
|
1787
|
+
}
|
|
1788
|
+
function getUrlPrefix(config) {
|
|
1789
|
+
if (config.cdnUrl) {
|
|
1790
|
+
return config.cdnUrl.replace(/\/$/, "") + "/";
|
|
1791
|
+
}
|
|
1792
|
+
return `https://${config.storageZone}.b-cdn.net/`;
|
|
1793
|
+
}
|
|
1794
|
+
function createBunnyStorage(config) {
|
|
1795
|
+
const baseUrl = `https://storage.bunnycdn.com/${config.storageZone}`;
|
|
1796
|
+
const getKey = (path3) => {
|
|
1797
|
+
const prefix = config.prefix ? `${config.prefix}/` : "";
|
|
1798
|
+
return `${prefix}${path3}`.replace(/\/+/g, "/");
|
|
1799
|
+
};
|
|
1800
|
+
return {
|
|
1801
|
+
name: "bunny",
|
|
1802
|
+
displayName: "Bunny.net Storage",
|
|
1803
|
+
supportsDynamicResize: true,
|
|
1804
|
+
async upload(file, options) {
|
|
1805
|
+
const key = getKey(
|
|
1806
|
+
`${options?.folder ? `${options.folder}/` : ""}${options?.filename || file.name}`
|
|
1807
|
+
);
|
|
1808
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
1809
|
+
const response = await fetch(`${baseUrl}/${key}`, {
|
|
1810
|
+
method: "PUT",
|
|
1811
|
+
headers: {
|
|
1812
|
+
AccessKey: config.apiKey,
|
|
1813
|
+
"Content-Type": file.type
|
|
1814
|
+
},
|
|
1815
|
+
body: buffer
|
|
1816
|
+
});
|
|
1817
|
+
if (!response.ok) {
|
|
1818
|
+
const errorText = await response.text();
|
|
1819
|
+
throw new Error(
|
|
1820
|
+
`Bunny.net upload failed: ${response.status} ${errorText}`
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
return {
|
|
1824
|
+
id: Buffer.from(key).toString("base64url"),
|
|
1825
|
+
filename: options?.filename || file.name,
|
|
1826
|
+
originalName: file.name,
|
|
1827
|
+
mimeType: file.type,
|
|
1828
|
+
size: buffer.length,
|
|
1829
|
+
url: getUrl(key, config),
|
|
1830
|
+
thumbnailUrl: file.type.startsWith("image/") ? getUrl(key, config) : void 0,
|
|
1831
|
+
folder: options?.folder,
|
|
1832
|
+
provider: "bunny",
|
|
1833
|
+
metadata: options?.metadata,
|
|
1834
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1835
|
+
};
|
|
1836
|
+
},
|
|
1837
|
+
async uploadFromUrl(url) {
|
|
1838
|
+
const response = await fetch(url);
|
|
1839
|
+
if (!response.ok) {
|
|
1840
|
+
throw new Error(`Failed to fetch: ${response.statusText}`);
|
|
1841
|
+
}
|
|
1842
|
+
const blob = await response.blob();
|
|
1843
|
+
const filename = url.split("/").pop() || "file";
|
|
1844
|
+
const file = new File([blob], filename, { type: blob.type });
|
|
1845
|
+
return this.upload(file);
|
|
1846
|
+
},
|
|
1847
|
+
async delete(url) {
|
|
1848
|
+
const key = url.replace(getUrlPrefix(config), "");
|
|
1849
|
+
const response = await fetch(`${baseUrl}/${key}`, {
|
|
1850
|
+
method: "DELETE",
|
|
1851
|
+
headers: {
|
|
1852
|
+
AccessKey: config.apiKey
|
|
1853
|
+
}
|
|
1854
|
+
});
|
|
1855
|
+
if (!response.ok && response.status !== 404) {
|
|
1856
|
+
const errorText = await response.text();
|
|
1857
|
+
throw new Error(
|
|
1858
|
+
`Bunny.net delete failed: ${response.status} ${errorText}`
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
},
|
|
1862
|
+
async rename(oldUrl, newKey) {
|
|
1863
|
+
const oldKey = oldUrl.replace(getUrlPrefix(config), "");
|
|
1864
|
+
const fullPath = config.prefix ? `${config.prefix}/${newKey}` : newKey;
|
|
1865
|
+
const response = await fetch(`${baseUrl}/${oldKey}`, {
|
|
1866
|
+
method: "GET",
|
|
1867
|
+
headers: {
|
|
1868
|
+
AccessKey: config.apiKey
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
if (!response.ok) {
|
|
1872
|
+
throw new Error(`Bunny.net rename failed: could not read old file`);
|
|
1873
|
+
}
|
|
1874
|
+
const content = await response.arrayBuffer();
|
|
1875
|
+
await fetch(`${baseUrl}/${fullPath}`, {
|
|
1876
|
+
method: "PUT",
|
|
1877
|
+
headers: {
|
|
1878
|
+
AccessKey: config.apiKey,
|
|
1879
|
+
"Content-Type": response.headers.get("Content-Type") || "application/octet-stream"
|
|
1880
|
+
},
|
|
1881
|
+
body: content
|
|
1882
|
+
});
|
|
1883
|
+
await this.delete(oldUrl);
|
|
1884
|
+
return getUrl(fullPath, config);
|
|
1885
|
+
},
|
|
1886
|
+
getImageUrl(url, transforms) {
|
|
1887
|
+
if (!transforms || Object.keys(transforms).length === 0) return url;
|
|
1888
|
+
const params = new URLSearchParams({ url });
|
|
1889
|
+
if (transforms.width) params.set("w", String(transforms.width));
|
|
1890
|
+
if (transforms.height) params.set("h", String(transforms.height));
|
|
1891
|
+
if (transforms.quality) params.set("q", String(transforms.quality));
|
|
1892
|
+
if (transforms.format) params.set("f", transforms.format);
|
|
1893
|
+
return `/api/media/resize?${params.toString()}`;
|
|
1894
|
+
},
|
|
1895
|
+
async generateThumbnail(file) {
|
|
1896
|
+
return this.getImageUrl(file.url, { width: 400, height: 400 });
|
|
1897
|
+
},
|
|
1898
|
+
async list(prefix) {
|
|
1899
|
+
const key = getKey(prefix || "");
|
|
1900
|
+
const response = await fetch(`${baseUrl}/${key}`, {
|
|
1901
|
+
method: "GET",
|
|
1902
|
+
headers: {
|
|
1903
|
+
AccessKey: config.apiKey
|
|
1904
|
+
}
|
|
1905
|
+
});
|
|
1906
|
+
if (!response.ok) {
|
|
1907
|
+
const errorText = await response.text();
|
|
1908
|
+
throw new Error(
|
|
1909
|
+
`Bunny.net list failed: ${response.status} ${errorText}`
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
const items = await response.json();
|
|
1913
|
+
return items.map((item) => ({
|
|
1914
|
+
id: Buffer.from(item.ObjectName || "").toString("base64url"),
|
|
1915
|
+
filename: item.ObjectName?.split("/").pop() || "",
|
|
1916
|
+
originalName: item.ObjectName?.split("/").pop() || "",
|
|
1917
|
+
mimeType: "application/octet-stream",
|
|
1918
|
+
size: item.Length || 0,
|
|
1919
|
+
url: getUrl(item.ObjectName || "", config),
|
|
1920
|
+
provider: "bunny",
|
|
1921
|
+
createdAt: item.LastChanged || (/* @__PURE__ */ new Date()).toISOString()
|
|
1922
|
+
}));
|
|
1923
|
+
},
|
|
1924
|
+
async exists(url) {
|
|
1925
|
+
const key = url.replace(getUrlPrefix(config), "");
|
|
1926
|
+
const response = await fetch(`${baseUrl}/${key}`, {
|
|
1927
|
+
method: "HEAD",
|
|
1928
|
+
headers: {
|
|
1929
|
+
AccessKey: config.apiKey
|
|
1930
|
+
}
|
|
1931
|
+
});
|
|
1932
|
+
return response.ok;
|
|
1933
|
+
},
|
|
1934
|
+
async createFolder(folder) {
|
|
1935
|
+
const key = getKey(`${folder}/`);
|
|
1936
|
+
await fetch(`${baseUrl}/${key}`, {
|
|
1937
|
+
method: "PUT",
|
|
1938
|
+
headers: {
|
|
1939
|
+
AccessKey: config.apiKey,
|
|
1940
|
+
"Content-Length": "0"
|
|
1941
|
+
},
|
|
1942
|
+
body: ""
|
|
1943
|
+
});
|
|
1944
|
+
},
|
|
1945
|
+
async deleteFolder(folder) {
|
|
1946
|
+
const key = getKey(`${folder}/`);
|
|
1947
|
+
await fetch(`${baseUrl}/${key}`, {
|
|
1948
|
+
method: "DELETE",
|
|
1949
|
+
headers: {
|
|
1950
|
+
AccessKey: config.apiKey
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
var StorageProviderRegistry = class {
|
|
1957
|
+
providers = /* @__PURE__ */ new Map();
|
|
1958
|
+
providerToPlugin = /* @__PURE__ */ new Map();
|
|
1959
|
+
disabledPlugins = /* @__PURE__ */ new Set();
|
|
1960
|
+
constructor() {
|
|
1961
|
+
this.registerLocal();
|
|
1962
|
+
this.registerImgix();
|
|
1963
|
+
this.registerBunny();
|
|
1964
|
+
}
|
|
1965
|
+
registerLocal() {
|
|
1966
|
+
this.register({
|
|
1967
|
+
type: "local",
|
|
1968
|
+
displayName: "Local Server",
|
|
1969
|
+
configKey: "local",
|
|
1970
|
+
configFields: [
|
|
1971
|
+
{
|
|
1972
|
+
name: "uploadDir",
|
|
1973
|
+
type: "text",
|
|
1974
|
+
label: "Upload Directory",
|
|
1975
|
+
defaultValue: "./public/uploads"
|
|
1976
|
+
},
|
|
1977
|
+
{
|
|
1978
|
+
name: "baseUrl",
|
|
1979
|
+
type: "text",
|
|
1980
|
+
label: "Base URL",
|
|
1981
|
+
defaultValue: "/uploads"
|
|
1982
|
+
}
|
|
1983
|
+
],
|
|
1984
|
+
extractConfig: (sc, key) => sc[key] || {},
|
|
1985
|
+
extractRawConfig: (c) => {
|
|
1986
|
+
const localConfig = c?.local || c;
|
|
1987
|
+
const savedUploadDir = (localConfig?.uploadDir || "").trim();
|
|
1988
|
+
let uploadDir;
|
|
1989
|
+
if (savedUploadDir) {
|
|
1990
|
+
if (path.isAbsolute(savedUploadDir)) {
|
|
1991
|
+
uploadDir = savedUploadDir;
|
|
1992
|
+
} else if (savedUploadDir.includes("/") || savedUploadDir.includes("\\")) {
|
|
1993
|
+
uploadDir = path.resolve(process.cwd(), savedUploadDir);
|
|
1994
|
+
} else {
|
|
1995
|
+
uploadDir = path.join(process.cwd(), "public", savedUploadDir);
|
|
1996
|
+
}
|
|
1997
|
+
} else {
|
|
1998
|
+
uploadDir = path.join(process.cwd(), "public", "uploads");
|
|
1999
|
+
}
|
|
2000
|
+
const savedBaseUrl = (localConfig?.baseUrl || "").trim();
|
|
2001
|
+
let baseUrl;
|
|
2002
|
+
if (savedBaseUrl) {
|
|
2003
|
+
baseUrl = savedBaseUrl.startsWith("/") ? savedBaseUrl : `/${savedBaseUrl}`;
|
|
2004
|
+
} else {
|
|
2005
|
+
baseUrl = "/uploads";
|
|
2006
|
+
}
|
|
2007
|
+
return { uploadDir, baseUrl };
|
|
2008
|
+
},
|
|
2009
|
+
factory: (c) => createLocalStorage(c)
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
registerImgix() {
|
|
2013
|
+
this.register({
|
|
2014
|
+
type: "imgix",
|
|
2015
|
+
displayName: "Imgix",
|
|
2016
|
+
configKey: "imgix",
|
|
2017
|
+
configFields: [
|
|
2018
|
+
{ name: "domain", type: "text", label: "Domain", required: true },
|
|
2019
|
+
{ name: "signKey", type: "password", label: "Sign Key" }
|
|
2020
|
+
],
|
|
2021
|
+
extractConfig: (sc, key) => sc[key] || {},
|
|
2022
|
+
extractRawConfig: (c) => c?.imgix || c,
|
|
2023
|
+
factory: (c) => createImgixStorage(c)
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
registerBunny() {
|
|
2027
|
+
this.register({
|
|
2028
|
+
type: "bunny",
|
|
2029
|
+
displayName: "Bunny.net",
|
|
2030
|
+
configKey: "bunny",
|
|
2031
|
+
configFields: [
|
|
2032
|
+
{
|
|
2033
|
+
name: "storageZone",
|
|
2034
|
+
type: "text",
|
|
2035
|
+
label: "Storage Zone",
|
|
2036
|
+
required: true
|
|
2037
|
+
},
|
|
2038
|
+
{ name: "apiKey", type: "password", label: "API Key", required: true },
|
|
2039
|
+
{ name: "cdnUrl", type: "text", label: "CDN URL" },
|
|
2040
|
+
{ name: "prefix", type: "text", label: "Path Prefix" }
|
|
2041
|
+
],
|
|
2042
|
+
extractConfig: (sc, key) => sc[key] || {},
|
|
2043
|
+
extractRawConfig: (c) => c?.bunny || c,
|
|
2044
|
+
factory: (c) => createBunnyStorage(c)
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
register(registration) {
|
|
2048
|
+
if (this.providers.has(registration.type)) {
|
|
2049
|
+
console.warn(
|
|
2050
|
+
`[StorageRegistry] Provider "${registration.type}" already registered, skipping`
|
|
2051
|
+
);
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
if (registration.pluginName) {
|
|
2055
|
+
this.providerToPlugin.set(registration.type, registration.pluginName);
|
|
2056
|
+
}
|
|
2057
|
+
this.providers.set(registration.type, registration);
|
|
2058
|
+
}
|
|
2059
|
+
unregister(type) {
|
|
2060
|
+
this.providers.delete(type);
|
|
2061
|
+
this.providerToPlugin.delete(type);
|
|
2062
|
+
}
|
|
2063
|
+
get(type) {
|
|
2064
|
+
return this.providers.get(type);
|
|
2065
|
+
}
|
|
2066
|
+
getAll() {
|
|
2067
|
+
return Array.from(this.providers.values());
|
|
2068
|
+
}
|
|
2069
|
+
getAllAvailable(isPluginEnabled) {
|
|
2070
|
+
const all = this.getAll();
|
|
2071
|
+
if (!isPluginEnabled) return all;
|
|
2072
|
+
return all.filter((p) => {
|
|
2073
|
+
if (!p.pluginName) return true;
|
|
2074
|
+
return isPluginEnabled(p.pluginName);
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
has(type) {
|
|
2078
|
+
return this.providers.has(type);
|
|
2079
|
+
}
|
|
2080
|
+
async resolve(type, storageConfig) {
|
|
2081
|
+
const reg = this.providers.get(type);
|
|
2082
|
+
if (!reg) {
|
|
2083
|
+
throw new Error(
|
|
2084
|
+
`Unknown storage provider type: "${type}". Is the plugin that provides it enabled?`
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
if (reg.pluginName && this.disabledPlugins.has(reg.pluginName)) {
|
|
2088
|
+
throw new Error(
|
|
2089
|
+
`Storage provider "${type}" is not available (plugin "${reg.pluginName}" is disabled)`
|
|
2090
|
+
);
|
|
2091
|
+
}
|
|
2092
|
+
const configKey = reg.configKey || type;
|
|
2093
|
+
const config = reg.extractConfig(storageConfig, configKey);
|
|
2094
|
+
return reg.factory(config);
|
|
2095
|
+
}
|
|
2096
|
+
async resolveWithConfig(type, config) {
|
|
2097
|
+
const reg = this.providers.get(type);
|
|
2098
|
+
if (!reg) {
|
|
2099
|
+
throw new Error(
|
|
2100
|
+
`Unknown storage provider type: "${type}". Is the plugin that provides it enabled?`
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2103
|
+
if (reg.pluginName && this.disabledPlugins.has(reg.pluginName)) {
|
|
2104
|
+
throw new Error(
|
|
2105
|
+
`Storage provider "${type}" is not available (plugin "${reg.pluginName}" is disabled)`
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
const providerConfig = reg.extractRawConfig(config);
|
|
2109
|
+
return reg.factory(providerConfig);
|
|
2110
|
+
}
|
|
2111
|
+
getProviderPluginName(type) {
|
|
2112
|
+
return this.providerToPlugin.get(type);
|
|
2113
|
+
}
|
|
2114
|
+
getAllPluginNames() {
|
|
2115
|
+
return Array.from(new Set(this.providerToPlugin.values()));
|
|
2116
|
+
}
|
|
2117
|
+
setPluginEnabled(name, enabled) {
|
|
2118
|
+
if (enabled) {
|
|
2119
|
+
this.disabledPlugins.delete(name);
|
|
2120
|
+
} else {
|
|
2121
|
+
this.disabledPlugins.add(name);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
isPluginEnabled(name) {
|
|
2125
|
+
return !this.disabledPlugins.has(name);
|
|
2126
|
+
}
|
|
2127
|
+
};
|
|
2128
|
+
var instance = null;
|
|
2129
|
+
function getDefaultRegistry() {
|
|
2130
|
+
if (!instance) {
|
|
2131
|
+
instance = new StorageProviderRegistry();
|
|
2132
|
+
}
|
|
2133
|
+
return instance;
|
|
2134
|
+
}
|
|
1673
2135
|
function extractPublicDevUrlId(url) {
|
|
1674
2136
|
if (!url) return "";
|
|
1675
2137
|
if (url.startsWith("pub-")) return url;
|
|
@@ -1703,7 +2165,7 @@ function getPublicUrl(key, config) {
|
|
|
1703
2165
|
return `https://${config.bucket}.s3.${config.region}.amazonaws.com/${normalizedKey}`;
|
|
1704
2166
|
}
|
|
1705
2167
|
}
|
|
1706
|
-
function
|
|
2168
|
+
function getUrlPrefix2(config) {
|
|
1707
2169
|
if (config.cdnUrl) {
|
|
1708
2170
|
return config.cdnUrl.replace(/\/$/, "") + "/";
|
|
1709
2171
|
}
|
|
@@ -1765,17 +2227,17 @@ function createS3Storage(config) {
|
|
|
1765
2227
|
tls: true,
|
|
1766
2228
|
// R2 requires specific SSL configuration
|
|
1767
2229
|
...config.provider === "r2" && {
|
|
1768
|
-
requestHandler: new
|
|
2230
|
+
requestHandler: new NodeHttpHandler({
|
|
1769
2231
|
connectionTimeout: 1e4,
|
|
1770
2232
|
socketTimeout: 1e4
|
|
1771
2233
|
})
|
|
1772
2234
|
}
|
|
1773
2235
|
});
|
|
1774
|
-
const getKey = (
|
|
2236
|
+
const getKey = (path3) => {
|
|
1775
2237
|
const prefix = config.prefix ? `${config.prefix}/` : "";
|
|
1776
|
-
return `${prefix}${
|
|
2238
|
+
return `${prefix}${path3}`.replace(/\/+/g, "/");
|
|
1777
2239
|
};
|
|
1778
|
-
const
|
|
2240
|
+
const getUrl2 = (key) => getPublicUrl(key, config);
|
|
1779
2241
|
return {
|
|
1780
2242
|
name: config.provider,
|
|
1781
2243
|
displayName: getDisplayName(config.provider),
|
|
@@ -1806,8 +2268,8 @@ function createS3Storage(config) {
|
|
|
1806
2268
|
originalName: file.name,
|
|
1807
2269
|
mimeType: file.type,
|
|
1808
2270
|
size: buffer.length,
|
|
1809
|
-
url:
|
|
1810
|
-
thumbnailUrl: file.type.startsWith("image/") ?
|
|
2271
|
+
url: getUrl2(key),
|
|
2272
|
+
thumbnailUrl: file.type.startsWith("image/") ? getUrl2(key) : void 0,
|
|
1811
2273
|
folder: options?.folder,
|
|
1812
2274
|
provider: config.provider,
|
|
1813
2275
|
metadata: {
|
|
@@ -1828,7 +2290,7 @@ function createS3Storage(config) {
|
|
|
1828
2290
|
return this.upload(file);
|
|
1829
2291
|
},
|
|
1830
2292
|
async delete(url) {
|
|
1831
|
-
const key = url.replace(
|
|
2293
|
+
const key = url.replace(getUrlPrefix2(config), "");
|
|
1832
2294
|
await client.send(
|
|
1833
2295
|
new DeleteObjectCommand({
|
|
1834
2296
|
Bucket: config.bucket,
|
|
@@ -1837,7 +2299,7 @@ function createS3Storage(config) {
|
|
|
1837
2299
|
);
|
|
1838
2300
|
},
|
|
1839
2301
|
async rename(oldUrl, newKey) {
|
|
1840
|
-
const oldKey = oldUrl.replace(
|
|
2302
|
+
const oldKey = oldUrl.replace(getUrlPrefix2(config), "");
|
|
1841
2303
|
const newKeyWithPrefix = config.prefix ? `${config.prefix}/${newKey}` : newKey;
|
|
1842
2304
|
await client.send(
|
|
1843
2305
|
new CopyObjectCommand({
|
|
@@ -1852,7 +2314,7 @@ function createS3Storage(config) {
|
|
|
1852
2314
|
Key: oldKey
|
|
1853
2315
|
})
|
|
1854
2316
|
);
|
|
1855
|
-
return
|
|
2317
|
+
return getUrl2(newKeyWithPrefix);
|
|
1856
2318
|
},
|
|
1857
2319
|
getImageUrl(url, transforms) {
|
|
1858
2320
|
if (!transforms || Object.keys(transforms).length === 0) return url;
|
|
@@ -1880,14 +2342,14 @@ function createS3Storage(config) {
|
|
|
1880
2342
|
originalName: item.Key?.split("/").pop() || "",
|
|
1881
2343
|
mimeType: "application/octet-stream",
|
|
1882
2344
|
size: item.Size || 0,
|
|
1883
|
-
url:
|
|
2345
|
+
url: getUrl2(item.Key || ""),
|
|
1884
2346
|
provider: config.provider,
|
|
1885
2347
|
createdAt: item.LastModified?.toISOString() || (/* @__PURE__ */ new Date()).toISOString()
|
|
1886
2348
|
}));
|
|
1887
2349
|
},
|
|
1888
2350
|
async exists(url) {
|
|
1889
2351
|
try {
|
|
1890
|
-
const key = url.replace(
|
|
2352
|
+
const key = url.replace(getUrlPrefix2(config), "");
|
|
1891
2353
|
await client.send(
|
|
1892
2354
|
new HeadObjectCommand({
|
|
1893
2355
|
Bucket: config.bucket,
|
|
@@ -1947,9 +2409,9 @@ function createS3Storage(config) {
|
|
|
1947
2409
|
function createCloudinaryStorage(config) {
|
|
1948
2410
|
const getBaseUrl = () => `https://api.cloudinary.com/v1_1/${config.cloudName}/upload`;
|
|
1949
2411
|
const generateSignature = async (params) => {
|
|
1950
|
-
const
|
|
2412
|
+
const crypto4 = await import('crypto');
|
|
1951
2413
|
const sortedParams = Object.keys(params).sort().map((key) => `${key}=${params[key]}`).join("&");
|
|
1952
|
-
return
|
|
2414
|
+
return crypto4.createHash("sha256").update(sortedParams + config.apiSecret).digest("hex");
|
|
1953
2415
|
};
|
|
1954
2416
|
return {
|
|
1955
2417
|
name: "cloudinary",
|
|
@@ -2051,8 +2513,8 @@ function createCloudinaryStorage(config) {
|
|
|
2051
2513
|
public_id: publicId
|
|
2052
2514
|
};
|
|
2053
2515
|
const sortedParams = Object.keys(signatureParams).sort().map((key) => `${key}=${signatureParams[key]}`).join("&");
|
|
2054
|
-
const
|
|
2055
|
-
const signature =
|
|
2516
|
+
const crypto4 = await import('crypto');
|
|
2517
|
+
const signature = crypto4.createHash("sha256").update(sortedParams + config.apiSecret).digest("hex");
|
|
2056
2518
|
const deleteUrl = `https://api.cloudinary.com/v1_1/${config.cloudName}/image/destroy`;
|
|
2057
2519
|
const formData = new FormData();
|
|
2058
2520
|
formData.append("public_id", publicId);
|
|
@@ -2103,8 +2565,8 @@ function createCloudinaryStorage(config) {
|
|
|
2103
2565
|
timestamp: String(timestamp)
|
|
2104
2566
|
};
|
|
2105
2567
|
const sortedParams = Object.keys(signatureParams).sort().map((key) => `${key}=${signatureParams[key]}`).join("&");
|
|
2106
|
-
const
|
|
2107
|
-
const signature =
|
|
2568
|
+
const crypto4 = await import('crypto');
|
|
2569
|
+
const signature = crypto4.createHash("sha256").update(sortedParams + config.apiSecret).digest("hex");
|
|
2108
2570
|
const formData = new FormData();
|
|
2109
2571
|
formData.append("from_public_id", publicIdWithoutVersion);
|
|
2110
2572
|
formData.append("to_public_id", newPublicId);
|
|
@@ -2122,140 +2584,38 @@ function createCloudinaryStorage(config) {
|
|
|
2122
2584
|
const data = await response.json();
|
|
2123
2585
|
console.log("[Cloudinary rename] Success:", data.secure_url);
|
|
2124
2586
|
return data.secure_url;
|
|
2125
|
-
} else {
|
|
2126
|
-
const error = await response.json();
|
|
2127
|
-
console.warn("[Cloudinary rename] Failed:", error);
|
|
2128
|
-
const versionStr = version ? `/${version}/` : "/";
|
|
2129
|
-
return `https://res.cloudinary.com/${config.cloudName}/image/upload${versionStr}${newPublicId}.${newKey.split(".").pop()}`;
|
|
2130
|
-
}
|
|
2131
|
-
} catch (e) {
|
|
2132
|
-
console.warn("[Cloudinary rename] Error:", e.message);
|
|
2133
|
-
const versionStr = version ? `/${version}/` : "/";
|
|
2134
|
-
return `https://res.cloudinary.com/${config.cloudName}/image/upload${versionStr}${newPublicId}.${newKey.split(".").pop()}`;
|
|
2135
|
-
}
|
|
2136
|
-
},
|
|
2137
|
-
getImageUrl(url, transforms) {
|
|
2138
|
-
if (!transforms) return url;
|
|
2139
|
-
const parts = url.split("/upload/");
|
|
2140
|
-
if (parts.length !== 2) return url;
|
|
2141
|
-
const transformationArr = [];
|
|
2142
|
-
if (transforms.width) transformationArr.push(`w_${transforms.width}`);
|
|
2143
|
-
if (transforms.height) transformationArr.push(`h_${transforms.height}`);
|
|
2144
|
-
if (transforms.fit) {
|
|
2145
|
-
const fitMap = {
|
|
2146
|
-
crop: "c_fill",
|
|
2147
|
-
clip: "c_fit",
|
|
2148
|
-
scale: "c_scale",
|
|
2149
|
-
fill: "c_fill"
|
|
2150
|
-
};
|
|
2151
|
-
transformationArr.push(fitMap[transforms.fit] || "c_limit");
|
|
2152
|
-
}
|
|
2153
|
-
if (transforms.quality) transformationArr.push(`q_${transforms.quality}`);
|
|
2154
|
-
if (transforms.format) transformationArr.push(`f_${transforms.format}`);
|
|
2155
|
-
const transformationStr = transformationArr.join(",");
|
|
2156
|
-
return `${parts[0]}/upload/${transformationStr}/${parts[1]}`;
|
|
2157
|
-
},
|
|
2158
|
-
async generateThumbnail(file) {
|
|
2159
|
-
return this.getImageUrl(file.url, {
|
|
2160
|
-
width: 200,
|
|
2161
|
-
height: 200,
|
|
2162
|
-
fit: "crop"
|
|
2163
|
-
});
|
|
2164
|
-
},
|
|
2165
|
-
async list() {
|
|
2166
|
-
return [];
|
|
2167
|
-
},
|
|
2168
|
-
async exists(url) {
|
|
2169
|
-
const response = await fetch(url, { method: "HEAD" });
|
|
2170
|
-
return response.ok;
|
|
2171
|
-
},
|
|
2172
|
-
async createFolder() {
|
|
2173
|
-
},
|
|
2174
|
-
async deleteFolder() {
|
|
2175
|
-
}
|
|
2176
|
-
};
|
|
2177
|
-
}
|
|
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}`);
|
|
2587
|
+
} else {
|
|
2588
|
+
const error = await response.json();
|
|
2589
|
+
console.warn("[Cloudinary rename] Failed:", error);
|
|
2590
|
+
const versionStr = version ? `/${version}/` : "/";
|
|
2591
|
+
return `https://res.cloudinary.com/${config.cloudName}/image/upload${versionStr}${newPublicId}.${newKey.split(".").pop()}`;
|
|
2592
|
+
}
|
|
2593
|
+
} catch (e) {
|
|
2594
|
+
console.warn("[Cloudinary rename] Error:", e.message);
|
|
2595
|
+
const versionStr = version ? `/${version}/` : "/";
|
|
2596
|
+
return `https://res.cloudinary.com/${config.cloudName}/image/upload${versionStr}${newPublicId}.${newKey.split(".").pop()}`;
|
|
2210
2597
|
}
|
|
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
2598
|
},
|
|
2234
2599
|
getImageUrl(url, transforms) {
|
|
2235
|
-
|
|
2236
|
-
const
|
|
2237
|
-
if (
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
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);
|
|
2600
|
+
if (!transforms) return url;
|
|
2601
|
+
const parts = url.split("/upload/");
|
|
2602
|
+
if (parts.length !== 2) return url;
|
|
2603
|
+
const transformationArr = [];
|
|
2604
|
+
if (transforms.width) transformationArr.push(`w_${transforms.width}`);
|
|
2605
|
+
if (transforms.height) transformationArr.push(`h_${transforms.height}`);
|
|
2606
|
+
if (transforms.fit) {
|
|
2607
|
+
const fitMap = {
|
|
2608
|
+
crop: "c_fill",
|
|
2609
|
+
clip: "c_fit",
|
|
2610
|
+
scale: "c_scale",
|
|
2611
|
+
fill: "c_fill"
|
|
2612
|
+
};
|
|
2613
|
+
transformationArr.push(fitMap[transforms.fit] || "c_limit");
|
|
2257
2614
|
}
|
|
2258
|
-
|
|
2615
|
+
if (transforms.quality) transformationArr.push(`q_${transforms.quality}`);
|
|
2616
|
+
if (transforms.format) transformationArr.push(`f_${transforms.format}`);
|
|
2617
|
+
const transformationStr = transformationArr.join(",");
|
|
2618
|
+
return `${parts[0]}/upload/${transformationStr}/${parts[1]}`;
|
|
2259
2619
|
},
|
|
2260
2620
|
async generateThumbnail(file) {
|
|
2261
2621
|
return this.getImageUrl(file.url, {
|
|
@@ -2268,12 +2628,8 @@ function createImgixStorage(config) {
|
|
|
2268
2628
|
return [];
|
|
2269
2629
|
},
|
|
2270
2630
|
async exists(url) {
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
return response.ok;
|
|
2274
|
-
} catch {
|
|
2275
|
-
return false;
|
|
2276
|
-
}
|
|
2631
|
+
const response = await fetch(url, { method: "HEAD" });
|
|
2632
|
+
return response.ok;
|
|
2277
2633
|
},
|
|
2278
2634
|
async createFolder() {
|
|
2279
2635
|
},
|
|
@@ -2298,15 +2654,15 @@ function createFtpStorage(config) {
|
|
|
2298
2654
|
}
|
|
2299
2655
|
return client;
|
|
2300
2656
|
}
|
|
2301
|
-
const getKey = (
|
|
2657
|
+
const getKey = (path3) => {
|
|
2302
2658
|
const prefix = config.prefix ? `${config.prefix}/` : "";
|
|
2303
|
-
return `${prefix}${
|
|
2659
|
+
return `${prefix}${path3}`.replace(/\/+/g, "/");
|
|
2304
2660
|
};
|
|
2305
|
-
const
|
|
2661
|
+
const getUrl2 = (key) => {
|
|
2306
2662
|
const base = config.baseUrl.replace(/\/$/, "");
|
|
2307
2663
|
return `${base}/${key}`;
|
|
2308
2664
|
};
|
|
2309
|
-
const
|
|
2665
|
+
const getUrlPrefix3 = () => {
|
|
2310
2666
|
const base = config.baseUrl.replace(/\/$/, "");
|
|
2311
2667
|
return base + "/";
|
|
2312
2668
|
};
|
|
@@ -2337,8 +2693,8 @@ function createFtpStorage(config) {
|
|
|
2337
2693
|
originalName: file.name,
|
|
2338
2694
|
mimeType: file.type,
|
|
2339
2695
|
size: buffer.length,
|
|
2340
|
-
url:
|
|
2341
|
-
thumbnailUrl: file.type.startsWith("image/") ?
|
|
2696
|
+
url: getUrl2(key),
|
|
2697
|
+
thumbnailUrl: file.type.startsWith("image/") ? getUrl2(key) : void 0,
|
|
2342
2698
|
folder: options?.folder,
|
|
2343
2699
|
provider: config.type,
|
|
2344
2700
|
metadata: options?.metadata,
|
|
@@ -2357,15 +2713,15 @@ function createFtpStorage(config) {
|
|
|
2357
2713
|
},
|
|
2358
2714
|
async delete(url) {
|
|
2359
2715
|
const ftp = await getClient();
|
|
2360
|
-
const key = url.replace(
|
|
2716
|
+
const key = url.replace(getUrlPrefix3(), "");
|
|
2361
2717
|
await ftp.remove(key);
|
|
2362
2718
|
},
|
|
2363
2719
|
async rename(oldUrl, newKey) {
|
|
2364
2720
|
const ftp = await getClient();
|
|
2365
|
-
const oldKey = oldUrl.replace(
|
|
2721
|
+
const oldKey = oldUrl.replace(getUrlPrefix3(), "");
|
|
2366
2722
|
const fullPath = config.prefix ? `${config.prefix}/${newKey}` : newKey;
|
|
2367
2723
|
await ftp.rename(oldKey, fullPath);
|
|
2368
|
-
return
|
|
2724
|
+
return getUrl2(fullPath);
|
|
2369
2725
|
},
|
|
2370
2726
|
getImageUrl(url, transforms) {
|
|
2371
2727
|
if (!transforms || Object.keys(transforms).length === 0) return url;
|
|
@@ -2394,14 +2750,14 @@ function createFtpStorage(config) {
|
|
|
2394
2750
|
originalName: item.name,
|
|
2395
2751
|
mimeType: "application/octet-stream",
|
|
2396
2752
|
size: Number(item.size) || 0,
|
|
2397
|
-
url:
|
|
2753
|
+
url: getUrl2(`${key}/${item.name}`.replace(/\/+/g, "/")),
|
|
2398
2754
|
provider: config.type,
|
|
2399
2755
|
createdAt: item.modifiedAt ? item.modifiedAt.toISOString() : (/* @__PURE__ */ new Date()).toISOString()
|
|
2400
2756
|
}));
|
|
2401
2757
|
},
|
|
2402
2758
|
async exists(url) {
|
|
2403
2759
|
const ftp = await getClient();
|
|
2404
|
-
const key = url.replace(
|
|
2760
|
+
const key = url.replace(getUrlPrefix3(), "");
|
|
2405
2761
|
try {
|
|
2406
2762
|
await ftp.size(key);
|
|
2407
2763
|
return true;
|
|
@@ -2425,105 +2781,17 @@ function createFtpStorage(config) {
|
|
|
2425
2781
|
// src/storage/index.ts
|
|
2426
2782
|
async function resolveProvider(configService) {
|
|
2427
2783
|
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
|
-
});
|
|
2784
|
+
const registry = getDefaultRegistry();
|
|
2785
|
+
try {
|
|
2786
|
+
return await registry.resolve(config.type, config);
|
|
2787
|
+
} catch (err) {
|
|
2788
|
+
console.warn(
|
|
2789
|
+
`[resolveProvider] ${err.message} \u2014 falling back to local storage`
|
|
2790
|
+
);
|
|
2791
|
+
return createLocalStorage({
|
|
2792
|
+
uploadDir: path.join(process.cwd(), "public", "uploads"),
|
|
2793
|
+
baseUrl: "/uploads"
|
|
2794
|
+
});
|
|
2527
2795
|
}
|
|
2528
2796
|
}
|
|
2529
2797
|
async function resolveProviderWithConfig(config) {
|
|
@@ -2534,121 +2802,18 @@ async function resolveProviderWithConfig(config) {
|
|
|
2534
2802
|
baseUrl: "/uploads"
|
|
2535
2803
|
});
|
|
2536
2804
|
}
|
|
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
|
-
}
|
|
2805
|
+
const type = config.type || "local";
|
|
2806
|
+
const registry = getDefaultRegistry();
|
|
2807
|
+
try {
|
|
2808
|
+
return await registry.resolveWithConfig(type, config);
|
|
2809
|
+
} catch (err) {
|
|
2810
|
+
console.warn(
|
|
2811
|
+
`[resolveProviderWithConfig] ${err.message} \u2014 falling back to local storage`
|
|
2812
|
+
);
|
|
2813
|
+
return createLocalStorage({
|
|
2814
|
+
uploadDir: path.join(process.cwd(), "public", "uploads"),
|
|
2815
|
+
baseUrl: "/uploads"
|
|
2816
|
+
});
|
|
2652
2817
|
}
|
|
2653
2818
|
}
|
|
2654
2819
|
async function processImage(buffer) {
|
|
@@ -2685,9 +2850,7 @@ var MediaService = class _MediaService {
|
|
|
2685
2850
|
storage2 = await resolveProviderWithConfig(options.storageConfig);
|
|
2686
2851
|
} else {
|
|
2687
2852
|
const configService = new ConfigService(db);
|
|
2688
|
-
|
|
2689
|
-
await configService.load();
|
|
2690
|
-
}
|
|
2853
|
+
await configService.load();
|
|
2691
2854
|
storage2 = await resolveProvider(configService);
|
|
2692
2855
|
}
|
|
2693
2856
|
const service = new _MediaService(db, storage2, options);
|
|
@@ -2889,7 +3052,7 @@ var MediaService = class _MediaService {
|
|
|
2889
3052
|
]
|
|
2890
3053
|
);
|
|
2891
3054
|
} else {
|
|
2892
|
-
const { media: mediaSchema } = await import('./media-
|
|
3055
|
+
const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
|
|
2893
3056
|
const mime = storageResult.mimeType;
|
|
2894
3057
|
const mediaType = mime.startsWith("image/") ? "image" : mime.startsWith("video/") ? "video" : mime.startsWith("audio/") ? "audio" : mime.startsWith("application/pdf") ? "document" : ["application/zip", "application/x-zip", "application/x-tar", "application/gzip", "application/x-7z"].includes(mime) ? "archive" : "other";
|
|
2895
3058
|
await this.db.insert(mediaSchema).values({
|
|
@@ -2951,7 +3114,7 @@ var MediaService = class _MediaService {
|
|
|
2951
3114
|
id
|
|
2952
3115
|
]);
|
|
2953
3116
|
} else {
|
|
2954
|
-
const { media: mediaSchema } = await import('./media-
|
|
3117
|
+
const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
|
|
2955
3118
|
const { eq } = await import('drizzle-orm');
|
|
2956
3119
|
const [row] = await this.db.select().from(mediaSchema).where(eq(mediaSchema.id, id));
|
|
2957
3120
|
if (row) item = this.rowToMedia(row);
|
|
@@ -2967,7 +3130,7 @@ var MediaService = class _MediaService {
|
|
|
2967
3130
|
if (this.dialect === "sqlite") {
|
|
2968
3131
|
await this.sqliteRun(`DELETE FROM ${this.mediaTable} WHERE id = ?`, [id]);
|
|
2969
3132
|
} else {
|
|
2970
|
-
const { media: mediaSchema } = await import('./media-
|
|
3133
|
+
const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
|
|
2971
3134
|
const { eq } = await import('drizzle-orm');
|
|
2972
3135
|
await this.db.delete(mediaSchema).where(eq(mediaSchema.id, id));
|
|
2973
3136
|
}
|
|
@@ -2982,7 +3145,7 @@ var MediaService = class _MediaService {
|
|
|
2982
3145
|
id
|
|
2983
3146
|
]);
|
|
2984
3147
|
} else {
|
|
2985
|
-
const { media: mediaSchema } = await import('./media-
|
|
3148
|
+
const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
|
|
2986
3149
|
const { eq } = await import('drizzle-orm');
|
|
2987
3150
|
const [row] = await this.db.select().from(mediaSchema).where(eq(mediaSchema.id, id));
|
|
2988
3151
|
if (row) item = this.rowToMedia(row);
|
|
@@ -3017,7 +3180,7 @@ var MediaService = class _MediaService {
|
|
|
3017
3180
|
[...vals, this.now(), id]
|
|
3018
3181
|
);
|
|
3019
3182
|
} else {
|
|
3020
|
-
const { media: mediaSchema } = await import('./media-
|
|
3183
|
+
const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
|
|
3021
3184
|
const { eq } = await import('drizzle-orm');
|
|
3022
3185
|
await this.db.update(mediaSchema).set({ ...updateData, updatedAt: this.now() }).where(eq(mediaSchema.id, id));
|
|
3023
3186
|
}
|
|
@@ -3036,7 +3199,7 @@ var MediaService = class _MediaService {
|
|
|
3036
3199
|
);
|
|
3037
3200
|
return row2 ? this.rowToMedia(row2) : null;
|
|
3038
3201
|
}
|
|
3039
|
-
const { media: mediaSchema } = await import('./media-
|
|
3202
|
+
const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
|
|
3040
3203
|
const { eq } = await import('drizzle-orm');
|
|
3041
3204
|
const [row] = await this.db.select().from(mediaSchema).where(eq(mediaSchema.id, id));
|
|
3042
3205
|
return row ? this.rowToMedia(row) : null;
|
|
@@ -3084,7 +3247,7 @@ var MediaService = class _MediaService {
|
|
|
3084
3247
|
totalPages: Math.ceil(totalDocs2 / limit)
|
|
3085
3248
|
};
|
|
3086
3249
|
}
|
|
3087
|
-
const { media: mediaSchema } = await import('./media-
|
|
3250
|
+
const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
|
|
3088
3251
|
const { like, or, and, asc, desc, eq, sql } = await import('drizzle-orm');
|
|
3089
3252
|
const conditions = [];
|
|
3090
3253
|
if (search) {
|
|
@@ -3163,7 +3326,7 @@ var MediaService = class _MediaService {
|
|
|
3163
3326
|
);
|
|
3164
3327
|
return row ? this.rowToMedia(row) : null;
|
|
3165
3328
|
}
|
|
3166
|
-
const { media: mediaSchema } = await import('./media-
|
|
3329
|
+
const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
|
|
3167
3330
|
const { eq } = await import('drizzle-orm');
|
|
3168
3331
|
const [updated] = await this.db.update(mediaSchema).set({ ...data, updatedAt: /* @__PURE__ */ new Date() }).where(eq(mediaSchema.id, id)).returning();
|
|
3169
3332
|
return updated ? this.rowToMedia(updated) : null;
|
|
@@ -3185,7 +3348,7 @@ var MediaService = class _MediaService {
|
|
|
3185
3348
|
);
|
|
3186
3349
|
}
|
|
3187
3350
|
} else {
|
|
3188
|
-
const { media: mediaSchema } = await import('./media-
|
|
3351
|
+
const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
|
|
3189
3352
|
const { eq } = await import('drizzle-orm');
|
|
3190
3353
|
for (const id of ids) {
|
|
3191
3354
|
await this.db.update(mediaSchema).set({ ...data, updatedAt: /* @__PURE__ */ new Date() }).where(eq(mediaSchema.id, id));
|
|
@@ -3200,7 +3363,7 @@ var MediaService = class _MediaService {
|
|
|
3200
3363
|
);
|
|
3201
3364
|
return rows.map((r) => r.path).filter((f) => f && f !== "").sort();
|
|
3202
3365
|
}
|
|
3203
|
-
const { media: mediaSchema, mediaFolders: folderSchema } = await import('./media-
|
|
3366
|
+
const { media: mediaSchema, mediaFolders: folderSchema } = await import('./media-7WDX4BDJ.js');
|
|
3204
3367
|
const { eq, sql } = await import('drizzle-orm');
|
|
3205
3368
|
const fromMedia = await this.db.select({ folder: mediaSchema.folder }).from(mediaSchema).groupBy(mediaSchema.folder);
|
|
3206
3369
|
const fromFolders = await this.db.select({ path: folderSchema.path }).from(folderSchema);
|
|
@@ -3220,7 +3383,7 @@ var MediaService = class _MediaService {
|
|
|
3220
3383
|
[fullPath, name, parentPath || null, now]
|
|
3221
3384
|
);
|
|
3222
3385
|
} else {
|
|
3223
|
-
const { mediaFolders: folderSchema } = await import('./media-
|
|
3386
|
+
const { mediaFolders: folderSchema } = await import('./media-7WDX4BDJ.js');
|
|
3224
3387
|
await this.db.insert(folderSchema).values({
|
|
3225
3388
|
path: fullPath,
|
|
3226
3389
|
name,
|
|
@@ -3241,7 +3404,7 @@ var MediaService = class _MediaService {
|
|
|
3241
3404
|
[folder, `${folder}/%`]
|
|
3242
3405
|
);
|
|
3243
3406
|
} else {
|
|
3244
|
-
const { mediaFolders: folderSchema } = await import('./media-
|
|
3407
|
+
const { mediaFolders: folderSchema } = await import('./media-7WDX4BDJ.js');
|
|
3245
3408
|
const { like, or, eq } = await import('drizzle-orm');
|
|
3246
3409
|
await this.db.delete(folderSchema).where(
|
|
3247
3410
|
or(
|
|
@@ -3252,8 +3415,75 @@ var MediaService = class _MediaService {
|
|
|
3252
3415
|
}
|
|
3253
3416
|
}
|
|
3254
3417
|
};
|
|
3255
|
-
|
|
3256
|
-
|
|
3418
|
+
function formatZodErrors(errors) {
|
|
3419
|
+
return errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
|
|
3420
|
+
}
|
|
3421
|
+
function normalizeEmptyStrings(data, fields) {
|
|
3422
|
+
if (!data || typeof data !== "object") return;
|
|
3423
|
+
for (const field of fields) {
|
|
3424
|
+
if (!field.name || !(field.name in data)) continue;
|
|
3425
|
+
const val = data[field.name];
|
|
3426
|
+
if (val === "") {
|
|
3427
|
+
const isTextual = field.type === "text" || field.type === "textarea" || field.type === "code" || field.type === "markdown" || field.type === "email" || field.type === "password" || field.type === "color";
|
|
3428
|
+
if (!isTextual) data[field.name] = null;
|
|
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)) normalizeEmptyStrings(data[field.name], tab.fields);
|
|
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
|
+
normalizeEmptyStrings(data[field.name], field.fields);
|
|
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") normalizeEmptyStrings(item, field.fields);
|
|
3439
|
+
}
|
|
3440
|
+
} else if (field.type === "blocks" && field.name && Array.isArray(field.blocks) && Array.isArray(data[field.name])) {
|
|
3441
|
+
for (const item of data[field.name]) {
|
|
3442
|
+
if (!item || typeof item !== "object") continue;
|
|
3443
|
+
const blockTypeStr = item.type || item.blockType;
|
|
3444
|
+
if (!blockTypeStr) continue;
|
|
3445
|
+
const blockDef = field.blocks.find((b) => b.slug === blockTypeStr);
|
|
3446
|
+
if (!blockDef || !Array.isArray(blockDef.fields)) continue;
|
|
3447
|
+
const target = item.data && typeof item.data === "object" ? item.data : item;
|
|
3448
|
+
normalizeEmptyStrings(target, blockDef.fields);
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
function convertRichtextFields(fields, data) {
|
|
3454
|
+
if (!data || typeof data !== "object") return;
|
|
3455
|
+
for (const field of fields) {
|
|
3456
|
+
if (field.type === "richtext" && field.name) {
|
|
3457
|
+
const val = data[field.name];
|
|
3458
|
+
if (typeof val === "string") {
|
|
3459
|
+
data[field.name] = [{ type: "paragraph", children: [{ text: val }] }];
|
|
3460
|
+
} else if (val && typeof val === "object" && !Array.isArray(val) && val.type === "doc" && Array.isArray(val.content)) {
|
|
3461
|
+
data[field.name] = val.content;
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
if (field.type === "tabs" && field.name && Array.isArray(field.tabs) && data[field.name] && typeof data[field.name] === "object") {
|
|
3465
|
+
for (const tab of field.tabs) {
|
|
3466
|
+
if (Array.isArray(tab.fields)) convertRichtextFields(tab.fields, data[field.name]);
|
|
3467
|
+
}
|
|
3468
|
+
} else if ((field.type === "group" || field.type === "collapsible") && field.name && Array.isArray(field.fields) && data[field.name] && typeof data[field.name] === "object") {
|
|
3469
|
+
convertRichtextFields(field.fields, data[field.name]);
|
|
3470
|
+
} else if (field.type === "array" && field.name && Array.isArray(field.fields) && Array.isArray(data[field.name])) {
|
|
3471
|
+
for (const item of data[field.name]) {
|
|
3472
|
+
if (item && typeof item === "object") convertRichtextFields(field.fields, item);
|
|
3473
|
+
}
|
|
3474
|
+
} else if (field.type === "blocks" && field.name && Array.isArray(field.blocks) && Array.isArray(data[field.name])) {
|
|
3475
|
+
for (const item of data[field.name]) {
|
|
3476
|
+
if (!item || typeof item !== "object") continue;
|
|
3477
|
+
const blockTypeStr = item.type || item.blockType;
|
|
3478
|
+
if (!blockTypeStr) continue;
|
|
3479
|
+
const blockDef = field.blocks.find((b) => b.slug === blockTypeStr);
|
|
3480
|
+
if (!blockDef || !Array.isArray(blockDef.fields)) continue;
|
|
3481
|
+
const target = item.data && typeof item.data === "object" ? item.data : item;
|
|
3482
|
+
convertRichtextFields(blockDef.fields, target);
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3257
3487
|
var COLLECTION_EVENT_MAP = {
|
|
3258
3488
|
_media: {
|
|
3259
3489
|
create: WEBHOOK_EVENTS.MEDIA_UPLOAD,
|
|
@@ -3347,7 +3577,7 @@ async function checkCollectionAccess(collection, operation, req, ctxUser, ctxTen
|
|
|
3347
3577
|
};
|
|
3348
3578
|
}
|
|
3349
3579
|
}
|
|
3350
|
-
if (ctxUser) {
|
|
3580
|
+
if (ctxUser && !(apiKeyContext?.permissions?.length > 0)) {
|
|
3351
3581
|
const resource = collection.slug;
|
|
3352
3582
|
const action = operation === "read" ? "read" : operation === "create" ? "create" : operation === "update" ? "update" : "delete";
|
|
3353
3583
|
const permission = `${resource}:${action}`;
|
|
@@ -3406,31 +3636,30 @@ async function checkGlobalAccess(global, operation, req, ctxUser, ctxTenantID, e
|
|
|
3406
3636
|
return { allowed: true };
|
|
3407
3637
|
}
|
|
3408
3638
|
async function resolveAuthContext(req, authMw, staticUser, staticTenantID) {
|
|
3409
|
-
if (
|
|
3410
|
-
return {
|
|
3411
|
-
user: staticUser,
|
|
3412
|
-
tenantID: staticTenantID,
|
|
3413
|
-
apiKeyContext: void 0
|
|
3414
|
-
};
|
|
3415
|
-
}
|
|
3416
|
-
let result;
|
|
3417
|
-
try {
|
|
3418
|
-
result = await authMw(req);
|
|
3419
|
-
} catch (err) {
|
|
3639
|
+
if (staticUser) {
|
|
3420
3640
|
return {
|
|
3421
3641
|
user: staticUser,
|
|
3422
3642
|
tenantID: staticTenantID,
|
|
3423
|
-
apiKeyContext: void 0
|
|
3643
|
+
apiKeyContext: void 0,
|
|
3644
|
+
authType: "static"
|
|
3424
3645
|
};
|
|
3425
3646
|
}
|
|
3426
|
-
if (
|
|
3427
|
-
|
|
3647
|
+
if (authMw) {
|
|
3648
|
+
const res = await authMw(req);
|
|
3649
|
+
if (res.status === 200 && res.user) {
|
|
3650
|
+
return {
|
|
3651
|
+
user: res.user,
|
|
3652
|
+
tenantID: res.tenantContext?.tenantId,
|
|
3653
|
+
apiKeyContext: res.apiKeyContext,
|
|
3654
|
+
authType: res.authType
|
|
3655
|
+
};
|
|
3656
|
+
}
|
|
3428
3657
|
}
|
|
3429
3658
|
return {
|
|
3430
|
-
user:
|
|
3431
|
-
tenantID:
|
|
3432
|
-
apiKeyContext:
|
|
3433
|
-
authType:
|
|
3659
|
+
user: void 0,
|
|
3660
|
+
tenantID: void 0,
|
|
3661
|
+
apiKeyContext: void 0,
|
|
3662
|
+
authType: void 0
|
|
3434
3663
|
};
|
|
3435
3664
|
}
|
|
3436
3665
|
function createDefaultAuthAdapter(db, rootDir) {
|
|
@@ -3544,6 +3773,13 @@ function createHonoApp(options) {
|
|
|
3544
3773
|
baseUrl: process.env.KYRO_BASE_URL || "http://localhost:4321",
|
|
3545
3774
|
rateLimiter
|
|
3546
3775
|
});
|
|
3776
|
+
EmailTransport.fromConfig(db).then((transport) => {
|
|
3777
|
+
if (transport) {
|
|
3778
|
+
authRoutes.email = transport;
|
|
3779
|
+
}
|
|
3780
|
+
}).catch((err) => {
|
|
3781
|
+
console.error("[Email] Failed to initialize transport from config:", err);
|
|
3782
|
+
});
|
|
3547
3783
|
app.post("/api/auth/login", async (c) => authRoutes.login(c.req.raw));
|
|
3548
3784
|
app.post("/api/auth/register", async (c) => authRoutes.register(c.req.raw));
|
|
3549
3785
|
app.post("/api/auth/logout", async (c) => authRoutes.logout(c.req.raw));
|
|
@@ -3554,69 +3790,6 @@ function createHonoApp(options) {
|
|
|
3554
3790
|
app.delete("/api/auth/sessions", async (c) => authRoutes.revokeOtherSessions(c.req.raw));
|
|
3555
3791
|
app.delete("/api/auth/sessions/:id", async (c) => authRoutes.revokeSession(c.req.raw, c.req.param("id")));
|
|
3556
3792
|
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
|
-
});
|
|
3620
3793
|
app.get("/api/auth/access", async (c) => {
|
|
3621
3794
|
try {
|
|
3622
3795
|
const { user: ctxUser, tenantID: ctxTenantID } = await resolveAuthContext(
|
|
@@ -3701,7 +3874,13 @@ function createHonoApp(options) {
|
|
|
3701
3874
|
return c.json({ error: error.message }, 500);
|
|
3702
3875
|
}
|
|
3703
3876
|
});
|
|
3704
|
-
const
|
|
3877
|
+
const usersCollection2 = typeof registry.hasCollection === "function" && registry.hasCollection("users") ? registry.getCollection("users") : (() => {
|
|
3878
|
+
try {
|
|
3879
|
+
return registry.getCollection("users");
|
|
3880
|
+
} catch {
|
|
3881
|
+
return usersCollection;
|
|
3882
|
+
}
|
|
3883
|
+
})();
|
|
3705
3884
|
app.get("/api/users", async (c) => {
|
|
3706
3885
|
try {
|
|
3707
3886
|
const { user: ctxUser, tenantID: ctxTenantID } = await resolveAuthContext(
|
|
@@ -3711,7 +3890,7 @@ function createHonoApp(options) {
|
|
|
3711
3890
|
tenantID
|
|
3712
3891
|
);
|
|
3713
3892
|
const access = await checkCollectionAccess(
|
|
3714
|
-
|
|
3893
|
+
usersCollection2,
|
|
3715
3894
|
"read",
|
|
3716
3895
|
c.req.raw,
|
|
3717
3896
|
ctxUser,
|
|
@@ -3756,19 +3935,6 @@ function createHonoApp(options) {
|
|
|
3756
3935
|
user,
|
|
3757
3936
|
tenantID
|
|
3758
3937
|
);
|
|
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
3938
|
const id = c.req.param("id");
|
|
3773
3939
|
const found = await sessionAuthAdapter.findUserById(id);
|
|
3774
3940
|
if (!found) {
|
|
@@ -3788,7 +3954,7 @@ function createHonoApp(options) {
|
|
|
3788
3954
|
tenantID
|
|
3789
3955
|
);
|
|
3790
3956
|
const access = await checkCollectionAccess(
|
|
3791
|
-
|
|
3957
|
+
usersCollection2,
|
|
3792
3958
|
"create",
|
|
3793
3959
|
c.req.raw,
|
|
3794
3960
|
ctxUser,
|
|
@@ -3813,8 +3979,18 @@ function createHonoApp(options) {
|
|
|
3813
3979
|
password: body.password,
|
|
3814
3980
|
name: body.name,
|
|
3815
3981
|
role: body.role || "customer",
|
|
3982
|
+
avatar: body.avatar,
|
|
3816
3983
|
tenantId: body.tenantId
|
|
3817
3984
|
});
|
|
3985
|
+
if (ctxUser) {
|
|
3986
|
+
sessionAuthAdapter?.createAuditLog({
|
|
3987
|
+
action: "user_create",
|
|
3988
|
+
userId: ctxUser.id,
|
|
3989
|
+
resource: "users",
|
|
3990
|
+
resourceId: created.id,
|
|
3991
|
+
success: true
|
|
3992
|
+
});
|
|
3993
|
+
}
|
|
3818
3994
|
return c.json(
|
|
3819
3995
|
{ data: created, message: "User created successfully" },
|
|
3820
3996
|
201
|
|
@@ -3832,7 +4008,7 @@ function createHonoApp(options) {
|
|
|
3832
4008
|
tenantID
|
|
3833
4009
|
);
|
|
3834
4010
|
const access = await checkCollectionAccess(
|
|
3835
|
-
|
|
4011
|
+
usersCollection2,
|
|
3836
4012
|
"update",
|
|
3837
4013
|
c.req.raw,
|
|
3838
4014
|
ctxUser,
|
|
@@ -3854,6 +4030,9 @@ function createHonoApp(options) {
|
|
|
3854
4030
|
if (body.name !== void 0) updateData.name = body.name;
|
|
3855
4031
|
if (body.email !== void 0) updateData.email = body.email;
|
|
3856
4032
|
if (body.role !== void 0) updateData.role = body.role;
|
|
4033
|
+
if (body.avatar !== void 0) {
|
|
4034
|
+
updateData.avatar = typeof body.avatar === "object" && body.avatar !== null ? body.avatar.id || String(body.avatar) : body.avatar;
|
|
4035
|
+
}
|
|
3857
4036
|
if (body.tenantId !== void 0) updateData.tenantId = body.tenantId;
|
|
3858
4037
|
if (body.emailVerified !== void 0)
|
|
3859
4038
|
updateData.emailVerified = body.emailVerified;
|
|
@@ -3865,6 +4044,25 @@ function createHonoApp(options) {
|
|
|
3865
4044
|
if (!updated) {
|
|
3866
4045
|
return c.json({ error: "User update failed" }, 500);
|
|
3867
4046
|
}
|
|
4047
|
+
if (ctxUser) {
|
|
4048
|
+
sessionAuthAdapter?.createAuditLog({
|
|
4049
|
+
action: "user_update",
|
|
4050
|
+
userId: ctxUser.id,
|
|
4051
|
+
resource: "users",
|
|
4052
|
+
resourceId: id,
|
|
4053
|
+
success: true
|
|
4054
|
+
});
|
|
4055
|
+
if (body.role && existing.role !== body.role) {
|
|
4056
|
+
sessionAuthAdapter?.createAuditLog({
|
|
4057
|
+
action: "role_change",
|
|
4058
|
+
userId: ctxUser.id,
|
|
4059
|
+
resource: "users",
|
|
4060
|
+
resourceId: id,
|
|
4061
|
+
success: true,
|
|
4062
|
+
metadata: { oldRole: existing.role, newRole: body.role }
|
|
4063
|
+
});
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
3868
4066
|
return c.json({ data: updated, message: "User updated successfully" });
|
|
3869
4067
|
} catch (error) {
|
|
3870
4068
|
return c.json({ error: error.message }, 500);
|
|
@@ -3879,7 +4077,7 @@ function createHonoApp(options) {
|
|
|
3879
4077
|
tenantID
|
|
3880
4078
|
);
|
|
3881
4079
|
const access = await checkCollectionAccess(
|
|
3882
|
-
|
|
4080
|
+
usersCollection2,
|
|
3883
4081
|
"delete",
|
|
3884
4082
|
c.req.raw,
|
|
3885
4083
|
ctxUser,
|
|
@@ -3903,6 +4101,15 @@ function createHonoApp(options) {
|
|
|
3903
4101
|
if (!deleted) {
|
|
3904
4102
|
return c.json({ error: "User deletion failed" }, 500);
|
|
3905
4103
|
}
|
|
4104
|
+
if (ctxUser) {
|
|
4105
|
+
sessionAuthAdapter?.createAuditLog({
|
|
4106
|
+
action: "user_delete",
|
|
4107
|
+
userId: ctxUser.id,
|
|
4108
|
+
resource: "users",
|
|
4109
|
+
resourceId: id,
|
|
4110
|
+
success: true
|
|
4111
|
+
});
|
|
4112
|
+
}
|
|
3906
4113
|
return c.json({ data: existing, message: "User deleted successfully" });
|
|
3907
4114
|
} catch (error) {
|
|
3908
4115
|
return c.json({ error: error.message }, 500);
|
|
@@ -4059,11 +4266,11 @@ function createHonoApp(options) {
|
|
|
4059
4266
|
const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
|
|
4060
4267
|
if (!ctxUser) return c.json({ error: "Authentication required" }, 401);
|
|
4061
4268
|
const service = await getMedia();
|
|
4062
|
-
const
|
|
4063
|
-
if (!
|
|
4269
|
+
const path3 = c.req.query("path");
|
|
4270
|
+
if (!path3) {
|
|
4064
4271
|
return c.json({ error: "Path is required" }, 400);
|
|
4065
4272
|
}
|
|
4066
|
-
await service.deleteFolder(
|
|
4273
|
+
await service.deleteFolder(path3);
|
|
4067
4274
|
return c.json({ message: "Folder deleted" });
|
|
4068
4275
|
} catch (error) {
|
|
4069
4276
|
console.error("[Media] delete folder error:", error);
|
|
@@ -4216,6 +4423,119 @@ function createHonoApp(options) {
|
|
|
4216
4423
|
return c.json({ error: error.message }, 500);
|
|
4217
4424
|
}
|
|
4218
4425
|
});
|
|
4426
|
+
app.get("/api/plugins", async (c) => {
|
|
4427
|
+
try {
|
|
4428
|
+
const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
|
|
4429
|
+
if (!ctxUser) return c.json({ error: "Authentication required" }, 401);
|
|
4430
|
+
const plugins = registry.getPlugins();
|
|
4431
|
+
const storageRegistry = registry.storageProviders;
|
|
4432
|
+
const pluginList = await Promise.all(
|
|
4433
|
+
plugins.map(async (p) => {
|
|
4434
|
+
let enabled = true;
|
|
4435
|
+
const pluginName = p.name;
|
|
4436
|
+
let states = {};
|
|
4437
|
+
try {
|
|
4438
|
+
const doc = await db.findOne({
|
|
4439
|
+
collection: "_globals_plugin-settings",
|
|
4440
|
+
where: {}
|
|
4441
|
+
});
|
|
4442
|
+
if (doc && doc.states) states = doc.states;
|
|
4443
|
+
} catch {
|
|
4444
|
+
}
|
|
4445
|
+
if (states[pluginName] !== void 0) {
|
|
4446
|
+
enabled = states[pluginName];
|
|
4447
|
+
}
|
|
4448
|
+
storageRegistry.setPluginEnabled(pluginName, enabled);
|
|
4449
|
+
return {
|
|
4450
|
+
id: pluginName,
|
|
4451
|
+
name: pluginName,
|
|
4452
|
+
version: p.version || "1.0.0",
|
|
4453
|
+
description: p.description || "",
|
|
4454
|
+
enabled,
|
|
4455
|
+
status: enabled ? "active" : "disabled"
|
|
4456
|
+
};
|
|
4457
|
+
})
|
|
4458
|
+
);
|
|
4459
|
+
return c.json(pluginList);
|
|
4460
|
+
} catch (error) {
|
|
4461
|
+
console.error("[Plugins] list error:", error);
|
|
4462
|
+
return c.json({ error: error.message }, 500);
|
|
4463
|
+
}
|
|
4464
|
+
});
|
|
4465
|
+
app.put("/api/plugins/:name/toggle", async (c) => {
|
|
4466
|
+
try {
|
|
4467
|
+
const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
|
|
4468
|
+
if (!ctxUser) return c.json({ error: "Authentication required" }, 401);
|
|
4469
|
+
const pluginName = c.req.param("name");
|
|
4470
|
+
const storageRegistry = registry.storageProviders;
|
|
4471
|
+
const currentEnabled = storageRegistry.isPluginEnabled(pluginName);
|
|
4472
|
+
const newEnabled = !currentEnabled;
|
|
4473
|
+
const affectedProviders = [];
|
|
4474
|
+
if (!newEnabled) {
|
|
4475
|
+
for (const p of storageRegistry.getAll()) {
|
|
4476
|
+
if (p.pluginName === pluginName) {
|
|
4477
|
+
affectedProviders.push(p.type);
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4481
|
+
const force = c.req.query("force") === "1";
|
|
4482
|
+
if (!force && !newEnabled && affectedProviders.length > 0) {
|
|
4483
|
+
let activeProvider = "local";
|
|
4484
|
+
try {
|
|
4485
|
+
const row = db?.prepare?.(`SELECT provider FROM "_globals_storage-settings" LIMIT 1`)?.get();
|
|
4486
|
+
if (row?.provider) activeProvider = row.provider;
|
|
4487
|
+
} catch {
|
|
4488
|
+
try {
|
|
4489
|
+
const result = await db?.findOne?.({
|
|
4490
|
+
collection: "_globals_storage-settings",
|
|
4491
|
+
where: {}
|
|
4492
|
+
});
|
|
4493
|
+
if (result?.provider) activeProvider = result.provider;
|
|
4494
|
+
} catch {
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
if (affectedProviders.includes(activeProvider)) {
|
|
4498
|
+
return c.json({
|
|
4499
|
+
error: `Cannot disable "${pluginName}" \u2014 storage provider "${activeProvider}" is currently active. Switch to Local storage first.`,
|
|
4500
|
+
requiresAction: true,
|
|
4501
|
+
activeProvider
|
|
4502
|
+
}, 409);
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
try {
|
|
4506
|
+
let states = {};
|
|
4507
|
+
let docId = "global";
|
|
4508
|
+
const doc = await db.findOne({
|
|
4509
|
+
collection: "_globals_plugin-settings",
|
|
4510
|
+
where: {}
|
|
4511
|
+
});
|
|
4512
|
+
if (doc) {
|
|
4513
|
+
states = doc.states || {};
|
|
4514
|
+
docId = doc.id;
|
|
4515
|
+
}
|
|
4516
|
+
states[pluginName] = newEnabled;
|
|
4517
|
+
if (doc) {
|
|
4518
|
+
await db.update({
|
|
4519
|
+
collection: "_globals_plugin-settings",
|
|
4520
|
+
id: docId,
|
|
4521
|
+
data: { states }
|
|
4522
|
+
});
|
|
4523
|
+
} else {
|
|
4524
|
+
await db.create({
|
|
4525
|
+
collection: "_globals_plugin-settings",
|
|
4526
|
+
data: { states, id: "global" }
|
|
4527
|
+
});
|
|
4528
|
+
}
|
|
4529
|
+
} catch (e) {
|
|
4530
|
+
console.warn(`[Plugins] Could not persist state for "${pluginName}":`, e);
|
|
4531
|
+
}
|
|
4532
|
+
storageRegistry.setPluginEnabled(pluginName, newEnabled);
|
|
4533
|
+
return c.json({ name: pluginName, enabled: newEnabled });
|
|
4534
|
+
} catch (error) {
|
|
4535
|
+
console.error("[Plugins] toggle error:", error);
|
|
4536
|
+
return c.json({ error: error.message }, 500);
|
|
4537
|
+
}
|
|
4538
|
+
});
|
|
4219
4539
|
app.get("/api/health", (c) => {
|
|
4220
4540
|
return c.json({
|
|
4221
4541
|
status: "ok",
|
|
@@ -4224,6 +4544,102 @@ function createHonoApp(options) {
|
|
|
4224
4544
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4225
4545
|
});
|
|
4226
4546
|
});
|
|
4547
|
+
app.get("/api/kyro/schema", async (c) => {
|
|
4548
|
+
const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
|
|
4549
|
+
if (!ctxUser) return c.json({ error: "Authentication required" }, 401);
|
|
4550
|
+
const extractFields = (fields) => fields.map((f) => {
|
|
4551
|
+
const meta = {
|
|
4552
|
+
name: f.name,
|
|
4553
|
+
type: f.type,
|
|
4554
|
+
label: f.label,
|
|
4555
|
+
required: f.required ?? false,
|
|
4556
|
+
unique: f.unique ?? false,
|
|
4557
|
+
indexed: f.indexed ?? false,
|
|
4558
|
+
defaultValue: f.defaultValue,
|
|
4559
|
+
admin: f.admin ? { ...f.admin } : void 0
|
|
4560
|
+
};
|
|
4561
|
+
if (f.minLength !== void 0) meta.minLength = f.minLength;
|
|
4562
|
+
if (f.maxLength !== void 0) meta.maxLength = f.maxLength;
|
|
4563
|
+
if (f.pattern !== void 0) meta.pattern = f.pattern;
|
|
4564
|
+
if (f.variant !== void 0) meta.variant = f.variant;
|
|
4565
|
+
if (f.hasMany !== void 0) meta.hasMany = f.hasMany;
|
|
4566
|
+
if (f.min !== void 0) meta.min = f.min;
|
|
4567
|
+
if (f.max !== void 0) meta.max = f.max;
|
|
4568
|
+
if (f.step !== void 0) meta.step = f.step;
|
|
4569
|
+
if (f.integer !== void 0) meta.integer = f.integer;
|
|
4570
|
+
if (f.options) meta.options = f.options;
|
|
4571
|
+
if (f.relationTo) meta.relationTo = f.relationTo;
|
|
4572
|
+
if (f.maxDepth !== void 0) meta.maxDepth = f.maxDepth;
|
|
4573
|
+
if (f.minRows !== void 0) meta.minRows = f.minRows;
|
|
4574
|
+
if (f.maxRows !== void 0) meta.maxRows = f.maxRows;
|
|
4575
|
+
if (f.localized !== void 0) meta.localized = f.localized;
|
|
4576
|
+
if (f.language) meta.language = f.language;
|
|
4577
|
+
if (f.format) meta.format = f.format;
|
|
4578
|
+
if (f.allowedTypes) meta.allowedTypes = f.allowedTypes;
|
|
4579
|
+
if (f.maxSize) meta.maxSize = f.maxSize;
|
|
4580
|
+
if (f.type === "group" || f.type === "row" || f.type === "collapsible" && f.fields) {
|
|
4581
|
+
meta.fields = extractFields(f.fields);
|
|
4582
|
+
}
|
|
4583
|
+
if (f.type === "array" && f.fields) {
|
|
4584
|
+
meta.fields = extractFields(f.fields);
|
|
4585
|
+
}
|
|
4586
|
+
if (f.type === "blocks" && f.blocks) {
|
|
4587
|
+
meta.blocks = f.blocks.map((b) => ({
|
|
4588
|
+
slug: b.slug,
|
|
4589
|
+
label: b.label,
|
|
4590
|
+
fields: extractFields(b.fields)
|
|
4591
|
+
}));
|
|
4592
|
+
}
|
|
4593
|
+
if (f.type === "tabs" && f.tabs) {
|
|
4594
|
+
meta.tabs = f.tabs.map((t) => ({
|
|
4595
|
+
label: t.label,
|
|
4596
|
+
name: t.name,
|
|
4597
|
+
fields: extractFields(t.fields)
|
|
4598
|
+
}));
|
|
4599
|
+
}
|
|
4600
|
+
return meta;
|
|
4601
|
+
});
|
|
4602
|
+
const data = { collections: {}, globals: {} };
|
|
4603
|
+
for (const col of registry.getCollections()) {
|
|
4604
|
+
const slug = col.slug;
|
|
4605
|
+
try {
|
|
4606
|
+
data.collections[slug] = {
|
|
4607
|
+
slug,
|
|
4608
|
+
label: col.label || slug,
|
|
4609
|
+
fields: extractFields(col.fields),
|
|
4610
|
+
jsonSchema: zodToJsonSchema(registry.getZodSchema(slug), { target: "openApi3" }),
|
|
4611
|
+
createSchema: zodToJsonSchema(registry.getCreateZodSchema(slug), { target: "openApi3" }),
|
|
4612
|
+
updateSchema: zodToJsonSchema(registry.getUpdateZodSchema(slug), { target: "openApi3" }),
|
|
4613
|
+
procedures: {
|
|
4614
|
+
find: { collection: slug, where: "Record<string,any>", sort: "string", limit: "number", page: "number", depth: "number", select: "string[]", draft: "boolean" },
|
|
4615
|
+
findByID: { collection: slug, id: "string", depth: "number", select: "string[]", draft: "boolean" },
|
|
4616
|
+
create: { collection: slug, data: "Record<string,any>", depth: "number", select: "string[]" },
|
|
4617
|
+
update: { collection: slug, id: "string", data: "Record<string,any>", depth: "number", select: "string[]", baseUpdatedAt: "string" },
|
|
4618
|
+
delete: { collection: slug, id: "string" },
|
|
4619
|
+
count: { collection: slug, where: "Record<string,any>" }
|
|
4620
|
+
}
|
|
4621
|
+
};
|
|
4622
|
+
} catch {
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4625
|
+
for (const global of registry.getGlobals()) {
|
|
4626
|
+
const slug = global.slug;
|
|
4627
|
+
try {
|
|
4628
|
+
data.globals[slug] = {
|
|
4629
|
+
slug,
|
|
4630
|
+
label: global.label || slug,
|
|
4631
|
+
fields: extractFields(global.fields),
|
|
4632
|
+
jsonSchema: zodToJsonSchema(registry.getZodSchema(slug), { target: "openApi3" }),
|
|
4633
|
+
procedures: {
|
|
4634
|
+
get: {},
|
|
4635
|
+
update: { data: "Record<string,any>" }
|
|
4636
|
+
}
|
|
4637
|
+
};
|
|
4638
|
+
} catch {
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
return c.json(data);
|
|
4642
|
+
});
|
|
4227
4643
|
app.get("/api/collections", async (c) => {
|
|
4228
4644
|
const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
|
|
4229
4645
|
if (!ctxUser) return c.json({ error: "Authentication required" }, 401);
|
|
@@ -4239,6 +4655,129 @@ function createHonoApp(options) {
|
|
|
4239
4655
|
}));
|
|
4240
4656
|
return c.json(collections2);
|
|
4241
4657
|
});
|
|
4658
|
+
function resolveDocField(fields, doc, fieldName) {
|
|
4659
|
+
if (fieldName in doc) return doc[fieldName];
|
|
4660
|
+
for (const field of fields) {
|
|
4661
|
+
if (!field.name) continue;
|
|
4662
|
+
if (field.type === "tabs" && field.tabs) {
|
|
4663
|
+
const data = doc[field.name];
|
|
4664
|
+
if (data && typeof data === "object" && fieldName in data) return data[fieldName];
|
|
4665
|
+
}
|
|
4666
|
+
if ((field.type === "group" || field.type === "collapsible") && field.fields) {
|
|
4667
|
+
const data = doc[field.name];
|
|
4668
|
+
if (data && typeof data === "object") {
|
|
4669
|
+
if (fieldName in data) return data[fieldName];
|
|
4670
|
+
const nested = resolveDocField(field.fields, data, fieldName);
|
|
4671
|
+
if (nested !== void 0) return nested;
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
return void 0;
|
|
4676
|
+
}
|
|
4677
|
+
function flattenRelationshipFields(fields) {
|
|
4678
|
+
const relFields = [];
|
|
4679
|
+
for (const field of fields) {
|
|
4680
|
+
if (field.type === "relationship") {
|
|
4681
|
+
relFields.push(field);
|
|
4682
|
+
} else if (field.type === "tabs" && field.tabs) {
|
|
4683
|
+
for (const tab of field.tabs) {
|
|
4684
|
+
relFields.push(...flattenRelationshipFields(tab.fields || []));
|
|
4685
|
+
}
|
|
4686
|
+
} else if ((field.type === "group" || field.type === "collapsible") && field.fields) {
|
|
4687
|
+
relFields.push(...flattenRelationshipFields(field.fields || []));
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
return relFields;
|
|
4691
|
+
}
|
|
4692
|
+
function extractRelValue(value) {
|
|
4693
|
+
if (!value) return [];
|
|
4694
|
+
if (typeof value === "string") return [value];
|
|
4695
|
+
if (Array.isArray(value)) return value.map((v) => typeof v === "object" ? v.value ?? v : v);
|
|
4696
|
+
if (typeof value === "object") {
|
|
4697
|
+
const v = value.value ?? value;
|
|
4698
|
+
return typeof v === "string" ? [v] : Array.isArray(v) ? v : [];
|
|
4699
|
+
}
|
|
4700
|
+
return [];
|
|
4701
|
+
}
|
|
4702
|
+
async function populateRelationships(docs, collection, db2, registry2) {
|
|
4703
|
+
if (docs.length === 0) return;
|
|
4704
|
+
const relFields = flattenRelationshipFields(collection.fields);
|
|
4705
|
+
if (relFields.length === 0) return;
|
|
4706
|
+
for (const relField of relFields) {
|
|
4707
|
+
const targetSlugs = Array.isArray(relField.relationTo) ? relField.relationTo : [relField.relationTo];
|
|
4708
|
+
const idsBySlug = {};
|
|
4709
|
+
for (const slug of targetSlugs) {
|
|
4710
|
+
idsBySlug[slug] = /* @__PURE__ */ new Set();
|
|
4711
|
+
}
|
|
4712
|
+
for (const doc of docs) {
|
|
4713
|
+
const raw = resolveDocField(collection.fields, doc, relField.name);
|
|
4714
|
+
const ids = extractRelValue(raw);
|
|
4715
|
+
for (const id of ids) {
|
|
4716
|
+
if (!id) continue;
|
|
4717
|
+
if (targetSlugs.length === 1) {
|
|
4718
|
+
idsBySlug[targetSlugs[0]].add(id);
|
|
4719
|
+
} else {
|
|
4720
|
+
for (const slug of targetSlugs) {
|
|
4721
|
+
idsBySlug[slug].add(id);
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
for (const [targetSlug, idSet] of Object.entries(idsBySlug)) {
|
|
4727
|
+
if (idSet.size === 0) continue;
|
|
4728
|
+
const targetCollection = registry2.getCollection(targetSlug);
|
|
4729
|
+
if (!targetCollection) continue;
|
|
4730
|
+
const titleField = targetCollection.admin?.useAsTitle || "title";
|
|
4731
|
+
const idArr = Array.from(idSet);
|
|
4732
|
+
const relatedDocs = [];
|
|
4733
|
+
for (const id of idArr) {
|
|
4734
|
+
try {
|
|
4735
|
+
const relDoc = await db2.findByID({ collection: targetSlug, id, draft: true });
|
|
4736
|
+
if (relDoc) {
|
|
4737
|
+
const title = resolveDocField(targetCollection.fields, relDoc, titleField) ?? id;
|
|
4738
|
+
relatedDocs.push({ id, title: String(title) });
|
|
4739
|
+
}
|
|
4740
|
+
} catch {
|
|
4741
|
+
relatedDocs.push({ id, title: id });
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
const titleMap = new Map(relatedDocs.map((d) => [d.id, d.title]));
|
|
4745
|
+
for (const doc of docs) {
|
|
4746
|
+
const raw = resolveDocField(collection.fields, doc, relField.name);
|
|
4747
|
+
if (!raw) continue;
|
|
4748
|
+
const setValue = (val) => {
|
|
4749
|
+
if (relField.name in doc) {
|
|
4750
|
+
doc[relField.name] = val;
|
|
4751
|
+
} else {
|
|
4752
|
+
for (const f of collection.fields) {
|
|
4753
|
+
if (f.type === "tabs" && f.tabs) {
|
|
4754
|
+
for (const tab of f.tabs) {
|
|
4755
|
+
if (tab.fields?.some((tf) => tf.name === relField.name)) {
|
|
4756
|
+
const tabData = doc[f.name];
|
|
4757
|
+
if (tabData && typeof tabData === "object") {
|
|
4758
|
+
tabData[relField.name] = val;
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
};
|
|
4766
|
+
if (typeof raw === "string") {
|
|
4767
|
+
setValue({ id: raw, title: titleMap.get(raw) || raw });
|
|
4768
|
+
} else if (Array.isArray(raw)) {
|
|
4769
|
+
setValue(raw.map((v) => {
|
|
4770
|
+
const id = typeof v === "object" ? v.value ?? v.id : v;
|
|
4771
|
+
return { id, title: titleMap.get(id) || id };
|
|
4772
|
+
}));
|
|
4773
|
+
} else if (typeof raw === "object") {
|
|
4774
|
+
const id = raw.value ?? raw.id;
|
|
4775
|
+
setValue({ id, title: titleMap.get(id) || id });
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4779
|
+
}
|
|
4780
|
+
}
|
|
4242
4781
|
app.get("/api/search", async (c) => {
|
|
4243
4782
|
try {
|
|
4244
4783
|
const query = c.req.query("q") || "";
|
|
@@ -4290,11 +4829,12 @@ function createHonoApp(options) {
|
|
|
4290
4829
|
limit,
|
|
4291
4830
|
tenantID: ctxTenantID
|
|
4292
4831
|
});
|
|
4832
|
+
await populateRelationships(searchResult.docs, collection, db, registry);
|
|
4293
4833
|
for (const doc of searchResult.docs) {
|
|
4294
4834
|
const titleField = collection.admin?.useAsTitle || searchableFields.find(
|
|
4295
4835
|
(f) => f === "title" || f === "name" || f === "heading" || f === "slug"
|
|
4296
4836
|
);
|
|
4297
|
-
const title = titleField ? doc
|
|
4837
|
+
const title = titleField ? resolveDocField(collection.fields, doc, titleField) ?? doc.id : doc.id;
|
|
4298
4838
|
results.push({
|
|
4299
4839
|
collection: collection.slug,
|
|
4300
4840
|
label: collection.label || collection.slug,
|
|
@@ -4315,15 +4855,13 @@ function createHonoApp(options) {
|
|
|
4315
4855
|
});
|
|
4316
4856
|
app.get("/api/keys", async (c) => {
|
|
4317
4857
|
try {
|
|
4318
|
-
const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
|
|
4858
|
+
const { user: ctxUser, tenantID: ctxTenantID } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
|
|
4319
4859
|
if (!ctxUser || !hasPermission(ctxUser, "users:read")) {
|
|
4320
4860
|
return c.json({ error: "Forbidden" }, 403);
|
|
4321
4861
|
}
|
|
4322
4862
|
const page = parseInt(c.req.query("page") || "1");
|
|
4323
4863
|
const limit = Math.min(parseInt(c.req.query("limit") || "50"), 100);
|
|
4324
|
-
|
|
4325
|
-
const result = await db.find({ collection: API_KEY_COLLECTION, where: {}, page, limit });
|
|
4326
|
-
console.log("[ApiKeys] Result:", result);
|
|
4864
|
+
const result = await db.find({ collection: API_KEY_COLLECTION, where: {}, page, limit, tenantID: ctxTenantID });
|
|
4327
4865
|
const docs = (result.docs || []).map((doc) => ({
|
|
4328
4866
|
id: doc.id,
|
|
4329
4867
|
name: doc.name,
|
|
@@ -4634,37 +5172,31 @@ function createHonoApp(options) {
|
|
|
4634
5172
|
if (!access.allowed) {
|
|
4635
5173
|
return c.json({ error: access.error }, access.status || 403);
|
|
4636
5174
|
}
|
|
4637
|
-
|
|
5175
|
+
if (ctxTenantID) {
|
|
5176
|
+
db.setTenantContext({ tenantId: ctxTenantID, userId: ctxUser?.id ?? "", role: ctxUser?.role, isSuperAdmin: ctxUser?.role === "super_admin" });
|
|
5177
|
+
}
|
|
4638
5178
|
const url = new URL(c.req.url);
|
|
4639
5179
|
const page = parseInt(url.searchParams.get("page") || "1");
|
|
4640
|
-
const limit = Math.min(
|
|
4641
|
-
parseInt(url.searchParams.get("limit") || "10"),
|
|
4642
|
-
100
|
|
4643
|
-
);
|
|
5180
|
+
const limit = Math.min(parseInt(url.searchParams.get("limit") || "10"), 100);
|
|
4644
5181
|
const sort = url.searchParams.get("sort") || void 0;
|
|
4645
5182
|
const depth = parseInt(url.searchParams.get("depth") || "0");
|
|
4646
5183
|
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
|
-
}
|
|
5184
|
+
const isDraftRequest = !!ctxUser;
|
|
4655
5185
|
const result = await db.find({
|
|
4656
5186
|
collection: slug,
|
|
4657
|
-
where,
|
|
5187
|
+
where: {},
|
|
4658
5188
|
sort,
|
|
4659
5189
|
limit,
|
|
4660
5190
|
page,
|
|
4661
5191
|
depth,
|
|
4662
5192
|
tenantID: ctxTenantID,
|
|
4663
|
-
select
|
|
5193
|
+
select,
|
|
5194
|
+
draft: isDraftRequest
|
|
4664
5195
|
});
|
|
5196
|
+
await populateRelationships(result.docs, collection, db, registry);
|
|
4665
5197
|
return c.json(result);
|
|
4666
5198
|
} catch (error) {
|
|
4667
|
-
console.error(
|
|
5199
|
+
console.error("[API] list error:", error);
|
|
4668
5200
|
return c.json({ error: error.message }, 500);
|
|
4669
5201
|
}
|
|
4670
5202
|
});
|
|
@@ -4689,6 +5221,9 @@ function createHonoApp(options) {
|
|
|
4689
5221
|
return c.json({ error: access.error }, access.status || 403);
|
|
4690
5222
|
}
|
|
4691
5223
|
const id = c.req.param("id");
|
|
5224
|
+
if (!id) {
|
|
5225
|
+
return c.json({ error: "Missing document ID" }, 400);
|
|
5226
|
+
}
|
|
4692
5227
|
const url = new URL(c.req.url);
|
|
4693
5228
|
const compareA = url.searchParams.get("compareA");
|
|
4694
5229
|
const compareB = url.searchParams.get("compareB");
|
|
@@ -4719,36 +5254,6 @@ function createHonoApp(options) {
|
|
|
4719
5254
|
return c.json({ error: error.message }, 500);
|
|
4720
5255
|
}
|
|
4721
5256
|
});
|
|
4722
|
-
app.get(`${basePath}/:id/draft`, async (c) => {
|
|
4723
|
-
try {
|
|
4724
|
-
const {
|
|
4725
|
-
user: ctxUser,
|
|
4726
|
-
tenantID: ctxTenantID,
|
|
4727
|
-
apiKeyContext
|
|
4728
|
-
} = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
|
|
4729
|
-
const access = await checkCollectionAccess(
|
|
4730
|
-
collection,
|
|
4731
|
-
"read",
|
|
4732
|
-
c.req.raw,
|
|
4733
|
-
ctxUser,
|
|
4734
|
-
ctxTenantID,
|
|
4735
|
-
apiKeyContext,
|
|
4736
|
-
enablePublicAccess,
|
|
4737
|
-
defaultCollectionAccess
|
|
4738
|
-
);
|
|
4739
|
-
if (!access.allowed) {
|
|
4740
|
-
return c.json({ error: access.error }, access.status || 403);
|
|
4741
|
-
}
|
|
4742
|
-
const draft = await db.findDraft({
|
|
4743
|
-
collection: slug,
|
|
4744
|
-
documentId: c.req.param("id"),
|
|
4745
|
-
tenantID: ctxTenantID
|
|
4746
|
-
});
|
|
4747
|
-
return c.json({ data: draft });
|
|
4748
|
-
} catch (error) {
|
|
4749
|
-
return c.json({ error: error.message }, 500);
|
|
4750
|
-
}
|
|
4751
|
-
});
|
|
4752
5257
|
app.put(`${basePath}/:id/draft`, async (c) => {
|
|
4753
5258
|
try {
|
|
4754
5259
|
const {
|
|
@@ -4772,24 +5277,39 @@ function createHonoApp(options) {
|
|
|
4772
5277
|
const id = c.req.param("id");
|
|
4773
5278
|
const body = await c.req.json();
|
|
4774
5279
|
const baseUpdatedAt = readBaseUpdatedAt(body);
|
|
4775
|
-
const data = body.data ?? omitRevisionFields(body);
|
|
4776
5280
|
const originalDoc = await db.findByID({
|
|
4777
5281
|
collection: slug,
|
|
4778
5282
|
id,
|
|
4779
|
-
tenantID: ctxTenantID
|
|
5283
|
+
tenantID: ctxTenantID,
|
|
5284
|
+
draft: true
|
|
4780
5285
|
});
|
|
4781
5286
|
if (!originalDoc) {
|
|
4782
5287
|
return c.json({ error: "Document not found" }, 404);
|
|
4783
5288
|
}
|
|
4784
|
-
|
|
5289
|
+
let finalData;
|
|
5290
|
+
if (body.delta) {
|
|
5291
|
+
finalData = { ...originalDoc, ...body.delta };
|
|
5292
|
+
} else {
|
|
5293
|
+
finalData = body.data ?? omitRevisionFields(body);
|
|
5294
|
+
}
|
|
5295
|
+
const version = await db.updateLatestVersion({
|
|
4785
5296
|
collection: slug,
|
|
4786
5297
|
documentId: id,
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
draftUpdatedAt: body.draftUpdatedAt
|
|
5298
|
+
data: finalData,
|
|
5299
|
+
status: "draft",
|
|
5300
|
+
tenantID: ctxTenantID
|
|
4791
5301
|
});
|
|
4792
|
-
|
|
5302
|
+
if (ctxUser) {
|
|
5303
|
+
sessionAuthAdapter?.createAuditLog({
|
|
5304
|
+
action: "document_update",
|
|
5305
|
+
userId: ctxUser.id,
|
|
5306
|
+
resource: slug,
|
|
5307
|
+
resourceId: id,
|
|
5308
|
+
success: true,
|
|
5309
|
+
metadata: { type: "draft_save" }
|
|
5310
|
+
});
|
|
5311
|
+
}
|
|
5312
|
+
return c.json({ data: version, message: "Draft saved successfully" });
|
|
4793
5313
|
} catch (error) {
|
|
4794
5314
|
return c.json({ error: error.message }, 500);
|
|
4795
5315
|
}
|
|
@@ -4814,11 +5334,16 @@ function createHonoApp(options) {
|
|
|
4814
5334
|
if (!access.allowed) {
|
|
4815
5335
|
return c.json({ error: access.error }, access.status || 403);
|
|
4816
5336
|
}
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
5337
|
+
if (ctxUser) {
|
|
5338
|
+
sessionAuthAdapter?.createAuditLog({
|
|
5339
|
+
action: "document_update",
|
|
5340
|
+
userId: ctxUser.id,
|
|
5341
|
+
resource: slug,
|
|
5342
|
+
resourceId: c.req.param("id"),
|
|
5343
|
+
success: true,
|
|
5344
|
+
metadata: { type: "draft_discard" }
|
|
5345
|
+
});
|
|
5346
|
+
}
|
|
4822
5347
|
return c.json({ message: "Draft discarded successfully" });
|
|
4823
5348
|
} catch (error) {
|
|
4824
5349
|
return c.json({ error: error.message }, 500);
|
|
@@ -4861,12 +5386,14 @@ function createHonoApp(options) {
|
|
|
4861
5386
|
doc = await db.findOne({
|
|
4862
5387
|
collection: slug,
|
|
4863
5388
|
where: { slug: id },
|
|
4864
|
-
tenantID: ctxTenantID
|
|
5389
|
+
tenantID: ctxTenantID,
|
|
5390
|
+
draft: isDraftRequest
|
|
4865
5391
|
});
|
|
4866
5392
|
}
|
|
4867
5393
|
if (!doc) {
|
|
4868
5394
|
return c.json({ error: "Document not found" }, 404);
|
|
4869
5395
|
}
|
|
5396
|
+
await populateRelationships([doc], collection, db, registry);
|
|
4870
5397
|
return c.json({ data: doc });
|
|
4871
5398
|
} catch (error) {
|
|
4872
5399
|
return c.json({ error: error.message }, 500);
|
|
@@ -4892,23 +5419,81 @@ function createHonoApp(options) {
|
|
|
4892
5419
|
if (!access.allowed) {
|
|
4893
5420
|
return c.json({ error: access.error }, access.status || 403);
|
|
4894
5421
|
}
|
|
5422
|
+
if (ctxTenantID) {
|
|
5423
|
+
db.setTenantContext({ tenantId: ctxTenantID, userId: ctxUser?.id ?? "", role: ctxUser?.role, isSuperAdmin: ctxUser?.role === "super_admin" });
|
|
5424
|
+
}
|
|
4895
5425
|
auditApiKeyUsage(sessionAuthAdapter, apiKeyContext, basePath, "POST", c.req.raw);
|
|
4896
5426
|
const body = await c.req.json();
|
|
5427
|
+
let validated = body;
|
|
5428
|
+
normalizeEmptyStrings(validated, collection.fields);
|
|
5429
|
+
convertRichtextFields(collection.fields, validated);
|
|
5430
|
+
const hookReq = c.req.raw;
|
|
5431
|
+
if (collection.hooks?.beforeValidate) {
|
|
5432
|
+
for (const hook of collection.hooks.beforeValidate) {
|
|
5433
|
+
const hookResult = await hook({
|
|
5434
|
+
collection: slug,
|
|
5435
|
+
data: validated,
|
|
5436
|
+
req: hookReq,
|
|
5437
|
+
user: ctxUser,
|
|
5438
|
+
tenantID: ctxTenantID,
|
|
5439
|
+
operation: "create"
|
|
5440
|
+
});
|
|
5441
|
+
if (hookResult) Object.assign(validated, hookResult);
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
4897
5444
|
const schema = registry.getCreateZodSchema(slug);
|
|
4898
|
-
let validated;
|
|
4899
5445
|
try {
|
|
4900
|
-
validated = schema.parse(
|
|
5446
|
+
validated = schema.parse(validated);
|
|
4901
5447
|
} catch (zodErr) {
|
|
4902
|
-
return c.json({ error:
|
|
5448
|
+
return c.json({ error: `Validation failed: ${formatZodErrors(zodErr.errors)}`, details: zodErr.errors }, 400);
|
|
4903
5449
|
}
|
|
4904
5450
|
if (collection.tenantScoped && ctxTenantID) {
|
|
4905
5451
|
validated.tenantID = ctxTenantID;
|
|
4906
5452
|
}
|
|
5453
|
+
const isDraftEnabled = collection.versions?.drafts === true;
|
|
5454
|
+
validated.status = isDraftEnabled ? "draft" : "published";
|
|
5455
|
+
if (collection.hooks?.beforeChange) {
|
|
5456
|
+
for (const hook of collection.hooks.beforeChange) {
|
|
5457
|
+
const hookResult = await hook({
|
|
5458
|
+
collection: slug,
|
|
5459
|
+
data: validated,
|
|
5460
|
+
req: hookReq,
|
|
5461
|
+
user: ctxUser,
|
|
5462
|
+
tenantID: ctxTenantID,
|
|
5463
|
+
operation: "create"
|
|
5464
|
+
});
|
|
5465
|
+
if (hookResult) Object.assign(validated, hookResult);
|
|
5466
|
+
}
|
|
5467
|
+
}
|
|
4907
5468
|
const doc = await db.create({
|
|
4908
5469
|
collection: slug,
|
|
4909
5470
|
data: validated,
|
|
4910
5471
|
tenantID: ctxTenantID
|
|
4911
5472
|
});
|
|
5473
|
+
if (isDraftEnabled) {
|
|
5474
|
+
await db.createVersion({
|
|
5475
|
+
collection: slug,
|
|
5476
|
+
documentId: doc.id,
|
|
5477
|
+
data: validated,
|
|
5478
|
+
status: "draft",
|
|
5479
|
+
createdBy: ctxUser?.id,
|
|
5480
|
+
changeDescription: "Created",
|
|
5481
|
+
tenantID: ctxTenantID
|
|
5482
|
+
});
|
|
5483
|
+
}
|
|
5484
|
+
if (collection.hooks?.afterChange) {
|
|
5485
|
+
for (const hook of collection.hooks.afterChange) {
|
|
5486
|
+
await hook({
|
|
5487
|
+
collection: slug,
|
|
5488
|
+
doc,
|
|
5489
|
+
data: validated,
|
|
5490
|
+
req: hookReq,
|
|
5491
|
+
user: ctxUser,
|
|
5492
|
+
tenantID: ctxTenantID,
|
|
5493
|
+
operation: "create"
|
|
5494
|
+
});
|
|
5495
|
+
}
|
|
5496
|
+
}
|
|
4912
5497
|
if (webhookService) {
|
|
4913
5498
|
webhookService.trigger(getWebhookEvent(slug, "create"), {
|
|
4914
5499
|
collection: slug,
|
|
@@ -4918,11 +5503,20 @@ function createHonoApp(options) {
|
|
|
4918
5503
|
tenantId: ctxTenantID
|
|
4919
5504
|
}).catch((err) => console.error(`[Webhook] Failed to trigger:`, err));
|
|
4920
5505
|
}
|
|
5506
|
+
if (ctxUser) {
|
|
5507
|
+
sessionAuthAdapter?.createAuditLog({
|
|
5508
|
+
action: "document_create",
|
|
5509
|
+
userId: ctxUser.id,
|
|
5510
|
+
resource: slug,
|
|
5511
|
+
resourceId: doc.id,
|
|
5512
|
+
success: true
|
|
5513
|
+
});
|
|
5514
|
+
}
|
|
4921
5515
|
return c.json({ data: doc, message: "Created successfully" }, 201);
|
|
4922
5516
|
} catch (error) {
|
|
4923
5517
|
if (error.name === "ZodError") {
|
|
4924
5518
|
return c.json(
|
|
4925
|
-
{ error:
|
|
5519
|
+
{ error: `Validation failed: ${formatZodErrors(error.errors)}`, details: error.errors },
|
|
4926
5520
|
400
|
|
4927
5521
|
);
|
|
4928
5522
|
}
|
|
@@ -4952,61 +5546,92 @@ function createHonoApp(options) {
|
|
|
4952
5546
|
const id = c.req.param("id");
|
|
4953
5547
|
const body = await c.req.json();
|
|
4954
5548
|
const baseUpdatedAt = readBaseUpdatedAt(body);
|
|
4955
|
-
console.log(`[PATCH] ${slug}/${id}`, {
|
|
4956
|
-
baseUpdatedAt,
|
|
4957
|
-
bodyKeys: Object.keys(body),
|
|
4958
|
-
tenantID: ctxTenantID
|
|
4959
|
-
});
|
|
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
5549
|
const originalDoc = await db.findByID({
|
|
4969
5550
|
collection: slug,
|
|
4970
5551
|
id,
|
|
4971
5552
|
tenantID: ctxTenantID,
|
|
4972
5553
|
draft: true
|
|
4973
|
-
// Always fetch current doc regardless of status
|
|
4974
5554
|
});
|
|
4975
|
-
if (originalDoc) {
|
|
4976
|
-
console.log(`[PATCH] Original doc updatedAt:`, originalDoc.updatedAt);
|
|
4977
|
-
}
|
|
4978
5555
|
if (!originalDoc) {
|
|
4979
5556
|
return c.json({ error: "Document not found" }, 404);
|
|
4980
5557
|
}
|
|
4981
5558
|
if (baseUpdatedAt && originalDoc.updatedAt && baseUpdatedAt !== originalDoc.updatedAt) {
|
|
4982
5559
|
return c.json(buildConflictResponse(baseUpdatedAt, originalDoc), 409);
|
|
4983
5560
|
}
|
|
5561
|
+
let validated = Object.fromEntries(
|
|
5562
|
+
Object.entries(omitRevisionFields(body)).filter(
|
|
5563
|
+
([_, v]) => v !== "null" && v !== void 0
|
|
5564
|
+
)
|
|
5565
|
+
);
|
|
5566
|
+
normalizeEmptyStrings(validated, collection.fields);
|
|
5567
|
+
convertRichtextFields(collection.fields, validated);
|
|
5568
|
+
const hookReq = c.req.raw;
|
|
5569
|
+
if (collection.hooks?.beforeValidate) {
|
|
5570
|
+
for (const hook of collection.hooks.beforeValidate) {
|
|
5571
|
+
const hookResult = await hook({
|
|
5572
|
+
collection: slug,
|
|
5573
|
+
data: validated,
|
|
5574
|
+
originalDoc,
|
|
5575
|
+
req: hookReq,
|
|
5576
|
+
user: ctxUser,
|
|
5577
|
+
tenantID: ctxTenantID,
|
|
5578
|
+
operation: "update"
|
|
5579
|
+
});
|
|
5580
|
+
if (hookResult) Object.assign(validated, hookResult);
|
|
5581
|
+
}
|
|
5582
|
+
}
|
|
5583
|
+
const schema = registry.getUpdateZodSchema(slug);
|
|
5584
|
+
validated = schema.parse(validated);
|
|
5585
|
+
if (collection.hooks?.beforeChange) {
|
|
5586
|
+
for (const hook of collection.hooks.beforeChange) {
|
|
5587
|
+
const hookResult = await hook({
|
|
5588
|
+
collection: slug,
|
|
5589
|
+
data: validated,
|
|
5590
|
+
originalDoc,
|
|
5591
|
+
req: hookReq,
|
|
5592
|
+
user: ctxUser,
|
|
5593
|
+
tenantID: ctxTenantID,
|
|
5594
|
+
operation: "update"
|
|
5595
|
+
});
|
|
5596
|
+
if (hookResult) Object.assign(validated, hookResult);
|
|
5597
|
+
}
|
|
5598
|
+
}
|
|
5599
|
+
const isDraft = c.req.header("X-Draft") === "true";
|
|
4984
5600
|
const isDraftEnabled = collection.versions?.drafts === true;
|
|
4985
|
-
const
|
|
5601
|
+
const isAutosave = c.req.query("autosave") === "true";
|
|
4986
5602
|
let doc;
|
|
4987
|
-
if (isDraftEnabled &&
|
|
5603
|
+
if (isDraftEnabled && isDraft) {
|
|
4988
5604
|
await db.createVersion({
|
|
4989
5605
|
collection: slug,
|
|
4990
5606
|
documentId: id,
|
|
4991
5607
|
data: validated,
|
|
4992
5608
|
status: "draft",
|
|
5609
|
+
autosave: isAutosave,
|
|
4993
5610
|
createdBy: ctxUser?.id,
|
|
4994
|
-
changeDescription: "
|
|
5611
|
+
changeDescription: isAutosave ? "Autosave" : "Draft saved",
|
|
4995
5612
|
tenantID: ctxTenantID
|
|
4996
5613
|
});
|
|
5614
|
+
} else if (isDraftEnabled) {
|
|
4997
5615
|
await db.update({
|
|
4998
5616
|
collection: slug,
|
|
4999
5617
|
id,
|
|
5000
|
-
data: {
|
|
5001
|
-
|
|
5618
|
+
data: { ...validated, status: "published" },
|
|
5619
|
+
tenantID: ctxTenantID
|
|
5620
|
+
});
|
|
5621
|
+
await db.createVersion({
|
|
5622
|
+
collection: slug,
|
|
5623
|
+
documentId: id,
|
|
5624
|
+
data: validated,
|
|
5625
|
+
status: "published",
|
|
5626
|
+
createdBy: ctxUser?.id,
|
|
5627
|
+
changeDescription: "Published",
|
|
5002
5628
|
tenantID: ctxTenantID
|
|
5003
5629
|
});
|
|
5004
5630
|
} else {
|
|
5005
|
-
const saveData = isDraftEnabled ? { ...validated, _status: "draft", _has_draft: false } : validated;
|
|
5006
5631
|
await db.update({
|
|
5007
5632
|
collection: slug,
|
|
5008
5633
|
id,
|
|
5009
|
-
data:
|
|
5634
|
+
data: validated,
|
|
5010
5635
|
tenantID: ctxTenantID
|
|
5011
5636
|
});
|
|
5012
5637
|
}
|
|
@@ -5016,24 +5641,19 @@ function createHonoApp(options) {
|
|
|
5016
5641
|
tenantID: ctxTenantID,
|
|
5017
5642
|
draft: true
|
|
5018
5643
|
});
|
|
5019
|
-
if (
|
|
5020
|
-
|
|
5021
|
-
await
|
|
5644
|
+
if (collection.hooks?.afterChange) {
|
|
5645
|
+
for (const hook of collection.hooks.afterChange) {
|
|
5646
|
+
await hook({
|
|
5022
5647
|
collection: slug,
|
|
5023
|
-
|
|
5648
|
+
doc,
|
|
5024
5649
|
data: validated,
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
tenantID: ctxTenantID
|
|
5650
|
+
originalDoc,
|
|
5651
|
+
req: hookReq,
|
|
5652
|
+
user: ctxUser,
|
|
5653
|
+
tenantID: ctxTenantID,
|
|
5654
|
+
operation: "update"
|
|
5029
5655
|
});
|
|
5030
5656
|
}
|
|
5031
|
-
} else {
|
|
5032
|
-
await db.deleteDraft({
|
|
5033
|
-
collection: slug,
|
|
5034
|
-
documentId: id,
|
|
5035
|
-
tenantID: ctxTenantID
|
|
5036
|
-
});
|
|
5037
5657
|
}
|
|
5038
5658
|
if (webhookService) {
|
|
5039
5659
|
webhookService.trigger(getWebhookEvent(slug, "update"), {
|
|
@@ -5045,16 +5665,26 @@ function createHonoApp(options) {
|
|
|
5045
5665
|
tenantId: ctxTenantID
|
|
5046
5666
|
}).catch((err) => console.error(`[Webhook] Failed to trigger:`, err));
|
|
5047
5667
|
}
|
|
5048
|
-
|
|
5668
|
+
auditApiKeyUsage(sessionAuthAdapter, apiKeyContext, `${basePath}/${id}`, "PATCH", c.req.raw);
|
|
5669
|
+
if (ctxUser) {
|
|
5670
|
+
sessionAuthAdapter?.createAuditLog({
|
|
5671
|
+
action: "document_update",
|
|
5672
|
+
userId: ctxUser.id,
|
|
5673
|
+
resource: slug,
|
|
5674
|
+
resourceId: id,
|
|
5675
|
+
success: true
|
|
5676
|
+
});
|
|
5677
|
+
}
|
|
5049
5678
|
return c.json({ data: doc, message: isDraftEnabled ? "Draft saved" : "Updated successfully" });
|
|
5050
5679
|
} catch (error) {
|
|
5051
5680
|
if (error.name === "ZodError") {
|
|
5052
5681
|
console.error(`[PATCH ${basePath}/:id] Validation failed:`, error.errors);
|
|
5053
5682
|
return c.json(
|
|
5054
|
-
{ error:
|
|
5683
|
+
{ error: `Validation failed: ${formatZodErrors(error.errors)}`, details: error.errors },
|
|
5055
5684
|
400
|
|
5056
5685
|
);
|
|
5057
5686
|
}
|
|
5687
|
+
console.error(`[PATCH ${basePath}/:id] ERROR:`, error.message, `CAUSE:`, error.cause?.message || error.cause, `QUERY:`, error.query);
|
|
5058
5688
|
return c.json({ error: error.message }, 500);
|
|
5059
5689
|
}
|
|
5060
5690
|
});
|
|
@@ -5078,23 +5708,50 @@ function createHonoApp(options) {
|
|
|
5078
5708
|
if (!access.allowed) {
|
|
5079
5709
|
return c.json({ error: access.error }, access.status || 403);
|
|
5080
5710
|
}
|
|
5711
|
+
if (ctxTenantID) {
|
|
5712
|
+
db.setTenantContext({ tenantId: ctxTenantID, userId: ctxUser?.id ?? "", role: ctxUser?.role, isSuperAdmin: ctxUser?.role === "super_admin" });
|
|
5713
|
+
}
|
|
5081
5714
|
const id = c.req.param("id");
|
|
5082
|
-
|
|
5715
|
+
const hookReq = c.req.raw;
|
|
5083
5716
|
const originalDoc = await db.findByID({
|
|
5084
5717
|
collection: slug,
|
|
5085
5718
|
id,
|
|
5086
|
-
tenantID: ctxTenantID
|
|
5719
|
+
tenantID: ctxTenantID,
|
|
5720
|
+
draft: true
|
|
5087
5721
|
});
|
|
5722
|
+
if (!originalDoc) {
|
|
5723
|
+
return c.json({ error: "Document not found" }, 404);
|
|
5724
|
+
}
|
|
5725
|
+
if (collection.hooks?.beforeDelete) {
|
|
5726
|
+
for (const hook of collection.hooks.beforeDelete) {
|
|
5727
|
+
await hook({
|
|
5728
|
+
collection: slug,
|
|
5729
|
+
doc: originalDoc,
|
|
5730
|
+
req: hookReq,
|
|
5731
|
+
user: ctxUser,
|
|
5732
|
+
tenantID: ctxTenantID,
|
|
5733
|
+
operation: "delete"
|
|
5734
|
+
});
|
|
5735
|
+
}
|
|
5736
|
+
}
|
|
5088
5737
|
const doc = await db.delete({
|
|
5089
5738
|
collection: slug,
|
|
5090
5739
|
id,
|
|
5091
5740
|
tenantID: ctxTenantID
|
|
5092
5741
|
});
|
|
5093
|
-
|
|
5094
|
-
collection
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5742
|
+
if (collection.hooks?.afterDelete) {
|
|
5743
|
+
for (const hook of collection.hooks.afterDelete) {
|
|
5744
|
+
await hook({
|
|
5745
|
+
collection: slug,
|
|
5746
|
+
doc,
|
|
5747
|
+
originalDoc,
|
|
5748
|
+
req: hookReq,
|
|
5749
|
+
user: ctxUser,
|
|
5750
|
+
tenantID: ctxTenantID,
|
|
5751
|
+
operation: "delete"
|
|
5752
|
+
});
|
|
5753
|
+
}
|
|
5754
|
+
}
|
|
5098
5755
|
if (webhookService) {
|
|
5099
5756
|
webhookService.trigger(getWebhookEvent(slug, "delete"), {
|
|
5100
5757
|
collection: slug,
|
|
@@ -5105,6 +5762,16 @@ function createHonoApp(options) {
|
|
|
5105
5762
|
tenantId: ctxTenantID
|
|
5106
5763
|
}).catch((err) => console.error(`[Webhook] Failed to trigger:`, err));
|
|
5107
5764
|
}
|
|
5765
|
+
auditApiKeyUsage(sessionAuthAdapter, apiKeyContext, `${basePath}/${id}`, "DELETE", c.req.raw);
|
|
5766
|
+
if (ctxUser) {
|
|
5767
|
+
sessionAuthAdapter?.createAuditLog({
|
|
5768
|
+
action: "document_delete",
|
|
5769
|
+
userId: ctxUser.id,
|
|
5770
|
+
resource: slug,
|
|
5771
|
+
resourceId: id,
|
|
5772
|
+
success: true
|
|
5773
|
+
});
|
|
5774
|
+
}
|
|
5108
5775
|
return c.json({ data: doc, message: "Deleted successfully" });
|
|
5109
5776
|
} catch (error) {
|
|
5110
5777
|
console.error(`[DELETE] Error deleting ${slug}:`, error);
|
|
@@ -5113,7 +5780,6 @@ function createHonoApp(options) {
|
|
|
5113
5780
|
});
|
|
5114
5781
|
app.post(`${basePath}/:id/duplicate`, async (c) => {
|
|
5115
5782
|
try {
|
|
5116
|
-
console.log(`[Duplicate] Request for ${slug}`);
|
|
5117
5783
|
const {
|
|
5118
5784
|
user: ctxUser,
|
|
5119
5785
|
tenantID: ctxTenantID,
|
|
@@ -5130,25 +5796,21 @@ function createHonoApp(options) {
|
|
|
5130
5796
|
defaultCollectionAccess
|
|
5131
5797
|
);
|
|
5132
5798
|
if (!access.allowed) {
|
|
5133
|
-
console.log("[Duplicate] Access denied:", access.error);
|
|
5134
5799
|
return c.json({ error: access.error }, access.status || 403);
|
|
5135
5800
|
}
|
|
5136
5801
|
const id = c.req.param("id");
|
|
5137
|
-
console.log(`[Duplicate] ID: ${id}`);
|
|
5138
5802
|
const originalDoc = await db.findByID({
|
|
5139
5803
|
collection: slug,
|
|
5140
5804
|
id,
|
|
5141
|
-
tenantID: ctxTenantID
|
|
5805
|
+
tenantID: ctxTenantID,
|
|
5806
|
+
draft: true
|
|
5142
5807
|
});
|
|
5143
5808
|
if (!originalDoc) {
|
|
5144
|
-
console.log("[Duplicate] Document not found");
|
|
5145
5809
|
return c.json({ error: "Document not found" }, 404);
|
|
5146
5810
|
}
|
|
5147
|
-
console.log(`[Duplicate] Original doc:`, originalDoc);
|
|
5148
5811
|
const { id: _oldId, createdAt: _oldCreated, updatedAt: _oldUpdated, ...docData } = originalDoc;
|
|
5149
5812
|
const timestamp = Date.now().toString(36);
|
|
5150
5813
|
const newSlug = `${originalDoc.slug || "document"}-copy-${timestamp}`;
|
|
5151
|
-
console.log(`[Duplicate] New slug: ${newSlug}`);
|
|
5152
5814
|
const newDoc = await db.create({
|
|
5153
5815
|
collection: slug,
|
|
5154
5816
|
data: {
|
|
@@ -5158,7 +5820,6 @@ function createHonoApp(options) {
|
|
|
5158
5820
|
},
|
|
5159
5821
|
tenantID: ctxTenantID
|
|
5160
5822
|
});
|
|
5161
|
-
console.log(`[Duplicate] Created successfully:`, newDoc.id);
|
|
5162
5823
|
return c.json({ data: newDoc, message: "Document duplicated successfully" });
|
|
5163
5824
|
} catch (error) {
|
|
5164
5825
|
console.error("[Duplicate] Error:", error);
|
|
@@ -5198,7 +5859,7 @@ function createHonoApp(options) {
|
|
|
5198
5859
|
const doc = await db.update({
|
|
5199
5860
|
collection: slug,
|
|
5200
5861
|
id,
|
|
5201
|
-
data: { ...version.data,
|
|
5862
|
+
data: { ...version.data, status: "draft" },
|
|
5202
5863
|
tenantID: ctxTenantID
|
|
5203
5864
|
});
|
|
5204
5865
|
return c.json({
|
|
@@ -5243,7 +5904,7 @@ function createHonoApp(options) {
|
|
|
5243
5904
|
const doc = await db.update({
|
|
5244
5905
|
collection: slug,
|
|
5245
5906
|
id: c.req.param("id"),
|
|
5246
|
-
data: { ...version.data,
|
|
5907
|
+
data: { ...version.data, status: "draft" },
|
|
5247
5908
|
tenantID: ctxTenantID
|
|
5248
5909
|
});
|
|
5249
5910
|
return c.json({
|
|
@@ -5276,6 +5937,9 @@ function createHonoApp(options) {
|
|
|
5276
5937
|
if (!access.allowed) {
|
|
5277
5938
|
return c.json({ error: access.error }, access.status || 403);
|
|
5278
5939
|
}
|
|
5940
|
+
if (ctxTenantID) {
|
|
5941
|
+
db.setTenantContext({ tenantId: ctxTenantID, userId: ctxUser?.id ?? "", role: ctxUser?.role, isSuperAdmin: ctxUser?.role === "super_admin" });
|
|
5942
|
+
}
|
|
5279
5943
|
const id = c.req.param("id");
|
|
5280
5944
|
const body = await c.req.json().catch(() => ({}));
|
|
5281
5945
|
const baseUpdatedAt = readBaseUpdatedAt(body);
|
|
@@ -5291,9 +5955,9 @@ function createHonoApp(options) {
|
|
|
5291
5955
|
if (baseUpdatedAt && originalDoc.updatedAt && baseUpdatedAt !== originalDoc.updatedAt) {
|
|
5292
5956
|
return c.json(buildConflictResponse(baseUpdatedAt, originalDoc), 409);
|
|
5293
5957
|
}
|
|
5294
|
-
let publishData = {
|
|
5958
|
+
let publishData = { status: "published" };
|
|
5295
5959
|
let finalContent = originalDoc;
|
|
5296
|
-
if (
|
|
5960
|
+
if (collection.versions?.drafts) {
|
|
5297
5961
|
const versions = await db.findVersions({
|
|
5298
5962
|
collection: slug,
|
|
5299
5963
|
documentId: id,
|
|
@@ -5301,9 +5965,10 @@ function createHonoApp(options) {
|
|
|
5301
5965
|
sort: "-createdAt",
|
|
5302
5966
|
tenantID: ctxTenantID
|
|
5303
5967
|
});
|
|
5304
|
-
if (versions.docs.length > 0
|
|
5305
|
-
|
|
5306
|
-
|
|
5968
|
+
if (versions.docs.length > 0) {
|
|
5969
|
+
const latestVersion = versions.docs[0];
|
|
5970
|
+
finalContent = { ...originalDoc, ...latestVersion.data };
|
|
5971
|
+
publishData = { ...latestVersion.data, ...publishData };
|
|
5307
5972
|
}
|
|
5308
5973
|
}
|
|
5309
5974
|
const doc = await db.update({
|
|
@@ -5316,18 +5981,13 @@ function createHonoApp(options) {
|
|
|
5316
5981
|
await db.createVersion({
|
|
5317
5982
|
collection: slug,
|
|
5318
5983
|
documentId: id,
|
|
5319
|
-
data: { ...finalContent,
|
|
5984
|
+
data: { ...finalContent, status: "published" },
|
|
5320
5985
|
status: "published",
|
|
5321
5986
|
createdBy: ctxUser?.id,
|
|
5322
5987
|
changeDescription: "Published",
|
|
5323
5988
|
tenantID: ctxTenantID
|
|
5324
5989
|
});
|
|
5325
5990
|
}
|
|
5326
|
-
await db.deleteDraft({
|
|
5327
|
-
collection: slug,
|
|
5328
|
-
documentId: id,
|
|
5329
|
-
tenantID: ctxTenantID
|
|
5330
|
-
});
|
|
5331
5991
|
if (webhookService) {
|
|
5332
5992
|
webhookService.trigger(getWebhookEvent(slug, "update"), {
|
|
5333
5993
|
collection: slug,
|
|
@@ -5375,7 +6035,7 @@ function createHonoApp(options) {
|
|
|
5375
6035
|
const doc = await db.update({
|
|
5376
6036
|
collection: slug,
|
|
5377
6037
|
id,
|
|
5378
|
-
data: {
|
|
6038
|
+
data: { status: "draft" },
|
|
5379
6039
|
tenantID: ctxTenantID
|
|
5380
6040
|
});
|
|
5381
6041
|
if (webhookService) {
|
|
@@ -5410,13 +6070,31 @@ function createHonoApp(options) {
|
|
|
5410
6070
|
if (!access.allowed) {
|
|
5411
6071
|
return c.json({ error: access.error }, access.status || 403);
|
|
5412
6072
|
}
|
|
5413
|
-
const isDraftRequest =
|
|
5414
|
-
|
|
6073
|
+
const isDraftRequest = !!ctxUser;
|
|
6074
|
+
let doc = await db.findOne({
|
|
5415
6075
|
collection: `_globals_${slug}`,
|
|
5416
6076
|
where: {},
|
|
5417
6077
|
tenantID: ctxTenantID,
|
|
5418
6078
|
draft: isDraftRequest
|
|
5419
6079
|
});
|
|
6080
|
+
if (slug === "system") {
|
|
6081
|
+
const newSecret = crypto.randomBytes(32).toString("hex");
|
|
6082
|
+
if (!doc) {
|
|
6083
|
+
doc = await db.create({
|
|
6084
|
+
collection: `_globals_${slug}`,
|
|
6085
|
+
data: { id: slug, appSecret: newSecret },
|
|
6086
|
+
tenantID: ctxTenantID
|
|
6087
|
+
});
|
|
6088
|
+
} else if (!doc.appSecret) {
|
|
6089
|
+
await db.update({
|
|
6090
|
+
collection: `_globals_${slug}`,
|
|
6091
|
+
id: slug,
|
|
6092
|
+
data: { appSecret: newSecret },
|
|
6093
|
+
tenantID: ctxTenantID
|
|
6094
|
+
});
|
|
6095
|
+
doc.appSecret = newSecret;
|
|
6096
|
+
}
|
|
6097
|
+
}
|
|
5420
6098
|
return c.json({ data: doc || {} });
|
|
5421
6099
|
} catch (error) {
|
|
5422
6100
|
return c.json({ error: error.message }, 500);
|
|
@@ -5436,18 +6114,20 @@ function createHonoApp(options) {
|
|
|
5436
6114
|
if (!access.allowed) {
|
|
5437
6115
|
return c.json({ error: access.error }, access.status || 403);
|
|
5438
6116
|
}
|
|
5439
|
-
const body = await c.req.json();
|
|
6117
|
+
const body = omitRevisionFields(await c.req.json());
|
|
5440
6118
|
const cleaned = Object.fromEntries(
|
|
5441
6119
|
Object.entries(body).filter(([_, v]) => v !== null && v !== "null" && v !== void 0)
|
|
5442
6120
|
);
|
|
5443
|
-
|
|
6121
|
+
normalizeEmptyStrings(cleaned, globalConfig.fields);
|
|
6122
|
+
convertRichtextFields(globalConfig.fields, cleaned);
|
|
6123
|
+
const schema = registry.getUpdateZodSchema(slug);
|
|
5444
6124
|
let validated;
|
|
5445
6125
|
try {
|
|
5446
6126
|
validated = schema.parse(cleaned);
|
|
5447
6127
|
} catch (zodErr) {
|
|
5448
|
-
return c.json({ error:
|
|
6128
|
+
return c.json({ error: `Validation failed: ${formatZodErrors(zodErr.errors)}`, details: zodErr.errors }, 400);
|
|
5449
6129
|
}
|
|
5450
|
-
const SYSTEM_FIELDS = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt", "
|
|
6130
|
+
const SYSTEM_FIELDS = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt", "status", "baseUpdatedAt", "_baseUpdatedAt"]);
|
|
5451
6131
|
const userData = Object.fromEntries(
|
|
5452
6132
|
Object.entries(validated).filter(([k]) => !SYSTEM_FIELDS.has(k))
|
|
5453
6133
|
);
|
|
@@ -5458,49 +6138,68 @@ function createHonoApp(options) {
|
|
|
5458
6138
|
tenantID: ctxTenantID,
|
|
5459
6139
|
draft: true
|
|
5460
6140
|
});
|
|
6141
|
+
const isDraft = c.req.header("X-Draft") === "true";
|
|
5461
6142
|
const isDraftEnabled = globalConfig.versions?.drafts === true;
|
|
5462
|
-
const
|
|
6143
|
+
const isAutosave = c.req.query("autosave") === "true";
|
|
5463
6144
|
let doc;
|
|
5464
|
-
if (isDraftEnabled &&
|
|
6145
|
+
if (isDraftEnabled && isDraft) {
|
|
5465
6146
|
await db.createVersion({
|
|
5466
6147
|
collection: collectionSlug,
|
|
5467
6148
|
documentId: slug,
|
|
5468
6149
|
data: userData,
|
|
5469
6150
|
status: "draft",
|
|
6151
|
+
autosave: isAutosave,
|
|
5470
6152
|
createdBy: ctxUser?.id,
|
|
5471
|
-
changeDescription: "Manual save
|
|
5472
|
-
tenantID: ctxTenantID
|
|
5473
|
-
});
|
|
5474
|
-
doc = await db.update({
|
|
5475
|
-
collection: collectionSlug,
|
|
5476
|
-
id: slug,
|
|
5477
|
-
data: { _has_draft: true },
|
|
6153
|
+
changeDescription: isAutosave ? "Autosave" : "Manual save",
|
|
5478
6154
|
tenantID: ctxTenantID
|
|
5479
6155
|
});
|
|
5480
|
-
|
|
5481
|
-
|
|
6156
|
+
if (!originalDoc) {
|
|
6157
|
+
doc = await db.create({
|
|
6158
|
+
collection: collectionSlug,
|
|
6159
|
+
data: { ...userData, id: slug, status: "draft" },
|
|
6160
|
+
tenantID: ctxTenantID
|
|
6161
|
+
});
|
|
6162
|
+
} else {
|
|
6163
|
+
doc = originalDoc;
|
|
6164
|
+
}
|
|
6165
|
+
} else if (isDraftEnabled && !isDraft) {
|
|
6166
|
+
const publishStatus = "published";
|
|
5482
6167
|
if (originalDoc) {
|
|
5483
6168
|
doc = await db.update({
|
|
5484
6169
|
collection: collectionSlug,
|
|
5485
6170
|
id: slug,
|
|
5486
|
-
data:
|
|
6171
|
+
data: { ...userData, status: publishStatus },
|
|
5487
6172
|
tenantID: ctxTenantID
|
|
5488
6173
|
});
|
|
5489
6174
|
} else {
|
|
5490
6175
|
doc = await db.create({
|
|
5491
6176
|
collection: collectionSlug,
|
|
5492
|
-
data: { ...
|
|
6177
|
+
data: { ...userData, id: slug, status: publishStatus },
|
|
5493
6178
|
tenantID: ctxTenantID
|
|
5494
6179
|
});
|
|
5495
6180
|
}
|
|
5496
|
-
|
|
5497
|
-
|
|
6181
|
+
await db.createVersion({
|
|
6182
|
+
collection: collectionSlug,
|
|
6183
|
+
documentId: slug,
|
|
6184
|
+
data: userData,
|
|
6185
|
+
status: publishStatus,
|
|
6186
|
+
autosave: false,
|
|
6187
|
+
createdBy: ctxUser?.id,
|
|
6188
|
+
changeDescription: "Published",
|
|
6189
|
+
tenantID: ctxTenantID
|
|
6190
|
+
});
|
|
6191
|
+
} else {
|
|
6192
|
+
if (originalDoc) {
|
|
6193
|
+
doc = await db.update({
|
|
5498
6194
|
collection: collectionSlug,
|
|
5499
|
-
|
|
6195
|
+
id: slug,
|
|
5500
6196
|
data: userData,
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
6197
|
+
tenantID: ctxTenantID
|
|
6198
|
+
});
|
|
6199
|
+
} else {
|
|
6200
|
+
doc = await db.create({
|
|
6201
|
+
collection: collectionSlug,
|
|
6202
|
+
data: { ...userData, id: slug },
|
|
5504
6203
|
tenantID: ctxTenantID
|
|
5505
6204
|
});
|
|
5506
6205
|
}
|
|
@@ -5509,6 +6208,22 @@ function createHonoApp(options) {
|
|
|
5509
6208
|
mediaService = null;
|
|
5510
6209
|
mediaServiceInitError = null;
|
|
5511
6210
|
}
|
|
6211
|
+
if (slug === "email-settings") {
|
|
6212
|
+
const newEmailTransport = await EmailTransport.fromConfig(db);
|
|
6213
|
+
authRoutes.email = newEmailTransport || void 0;
|
|
6214
|
+
}
|
|
6215
|
+
if (slug === "system") {
|
|
6216
|
+
await loadSecrets();
|
|
6217
|
+
}
|
|
6218
|
+
if (ctxUser) {
|
|
6219
|
+
sessionAuthAdapter?.createAuditLog({
|
|
6220
|
+
action: "settings_change",
|
|
6221
|
+
userId: ctxUser.id,
|
|
6222
|
+
resource: `global:${slug}`,
|
|
6223
|
+
resourceId: slug,
|
|
6224
|
+
success: true
|
|
6225
|
+
});
|
|
6226
|
+
}
|
|
5512
6227
|
return c.json({ data: doc, message: "Updated successfully" });
|
|
5513
6228
|
} catch (error) {
|
|
5514
6229
|
console.error(`[API] Save global "${slug}" failed:`, error);
|
|
@@ -5533,9 +6248,9 @@ function createHonoApp(options) {
|
|
|
5533
6248
|
draft: true
|
|
5534
6249
|
});
|
|
5535
6250
|
if (!originalDoc) return c.json({ error: "Global not found" }, 404);
|
|
5536
|
-
let publishData = {
|
|
6251
|
+
let publishData = { status: "published" };
|
|
5537
6252
|
let finalContent = originalDoc;
|
|
5538
|
-
if (
|
|
6253
|
+
if (globalConfig.versions?.drafts) {
|
|
5539
6254
|
const versions = await db.findVersions({
|
|
5540
6255
|
collection: collectionSlug,
|
|
5541
6256
|
documentId: slug,
|
|
@@ -5543,7 +6258,7 @@ function createHonoApp(options) {
|
|
|
5543
6258
|
sort: "-createdAt",
|
|
5544
6259
|
tenantID: ctxTenantID
|
|
5545
6260
|
});
|
|
5546
|
-
if (versions.docs.length > 0
|
|
6261
|
+
if (versions.docs.length > 0) {
|
|
5547
6262
|
finalContent = { ...originalDoc, ...versions.docs[0].data };
|
|
5548
6263
|
publishData = { ...versions.docs[0].data, ...publishData };
|
|
5549
6264
|
}
|
|
@@ -5558,7 +6273,7 @@ function createHonoApp(options) {
|
|
|
5558
6273
|
await db.createVersion({
|
|
5559
6274
|
collection: collectionSlug,
|
|
5560
6275
|
documentId: slug,
|
|
5561
|
-
data: { ...finalContent,
|
|
6276
|
+
data: { ...finalContent, status: "published" },
|
|
5562
6277
|
status: "published",
|
|
5563
6278
|
createdBy: ctxUser?.id,
|
|
5564
6279
|
changeDescription: "Published",
|
|
@@ -5578,7 +6293,7 @@ function createHonoApp(options) {
|
|
|
5578
6293
|
const doc = await db.update({
|
|
5579
6294
|
collection: `_globals_${slug}`,
|
|
5580
6295
|
id: slug,
|
|
5581
|
-
data: {
|
|
6296
|
+
data: { status: "draft" },
|
|
5582
6297
|
tenantID: ctxTenantID
|
|
5583
6298
|
});
|
|
5584
6299
|
return c.json({ data: doc, message: "Unpublished successfully" });
|
|
@@ -5638,9 +6353,13 @@ function createHonoApp(options) {
|
|
|
5638
6353
|
const doc = await db.update({
|
|
5639
6354
|
collection: collectionSlug,
|
|
5640
6355
|
id: slug,
|
|
5641
|
-
data: { ...version.data,
|
|
6356
|
+
data: { ...version.data, status: "draft" },
|
|
5642
6357
|
tenantID: ctxTenantID
|
|
5643
6358
|
});
|
|
6359
|
+
return c.json({
|
|
6360
|
+
data: doc,
|
|
6361
|
+
message: "Version restored successfully"
|
|
6362
|
+
});
|
|
5644
6363
|
return c.json({ data: doc, message: "Restored successfully" });
|
|
5645
6364
|
} catch (error) {
|
|
5646
6365
|
return c.json({ error: error.message }, 500);
|
|
@@ -5742,6 +6461,6 @@ function createRESTAPI(registry, db, options) {
|
|
|
5742
6461
|
});
|
|
5743
6462
|
}
|
|
5744
6463
|
|
|
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-
|
|
6464
|
+
export { AuditLogger, AuthRoutes, InMemoryAuditLogger, InMemoryRateLimiter, MediaService, createAuditContext2 as createAuditContext, createCloudinaryStorage, createFtpStorage, createHonoApp, createLocalStorage, createRESTAPI, createS3Storage, getAppSecret, getDefaultRegistry, getEncryptionKey, getSessionConfig, init_secret, loadSecrets, resolveProvider, setDbAdapter };
|
|
6465
|
+
//# sourceMappingURL=chunk-IPTZM3VE.js.map
|
|
6466
|
+
//# sourceMappingURL=chunk-IPTZM3VE.js.map
|