opacacms 0.1.13 → 0.1.15

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.
@@ -736,9 +736,9 @@ function createGlobalHandlers(config, globalConfig, getAuth) {
736
736
  }
737
737
 
738
738
  // src/server/router.ts
739
- import { Hono as Hono3 } from "hono";
739
+ import { Hono as Hono4 } from "hono";
740
740
 
741
- // src/server/admin-router.ts
741
+ // src/server/routers/admin.ts
742
742
  import { Hono } from "hono";
743
743
 
744
744
  // src/server/middlewares/admin.ts
@@ -759,7 +759,7 @@ var adminMiddleware = async (c, next) => {
759
759
  return c.json({ message: "Forbidden" }, 403);
760
760
  };
761
761
 
762
- // src/server/admin-router.ts
762
+ // src/server/routers/admin.ts
763
763
  function createAdminRouter(config, state) {
764
764
  const adminRouter = new Hono;
765
765
  const adminHandlers = createAdminHandlers(config, () => state.auth);
@@ -771,8 +771,259 @@ function createAdminRouter(config, state) {
771
771
  return adminRouter;
772
772
  }
773
773
 
774
- // src/server/collection-router.ts
774
+ // src/server/routers/auth.ts
775
+ import { Hono as Hono2 } from "hono";
776
+ function createAuthRouter(config, state) {
777
+ const authRouter = new Hono2;
778
+ authRouter.all("/*", async (c) => {
779
+ if (!state.auth) {
780
+ return c.json({ message: "Auth not initialized" }, 503);
781
+ }
782
+ return await state.auth.handler(c.req.raw);
783
+ });
784
+ return authRouter;
785
+ }
786
+
787
+ // src/server/routers/collections.ts
775
788
  init_system_schema();
789
+ import { Hono as Hono3 } from "hono";
790
+
791
+ // src/server/assets.ts
792
+ function createAssetsHandlers(config) {
793
+ return {
794
+ async upload(c) {
795
+ const user = c.get("user");
796
+ if (!user)
797
+ return c.json({ error: "Unauthorized" }, 401);
798
+ const bucket = c.req.query("bucket") || "default";
799
+ if (!config.storages)
800
+ return c.json({ error: "Storage not configured" }, 500);
801
+ const storageAdapter = config.storages[bucket];
802
+ if (!storageAdapter) {
803
+ return c.json({ error: `Bucket '${bucket}' not found` }, 404);
804
+ }
805
+ try {
806
+ try {
807
+ if (config.db.name === "sqlite" || config.db.name === "d1") {
808
+ const tableInfo = await config.db.unsafe(`PRAGMA table_info(_opaca_assets)`);
809
+ const columns = tableInfo.map((c2) => c2.name);
810
+ if (!columns.includes("folder"))
811
+ await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN folder TEXT`);
812
+ if (!columns.includes("alt_text"))
813
+ await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN alt_text TEXT`);
814
+ if (!columns.includes("caption"))
815
+ await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN caption TEXT`);
816
+ } else if (config.db.name === "postgres") {
817
+ const checkCols = await config.db.unsafe(`
818
+ SELECT column_name FROM information_schema.columns
819
+ WHERE table_name = '_opaca_assets' AND column_name IN ('folder', 'alt_text', 'caption')
820
+ `);
821
+ const existing = checkCols.map((c2) => c2.column_name);
822
+ if (!existing.includes("folder"))
823
+ await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN folder TEXT`);
824
+ if (!existing.includes("alt_text"))
825
+ await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN "alt_text" TEXT`);
826
+ if (!existing.includes("caption"))
827
+ await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN caption TEXT`);
828
+ }
829
+ } catch (e) {
830
+ console.error("Auto-patch columns failed", e);
831
+ }
832
+ const folder = c.req.query("folder") || null;
833
+ const keyPrefix = folder ? `${folder}/` : "";
834
+ const now = new Date().toISOString();
835
+ const formData = await c.req.parseBody({ all: true });
836
+ const fileRaw = formData["file"];
837
+ const file = Array.isArray(fileRaw) ? fileRaw[0] : fileRaw;
838
+ if (!file || typeof file !== "object" && typeof file !== "string") {
839
+ return c.json({ error: "No file provided" }, 400);
840
+ }
841
+ const fileName = file.name || "unnamed";
842
+ const fileType = file.type || "application/octet-stream";
843
+ const fileSize = file.size || 0;
844
+ const fileRecord = {
845
+ filename: fileName,
846
+ original_filename: fileName,
847
+ mime_type: fileType,
848
+ filesize: fileSize,
849
+ stream: typeof file.stream === "function" ? file.stream() : new Response(file).body
850
+ };
851
+ const uploadedFileData = await storageAdapter.upload(fileRecord, {
852
+ generateUniqueName: true,
853
+ keyPrefix
854
+ });
855
+ const storedKey = keyPrefix + uploadedFileData.filename;
856
+ try {
857
+ const assetId = (globalThis.crypto?.randomUUID?.() || Math.random().toString(36).slice(2)).replace(/-/g, "");
858
+ await config.db.create("_opaca_assets", {
859
+ id: assetId,
860
+ key: storedKey,
861
+ filename: fileName,
862
+ originalFilename: fileName,
863
+ mimeType: uploadedFileData.mime_type,
864
+ filesize: uploadedFileData.filesize,
865
+ bucket,
866
+ folder,
867
+ altText: null,
868
+ caption: null,
869
+ uploadedBy: user.id || null
870
+ });
871
+ return c.json({
872
+ assetId,
873
+ ...uploadedFileData,
874
+ key: storedKey
875
+ }, 201);
876
+ } catch (dbError) {
877
+ console.error(`[OpacaCMS] Registry insert failed, rolling back physical file upload: ${storedKey}`);
878
+ storageAdapter.delete(storedKey).catch((cleanupError) => {
879
+ console.error(`[OpacaCMS] CRITICAL: Failed to clean up orphaned file ${storedKey}!`, cleanupError);
880
+ });
881
+ throw dbError;
882
+ }
883
+ } catch (error) {
884
+ return c.json({ error: error.message }, 400);
885
+ }
886
+ },
887
+ async list(c) {
888
+ const user = c.get("user");
889
+ if (!user || user.role !== "admin" && !user.role?.includes("admin")) {
890
+ return c.json({ error: "Unauthorized" }, 401);
891
+ }
892
+ const bucket = c.req.query("bucket") || "all";
893
+ const page = parseInt(c.req.query("page") || "1", 10);
894
+ const limit = parseInt(c.req.query("limit") || "20", 10);
895
+ const offset = (page - 1) * limit;
896
+ const folder = c.req.query("folder") || null;
897
+ try {
898
+ let query = {};
899
+ if (bucket !== "all")
900
+ query.bucket = bucket;
901
+ if (folder !== null && folder !== "") {
902
+ query.folder = folder;
903
+ } else {
904
+ if (bucket !== "all") {
905
+ query = {
906
+ and: [{ bucket }, { or: [{ folder: null }, { folder: "" }] }]
907
+ };
908
+ } else {
909
+ query = { or: [{ folder: null }, { folder: "" }] };
910
+ }
911
+ }
912
+ const result = await config.db.find("_opaca_assets", query, {
913
+ page,
914
+ limit,
915
+ sort: "created_at:desc"
916
+ });
917
+ const rows = result.docs;
918
+ const total = result.totalDocs;
919
+ let folderRows = [];
920
+ const bucketFilter = bucket !== "all" ? `AND bucket = ?` : "";
921
+ const bucketParam = bucket !== "all" ? [bucket] : [];
922
+ if (config.db.name === "postgres") {
923
+ const pgBucketFilter = bucketFilter.replace("?", "$1");
924
+ if (folder === null || folder === "") {
925
+ folderRows = await config.db.unsafe(`SELECT DISTINCT split_part(folder, '/', 1) as subfolder, bucket FROM _opaca_assets WHERE folder IS NOT NULL AND folder != '' ${pgBucketFilter}`, bucketParam);
926
+ } else {
927
+ folderRows = await config.db.unsafe(`SELECT DISTINCT split_part(substring(folder from length($1) + 2), '/', 1) as subfolder, bucket FROM _opaca_assets WHERE folder LIKE $2 ${bucket !== "all" ? "AND bucket = $3" : ""}`, [folder, `${folder}/%`, ...bucketParam]);
928
+ }
929
+ } else {
930
+ if (folder === null || folder === "") {
931
+ folderRows = await config.db.unsafe(`
932
+ SELECT DISTINCT
933
+ CASE
934
+ WHEN INSTR(folder, '/') > 0 THEN SUBSTR(folder, 1, INSTR(folder, '/') - 1)
935
+ ELSE folder
936
+ END as subfolder,
937
+ bucket
938
+ FROM _opaca_assets WHERE folder IS NOT NULL AND folder != '' ${bucketFilter}
939
+ `, bucketParam);
940
+ } else {
941
+ const skipLen = folder.length + 2;
942
+ folderRows = await config.db.unsafe(`
943
+ SELECT DISTINCT
944
+ CASE
945
+ WHEN INSTR(SUBSTR(folder, ?), '/') > 0 THEN SUBSTR(SUBSTR(folder, ?), 1, INSTR(SUBSTR(folder, ?), '/') - 1)
946
+ ELSE SUBSTR(folder, ?)
947
+ END as subfolder,
948
+ bucket
949
+ FROM _opaca_assets WHERE folder LIKE ? ${bucketFilter}
950
+ `, [skipLen, skipLen, skipLen, skipLen, `${folder}/%`, ...bucketParam]);
951
+ }
952
+ }
953
+ const folderMap = {};
954
+ for (const row of folderRows) {
955
+ if (!row.subfolder)
956
+ continue;
957
+ if (!folderMap[row.subfolder])
958
+ folderMap[row.subfolder] = [];
959
+ if (!folderMap[row.subfolder]?.includes(row.bucket)) {
960
+ folderMap[row.subfolder]?.push(row.bucket);
961
+ }
962
+ }
963
+ const folders = Object.entries(folderMap).map(([name, buckets]) => ({
964
+ name,
965
+ buckets
966
+ }));
967
+ return c.json({
968
+ docs: rows,
969
+ folders,
970
+ totalDocs: total,
971
+ limit,
972
+ page,
973
+ totalPages: Math.ceil(total / limit)
974
+ });
975
+ } catch (e) {
976
+ return c.json({ error: e.message }, 500);
977
+ }
978
+ },
979
+ async presign(c) {
980
+ const user = c.get("user");
981
+ if (!user)
982
+ return c.json({ error: "Unauthorized" }, 401);
983
+ const { filename, bucket = "default", operation = "write" } = await c.req.json();
984
+ if (!config.storages || !config.storages[bucket]) {
985
+ return c.json({ error: "Bucket not found" }, 404);
986
+ }
987
+ const adapter = config.storages[bucket];
988
+ if (!adapter.generatePresignedUrl) {
989
+ return c.json({ error: "Adapter does not support presigned URLs" }, 400);
990
+ }
991
+ try {
992
+ const url = await adapter.generatePresignedUrl(filename, operation, 3600);
993
+ return c.json({ uploadUrl: url, filename });
994
+ } catch (e) {
995
+ return c.json({ error: e.message }, 500);
996
+ }
997
+ },
998
+ async serve(c) {
999
+ const id = c.req.param("id");
1000
+ try {
1001
+ const asset = await config.db.findOne("_opaca_assets", { id });
1002
+ if (!asset) {
1003
+ return c.json({ error: "Asset not found" }, 404);
1004
+ }
1005
+ const bucket = asset.bucket || "default";
1006
+ if (!config.storages || !config.storages[bucket]) {
1007
+ return c.json({ error: "Storage bucket not configured" }, 500);
1008
+ }
1009
+ const adapter = config.storages[bucket];
1010
+ if (!adapter.download) {
1011
+ return c.json({ error: "Storage adapter does not support direct downloads" }, 400);
1012
+ }
1013
+ const stream = await adapter.download(asset.key || asset.filename);
1014
+ c.header("Content-Type", asset.mimeType || "application/octet-stream");
1015
+ c.header("Content-Length", asset.filesize.toString());
1016
+ c.header("Cache-Control", "public, max-age=86400");
1017
+ return c.body(stream);
1018
+ } catch (e) {
1019
+ console.error(`[OpacaCMS] Failed to serve asset ${id}:`, e);
1020
+ return c.json({ error: e.message }, 500);
1021
+ }
1022
+ }
1023
+ };
1024
+ }
1025
+
1026
+ // src/server/routers/collections.ts
776
1027
  function mountCollectionRoutes(router, config, state) {
777
1028
  const combinedCollections = [...config.collections];
778
1029
  for (const systemCol of getSystemCollections()) {
@@ -804,18 +1055,38 @@ function mountGlobalRoutes(router, config, state) {
804
1055
  }
805
1056
  }
806
1057
  }
807
-
808
- // src/server/middlewares/auth.ts
809
- function createAuthMiddleware(getAuth) {
810
- return async (c, next) => {
811
- const auth = getAuth();
812
- if (!auth) {
813
- c.set("user", null);
814
- c.set("session", null);
815
- c.set("apiKey", null);
816
- await next();
817
- return;
818
- }
1058
+ function createSystemRouter(config) {
1059
+ const systemRouter = new Hono3;
1060
+ if (config.storages) {
1061
+ const assetsHandlers = createAssetsHandlers(config);
1062
+ systemRouter.post("/assets/upload", adminMiddleware, assetsHandlers.upload);
1063
+ systemRouter.get("/assets", adminMiddleware, assetsHandlers.list);
1064
+ systemRouter.post("/assets/presign-upload", adminMiddleware, assetsHandlers.presign);
1065
+ }
1066
+ return systemRouter;
1067
+ }
1068
+ function createAssetsServingRouter(config) {
1069
+ const assetsServingRouter = new Hono3;
1070
+ if (config.storages) {
1071
+ const assetsHandlers = createAssetsHandlers(config);
1072
+ const assetCol = getSystemCollections().find((c) => c.slug === "_opaca_assets");
1073
+ const assetPath = `/${assetCol?.apiPath || assetCol?.slug || "_opaca_assets"}`;
1074
+ assetsServingRouter.get(`${assetPath}/:id/view`, assetsHandlers.serve);
1075
+ }
1076
+ return assetsServingRouter;
1077
+ }
1078
+
1079
+ // src/server/middlewares/auth.ts
1080
+ function createAuthMiddleware(getAuth) {
1081
+ return async (c, next) => {
1082
+ const auth = getAuth();
1083
+ if (!auth) {
1084
+ c.set("user", null);
1085
+ c.set("session", null);
1086
+ c.set("apiKey", null);
1087
+ await next();
1088
+ return;
1089
+ }
819
1090
  const session = await auth.api.getSession({ headers: c.req.raw.headers });
820
1091
  if (session) {
821
1092
  c.set("user", session.user);
@@ -1064,288 +1335,19 @@ function setupMiddlewares(router, config, state) {
1064
1335
  });
1065
1336
  }
1066
1337
  function setupAuthMiddlewares(router, config, state) {
1067
- const supportsAuth = config.db.name === "sqlite" || config.db.name === "postgres" || config.db.name === "d1";
1068
- if (supportsAuth) {
1069
- router.use("*", createAuthMiddleware(() => state.auth));
1070
- router.on(["POST", "GET"], ["/auth/*"], async (c) => {
1071
- if (!state.auth) {
1072
- return c.json({ message: "Auth not initialized" }, 503);
1073
- }
1074
- return await state.auth.handler(c.req.raw);
1075
- });
1076
- }
1077
- }
1078
-
1079
- // src/server/system-router.ts
1080
- init_system_schema();
1081
- import { Hono as Hono2 } from "hono";
1082
-
1083
- // src/server/assets.ts
1084
- function createAssetsHandlers(config) {
1085
- return {
1086
- async upload(c) {
1087
- const user = c.get("user");
1088
- if (!user)
1089
- return c.json({ error: "Unauthorized" }, 401);
1090
- const bucket = c.req.query("bucket") || "default";
1091
- if (!config.storages)
1092
- return c.json({ error: "Storage not configured" }, 500);
1093
- const storageAdapter = config.storages[bucket];
1094
- if (!storageAdapter) {
1095
- return c.json({ error: `Bucket '${bucket}' not found` }, 404);
1096
- }
1097
- try {
1098
- try {
1099
- if (config.db.name === "sqlite" || config.db.name === "d1") {
1100
- const tableInfo = await config.db.unsafe(`PRAGMA table_info(_opaca_assets)`);
1101
- const columns = tableInfo.map((c2) => c2.name);
1102
- if (!columns.includes("folder"))
1103
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN folder TEXT`);
1104
- if (!columns.includes("alt_text"))
1105
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN alt_text TEXT`);
1106
- if (!columns.includes("caption"))
1107
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN caption TEXT`);
1108
- } else if (config.db.name === "postgres") {
1109
- const checkCols = await config.db.unsafe(`
1110
- SELECT column_name FROM information_schema.columns
1111
- WHERE table_name = '_opaca_assets' AND column_name IN ('folder', 'alt_text', 'caption')
1112
- `);
1113
- const existing = checkCols.map((c2) => c2.column_name);
1114
- if (!existing.includes("folder"))
1115
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN folder TEXT`);
1116
- if (!existing.includes("alt_text"))
1117
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN "alt_text" TEXT`);
1118
- if (!existing.includes("caption"))
1119
- await config.db.unsafe(`ALTER TABLE _opaca_assets ADD COLUMN caption TEXT`);
1120
- }
1121
- } catch (e) {
1122
- console.error("Auto-patch columns failed", e);
1123
- }
1124
- const folder = c.req.query("folder") || null;
1125
- const keyPrefix = folder ? `${folder}/` : "";
1126
- const now = new Date().toISOString();
1127
- const formData = await c.req.parseBody({ all: true });
1128
- const fileRaw = formData["file"];
1129
- const file = Array.isArray(fileRaw) ? fileRaw[0] : fileRaw;
1130
- if (!file || typeof file !== "object" && typeof file !== "string") {
1131
- return c.json({ error: "No file provided" }, 400);
1132
- }
1133
- const fileName = file.name || "unnamed";
1134
- const fileType = file.type || "application/octet-stream";
1135
- const fileSize = file.size || 0;
1136
- const fileRecord = {
1137
- filename: fileName,
1138
- original_filename: fileName,
1139
- mime_type: fileType,
1140
- filesize: fileSize,
1141
- stream: typeof file.stream === "function" ? file.stream() : new Response(file).body
1142
- };
1143
- const uploadedFileData = await storageAdapter.upload(fileRecord, {
1144
- generateUniqueName: true,
1145
- keyPrefix
1146
- });
1147
- const storedKey = keyPrefix + uploadedFileData.filename;
1148
- try {
1149
- const assetId = (globalThis.crypto?.randomUUID?.() || Math.random().toString(36).slice(2)).replace(/-/g, "");
1150
- await config.db.create("_opaca_assets", {
1151
- id: assetId,
1152
- key: storedKey,
1153
- filename: fileName,
1154
- originalFilename: fileName,
1155
- mimeType: uploadedFileData.mime_type,
1156
- filesize: uploadedFileData.filesize,
1157
- bucket,
1158
- folder,
1159
- altText: null,
1160
- caption: null,
1161
- uploadedBy: user.id || null
1162
- });
1163
- return c.json({
1164
- assetId,
1165
- ...uploadedFileData,
1166
- key: storedKey
1167
- }, 201);
1168
- } catch (dbError) {
1169
- console.error(`[OpacaCMS] Registry insert failed, rolling back physical file upload: ${storedKey}`);
1170
- storageAdapter.delete(storedKey).catch((cleanupError) => {
1171
- console.error(`[OpacaCMS] CRITICAL: Failed to clean up orphaned file ${storedKey}!`, cleanupError);
1172
- });
1173
- throw dbError;
1174
- }
1175
- } catch (error) {
1176
- return c.json({ error: error.message }, 400);
1177
- }
1178
- },
1179
- async list(c) {
1180
- const user = c.get("user");
1181
- if (!user || user.role !== "admin" && !user.role?.includes("admin")) {
1182
- return c.json({ error: "Unauthorized" }, 401);
1183
- }
1184
- const bucket = c.req.query("bucket") || "all";
1185
- const page = parseInt(c.req.query("page") || "1", 10);
1186
- const limit = parseInt(c.req.query("limit") || "20", 10);
1187
- const offset = (page - 1) * limit;
1188
- const folder = c.req.query("folder") || null;
1189
- try {
1190
- let query = {};
1191
- if (bucket !== "all")
1192
- query.bucket = bucket;
1193
- if (folder !== null && folder !== "") {
1194
- query.folder = folder;
1195
- } else {
1196
- if (bucket !== "all") {
1197
- query = {
1198
- and: [{ bucket }, { or: [{ folder: null }, { folder: "" }] }]
1199
- };
1200
- } else {
1201
- query = { or: [{ folder: null }, { folder: "" }] };
1202
- }
1203
- }
1204
- const result = await config.db.find("_opaca_assets", query, {
1205
- page,
1206
- limit,
1207
- sort: "created_at:desc"
1208
- });
1209
- const rows = result.docs;
1210
- const total = result.totalDocs;
1211
- let folderRows = [];
1212
- const bucketFilter = bucket !== "all" ? `AND bucket = ?` : "";
1213
- const bucketParam = bucket !== "all" ? [bucket] : [];
1214
- if (config.db.name === "postgres") {
1215
- const pgBucketFilter = bucketFilter.replace("?", "$1");
1216
- if (folder === null || folder === "") {
1217
- folderRows = await config.db.unsafe(`SELECT DISTINCT split_part(folder, '/', 1) as subfolder, bucket FROM _opaca_assets WHERE folder IS NOT NULL AND folder != '' ${pgBucketFilter}`, bucketParam);
1218
- } else {
1219
- folderRows = await config.db.unsafe(`SELECT DISTINCT split_part(substring(folder from length($1) + 2), '/', 1) as subfolder, bucket FROM _opaca_assets WHERE folder LIKE $2 ${bucket !== "all" ? "AND bucket = $3" : ""}`, [folder, `${folder}/%`, ...bucketParam]);
1220
- }
1221
- } else {
1222
- if (folder === null || folder === "") {
1223
- folderRows = await config.db.unsafe(`
1224
- SELECT DISTINCT
1225
- CASE
1226
- WHEN INSTR(folder, '/') > 0 THEN SUBSTR(folder, 1, INSTR(folder, '/') - 1)
1227
- ELSE folder
1228
- END as subfolder,
1229
- bucket
1230
- FROM _opaca_assets WHERE folder IS NOT NULL AND folder != '' ${bucketFilter}
1231
- `, bucketParam);
1232
- } else {
1233
- const skipLen = folder.length + 2;
1234
- folderRows = await config.db.unsafe(`
1235
- SELECT DISTINCT
1236
- CASE
1237
- WHEN INSTR(SUBSTR(folder, ?), '/') > 0 THEN SUBSTR(SUBSTR(folder, ?), 1, INSTR(SUBSTR(folder, ?), '/') - 1)
1238
- ELSE SUBSTR(folder, ?)
1239
- END as subfolder,
1240
- bucket
1241
- FROM _opaca_assets WHERE folder LIKE ? ${bucketFilter}
1242
- `, [skipLen, skipLen, skipLen, skipLen, `${folder}/%`, ...bucketParam]);
1243
- }
1244
- }
1245
- const folderMap = {};
1246
- for (const row of folderRows) {
1247
- if (!row.subfolder)
1248
- continue;
1249
- if (!folderMap[row.subfolder])
1250
- folderMap[row.subfolder] = [];
1251
- if (!folderMap[row.subfolder]?.includes(row.bucket)) {
1252
- folderMap[row.subfolder]?.push(row.bucket);
1253
- }
1254
- }
1255
- const folders = Object.entries(folderMap).map(([name, buckets]) => ({
1256
- name,
1257
- buckets
1258
- }));
1259
- return c.json({
1260
- docs: rows,
1261
- folders,
1262
- totalDocs: total,
1263
- limit,
1264
- page,
1265
- totalPages: Math.ceil(total / limit)
1266
- });
1267
- } catch (e) {
1268
- return c.json({ error: e.message }, 500);
1269
- }
1270
- },
1271
- async presign(c) {
1272
- const user = c.get("user");
1273
- if (!user)
1274
- return c.json({ error: "Unauthorized" }, 401);
1275
- const { filename, bucket = "default", operation = "write" } = await c.req.json();
1276
- if (!config.storages || !config.storages[bucket]) {
1277
- return c.json({ error: "Bucket not found" }, 404);
1278
- }
1279
- const adapter = config.storages[bucket];
1280
- if (!adapter.generatePresignedUrl) {
1281
- return c.json({ error: "Adapter does not support presigned URLs" }, 400);
1282
- }
1283
- try {
1284
- const url = await adapter.generatePresignedUrl(filename, operation, 3600);
1285
- return c.json({ uploadUrl: url, filename });
1286
- } catch (e) {
1287
- return c.json({ error: e.message }, 500);
1288
- }
1289
- },
1290
- async serve(c) {
1291
- const id = c.req.param("id");
1292
- try {
1293
- const asset = await config.db.findOne("_opaca_assets", { id });
1294
- if (!asset) {
1295
- return c.json({ error: "Asset not found" }, 404);
1296
- }
1297
- const bucket = asset.bucket || "default";
1298
- if (!config.storages || !config.storages[bucket]) {
1299
- return c.json({ error: "Storage bucket not configured" }, 500);
1300
- }
1301
- const adapter = config.storages[bucket];
1302
- if (!adapter.download) {
1303
- return c.json({ error: "Storage adapter does not support direct downloads" }, 400);
1304
- }
1305
- const stream = await adapter.download(asset.key || asset.filename);
1306
- c.header("Content-Type", asset.mimeType || "application/octet-stream");
1307
- c.header("Content-Length", asset.filesize.toString());
1308
- c.header("Cache-Control", "public, max-age=86400");
1309
- return c.body(stream);
1310
- } catch (e) {
1311
- console.error(`[OpacaCMS] Failed to serve asset ${id}:`, e);
1312
- return c.json({ error: e.message }, 500);
1313
- }
1314
- }
1315
- };
1316
- }
1317
-
1318
- // src/server/system-router.ts
1319
- function createSystemRouter(config) {
1320
- const systemRouter = new Hono2;
1321
- if (config.storages) {
1322
- const assetsHandlers = createAssetsHandlers(config);
1323
- systemRouter.post("/assets/upload", adminMiddleware, assetsHandlers.upload);
1324
- systemRouter.get("/assets", adminMiddleware, assetsHandlers.list);
1325
- systemRouter.post("/assets/presign-upload", adminMiddleware, assetsHandlers.presign);
1326
- }
1327
- return systemRouter;
1328
- }
1329
- function createAssetsServingRouter(config) {
1330
- const assetsServingRouter = new Hono2;
1331
- if (config.storages) {
1332
- const assetsHandlers = createAssetsHandlers(config);
1333
- const assetCol = getSystemCollections().find((c) => c.slug === "_opaca_assets");
1334
- const assetPath = `/${assetCol?.apiPath || assetCol?.slug || "_opaca_assets"}`;
1335
- assetsServingRouter.get(`${assetPath}/:id/view`, assetsHandlers.serve);
1336
- }
1337
- return assetsServingRouter;
1338
+ router.use("*", createAuthMiddleware(() => state.auth));
1338
1339
  }
1339
1340
 
1340
1341
  // src/server/router.ts
1341
1342
  function createAPIRouter(config) {
1342
1343
  const state = { auth: undefined, migrated: false };
1343
- const router = new Hono3().basePath("/api");
1344
+ const router = new Hono4().basePath("/api");
1344
1345
  setupMiddlewares(router, config, state);
1345
1346
  setupAuthMiddlewares(router, config, state);
1346
1347
  router.get("/", (c) => {
1347
1348
  return c.json({ status: "ok", version: "1.0.0", appName: config.appName });
1348
1349
  });
1350
+ router.route("/auth", createAuthRouter(config, state));
1349
1351
  router.route("/__admin", createAdminRouter(config, state));
1350
1352
  router.route("/__system", createSystemRouter(config));
1351
1353
  router.route("/", createAssetsServingRouter(config));
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createAPIRouter
3
- } from "../chunk-0d9aqz6z.js";
3
+ } from "../chunk-5ftxp8m2.js";
4
4
  import"../chunk-dy5t83hr.js";
5
5
  import"../chunk-62ev8gnc.js";
6
6
  import"../chunk-2kyhqvhc.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createAPIRouter
3
- } from "../chunk-0d9aqz6z.js";
3
+ } from "../chunk-5ftxp8m2.js";
4
4
  import"../chunk-dy5t83hr.js";
5
5
  import"../chunk-62ev8gnc.js";
6
6
  import"../chunk-2kyhqvhc.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createAPIRouter
3
- } from "../chunk-0d9aqz6z.js";
3
+ } from "../chunk-5ftxp8m2.js";
4
4
  import"../chunk-dy5t83hr.js";
5
5
  import"../chunk-62ev8gnc.js";
6
6
  import"../chunk-2kyhqvhc.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createAPIRouter
3
- } from "../chunk-0d9aqz6z.js";
3
+ } from "../chunk-5ftxp8m2.js";
4
4
  import"../chunk-dy5t83hr.js";
5
5
  import"../chunk-62ev8gnc.js";
6
6
  import"../chunk-2kyhqvhc.js";
@@ -0,0 +1,9 @@
1
+ import { Hono } from "hono";
2
+ import type { Auth } from "../../auth";
3
+ import type { OpacaConfig } from "../../types";
4
+ import type { ApiContextVariables } from "../router";
5
+ export declare function createAdminRouter(config: OpacaConfig, state: {
6
+ auth: Auth | undefined;
7
+ }): Hono<{
8
+ Variables: ApiContextVariables;
9
+ }, import("hono/types").BlankSchema, "/">;
@@ -0,0 +1,9 @@
1
+ import { Hono } from "hono";
2
+ import type { Auth } from "../../auth";
3
+ import type { OpacaConfig } from "../../types";
4
+ import type { ApiContextVariables } from "../router";
5
+ export declare function createAuthRouter(config: OpacaConfig, state: {
6
+ auth: Auth | undefined;
7
+ }): Hono<{
8
+ Variables: ApiContextVariables;
9
+ }, import("hono/types").BlankSchema, "/">;