@infuro/cms-core 1.0.15 → 1.0.18

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.
Files changed (52) hide show
  1. package/README.md +739 -724
  2. package/dist/admin.cjs +1840 -741
  3. package/dist/admin.cjs.map +1 -1
  4. package/dist/admin.d.cts +4 -0
  5. package/dist/admin.d.ts +4 -0
  6. package/dist/admin.js +1795 -681
  7. package/dist/admin.js.map +1 -1
  8. package/dist/api.cjs +700 -77
  9. package/dist/api.cjs.map +1 -1
  10. package/dist/api.d.cts +1 -1
  11. package/dist/api.d.ts +1 -1
  12. package/dist/api.js +696 -75
  13. package/dist/api.js.map +1 -1
  14. package/dist/auth.cjs.map +1 -1
  15. package/dist/auth.js.map +1 -1
  16. package/dist/cli.cjs +21 -6
  17. package/dist/cli.cjs.map +1 -1
  18. package/dist/cli.js +21 -6
  19. package/dist/cli.js.map +1 -1
  20. package/dist/hooks.cjs +159 -0
  21. package/dist/hooks.cjs.map +1 -1
  22. package/dist/hooks.d.cts +24 -1
  23. package/dist/hooks.d.ts +24 -1
  24. package/dist/hooks.js +165 -0
  25. package/dist/hooks.js.map +1 -1
  26. package/dist/{index-BQnqJ7EO.d.cts → index-D2C1O9b4.d.cts} +22 -3
  27. package/dist/{index-BiagwMjV.d.ts → index-GMn7-9PX.d.ts} +22 -3
  28. package/dist/index.cjs +5334 -4336
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +89 -11
  31. package/dist/index.d.ts +89 -11
  32. package/dist/index.js +5333 -4352
  33. package/dist/index.js.map +1 -1
  34. package/dist/migrations/1772178563554-InitialSchema.ts +304 -304
  35. package/dist/migrations/1772178563555-ChatAndKnowledgeBase.ts +55 -55
  36. package/dist/migrations/1772178563556-KnowledgeBaseVector.ts +16 -16
  37. package/dist/migrations/1774300000000-RbacSeedGroupsAndPermissionUnique.ts +24 -24
  38. package/dist/migrations/1774300000001-SeedAdministratorUsersPermission.ts +35 -35
  39. package/dist/migrations/1774400000000-CustomerAdminAccessContactUser.ts +37 -37
  40. package/dist/migrations/1774400000001-StorefrontCartWishlist.ts +100 -100
  41. package/dist/migrations/1774400000002-WishlistGuestId.ts +29 -29
  42. package/dist/migrations/1774500000000-ProductCollectionHsn.ts +15 -15
  43. package/dist/migrations/1774600000000-OrderKindParentOrderNumber.ts +36 -36
  44. package/dist/migrations/1774800000000-OtpChallengesUserPhone.ts +41 -41
  45. package/dist/migrations/1774900000000-MessageTemplates.ts +39 -39
  46. package/dist/migrations/1775000000000-ProductUomTypeOrderItemSnapshots.ts +29 -29
  47. package/dist/migrations/1775200000000-MediaDriveFolders.ts +38 -0
  48. package/dist/migrations/README.md +3 -3
  49. package/dist/theme.cjs.map +1 -1
  50. package/dist/theme.js.map +1 -1
  51. package/package.json +20 -15
  52. package/src/admin/admin.css +72 -72
package/dist/api.cjs CHANGED
@@ -428,9 +428,11 @@ __export(api_exports, {
428
428
  createCrudByIdHandler: () => createCrudByIdHandler,
429
429
  createCrudHandler: () => createCrudHandler,
430
430
  createDashboardStatsHandler: () => createDashboardStatsHandler,
431
+ createEcommerceAnalyticsHandler: () => createEcommerceAnalyticsHandler,
431
432
  createForgotPasswordHandler: () => createForgotPasswordHandler,
432
433
  createFormBySlugHandler: () => createFormBySlugHandler,
433
434
  createInviteAcceptHandler: () => createInviteAcceptHandler,
435
+ createMediaZipExtractHandler: () => createMediaZipExtractHandler,
434
436
  createSetPasswordHandler: () => createSetPasswordHandler,
435
437
  createSettingsApiHandlers: () => createSettingsApiHandlers,
436
438
  createStorefrontApiHandler: () => createStorefrontApiHandler,
@@ -567,6 +569,28 @@ function buildSearchWhereClause(repo, search) {
567
569
  if (ors.length === 0) return {};
568
570
  return ors.length === 1 ? ors[0] : ors;
569
571
  }
572
+ function entityHasSoftDelete(repo) {
573
+ return repo.metadata.columns.some((c) => c.propertyName === "deleted");
574
+ }
575
+ function mergeDeletedFalseWhere(repo, where) {
576
+ if (!entityHasSoftDelete(repo)) return where;
577
+ const d = { deleted: false };
578
+ if (Array.isArray(where)) {
579
+ if (where.length === 0) return [d];
580
+ return where.map((w) => ({ ...w, ...d }));
581
+ }
582
+ return Object.keys(where).length > 0 ? { ...where, ...d } : d;
583
+ }
584
+ function buildSoftDeletePayload(meta, deletedBy) {
585
+ const payload = { deleted: true };
586
+ if (meta.columns.some((c) => c.propertyName === "deletedAt")) {
587
+ payload.deletedAt = /* @__PURE__ */ new Date();
588
+ }
589
+ if (deletedBy != null && meta.columns.some((c) => c.propertyName === "deletedBy")) {
590
+ payload.deletedBy = deletedBy;
591
+ }
592
+ return payload;
593
+ }
570
594
  function makeContactErpSync(dataSource, entityMap, getCms) {
571
595
  return async function syncContactRowToErp(row) {
572
596
  if (!getCms) return;
@@ -591,10 +615,11 @@ function createCrudHandler(dataSource, entityMap, options) {
591
615
  async function authz(req, resource, action) {
592
616
  const authError = await requireAuth(req);
593
617
  if (authError) return authError;
594
- if (reqPerm) {
595
- const pe = await reqPerm(req, resource, action);
596
- if (pe) return pe;
618
+ if (!reqPerm) {
619
+ return json({ error: "Forbidden", reason: "entity_rbac_required", entity: resource, action }, { status: 403 });
597
620
  }
621
+ const pe = await reqPerm(req, resource, action);
622
+ if (pe) return pe;
598
623
  return null;
599
624
  }
600
625
  return {
@@ -630,7 +655,7 @@ function createCrudHandler(dataSource, entityMap, options) {
630
655
  return json({ total: 0, page, limit, totalPages: 0, data: [] });
631
656
  }
632
657
  }
633
- const qb = repo2.createQueryBuilder("order").leftJoinAndSelect("order.contact", "contact").leftJoinAndSelect("order.items", "items").leftJoinAndSelect("items.product", "product").leftJoinAndSelect("product.collection", "collection").orderBy(`order.${sortField2}`, sortOrderOrders).skip(skip).take(limit);
658
+ const qb = repo2.createQueryBuilder("order").leftJoinAndSelect("order.contact", "contact").leftJoinAndSelect("order.items", "items").leftJoinAndSelect("items.product", "product").leftJoinAndSelect("product.collection", "collection").andWhere("order.deleted = :orderDel", { orderDel: false }).orderBy(`order.${sortField2}`, sortOrderOrders).skip(skip).take(limit);
634
659
  if (search && typeof search === "string" && search.trim()) {
635
660
  const term = `%${search.trim()}%`;
636
661
  qb.andWhere(
@@ -668,7 +693,7 @@ function createCrudHandler(dataSource, entityMap, options) {
668
693
  const dateTo = searchParams.get("dateTo")?.trim();
669
694
  const methodFilter = searchParams.get("method")?.trim();
670
695
  const orderNumberParam = searchParams.get("orderNumber")?.trim();
671
- const qb = repo2.createQueryBuilder("payment").leftJoinAndSelect("payment.order", "ord").leftJoinAndSelect("ord.contact", "orderContact").leftJoinAndSelect("payment.contact", "contact").orderBy(`payment.${sortField2}`, sortOrderPayments).skip(skip).take(limit);
696
+ const qb = repo2.createQueryBuilder("payment").leftJoinAndSelect("payment.order", "ord").leftJoinAndSelect("ord.contact", "orderContact").leftJoinAndSelect("payment.contact", "contact").andWhere("payment.deleted = :payDel", { payDel: false }).orderBy(`payment.${sortField2}`, sortOrderPayments).skip(skip).take(limit);
672
697
  if (search && typeof search === "string" && search.trim()) {
673
698
  const term = `%${search.trim()}%`;
674
699
  qb.andWhere(
@@ -699,7 +724,7 @@ function createCrudHandler(dataSource, entityMap, options) {
699
724
  const repo2 = dataSource.getRepository(entity);
700
725
  const statusFilter = searchParams.get("status")?.trim();
701
726
  const inventory = searchParams.get("inventory")?.trim();
702
- const productWhere = {};
727
+ const productWhere = { deleted: false };
703
728
  if (statusFilter) productWhere.status = statusFilter;
704
729
  if (inventory === "in_stock") productWhere.quantity = (0, import_typeorm.MoreThan)(0);
705
730
  if (inventory === "out_of_stock") productWhere.quantity = 0;
@@ -722,7 +747,7 @@ function createCrudHandler(dataSource, entityMap, options) {
722
747
  const typeFilter2 = searchParams.get("type")?.trim();
723
748
  const orderIdParam = searchParams.get("orderId")?.trim();
724
749
  const includeSummary = searchParams.get("includeSummary") === "1";
725
- const qb = repo2.createQueryBuilder("contact").orderBy(`contact.${sortField2}`, sortOrderContacts).skip(skip).take(limit);
750
+ const qb = repo2.createQueryBuilder("contact").andWhere("contact.deleted = :contactDel", { contactDel: false }).orderBy(`contact.${sortField2}`, sortOrderContacts).skip(skip).take(limit);
726
751
  if (search && typeof search === "string" && search.trim()) {
727
752
  const term = `%${search.trim()}%`;
728
753
  qb.andWhere("(contact.name ILIKE :term OR contact.email ILIKE :term OR contact.phone ILIKE :term)", { term });
@@ -757,14 +782,38 @@ function createCrudHandler(dataSource, entityMap, options) {
757
782
  const repo = dataSource.getRepository(entity);
758
783
  const typeFilter = searchParams.get("type");
759
784
  const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
785
+ if (resource === "media") {
786
+ const qb = repo.createQueryBuilder("m");
787
+ const parentIdParam = searchParams.get("parentId");
788
+ if (parentIdParam != null && parentIdParam !== "") {
789
+ const n = Number(parentIdParam);
790
+ if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
791
+ qb.where("m.deleted = :mediaDel AND m.parentId = :pid", { mediaDel: false, pid: n });
792
+ } else {
793
+ qb.where("m.deleted = :mediaDel AND m.parentId IS NULL", { mediaDel: false });
794
+ }
795
+ if (search && typeof search === "string" && search.trim()) {
796
+ qb.andWhere("m.filename ILIKE :search", { search: `%${search.trim()}%` });
797
+ }
798
+ if (typeFilter) {
799
+ qb.andWhere(
800
+ new import_typeorm.Brackets((sq) => {
801
+ sq.where("m.kind = :folderKind", { folderKind: "folder" }).orWhere("m.mimeType LIKE :mtp", {
802
+ mtp: `${typeFilter}/%`
803
+ });
804
+ })
805
+ );
806
+ }
807
+ const allowedSort = ["filename", "createdAt", "id"];
808
+ const sf = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "filename";
809
+ const so = sortOrder === "DESC" ? "DESC" : "ASC";
810
+ qb.orderBy("CASE WHEN m.kind = :fk THEN 0 ELSE 1 END", "ASC").addOrderBy(`m.${sf}`, so).setParameter("fk", "folder").skip(skip).take(limit);
811
+ const [data2, total2] = await qb.getManyAndCount();
812
+ return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
813
+ }
760
814
  const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
761
815
  let where = {};
762
- if (resource === "media") {
763
- const mediaWhere = {};
764
- if (search) mediaWhere.filename = (0, import_typeorm.ILike)(`%${search}%`);
765
- if (typeFilter) mediaWhere.mimeType = (0, import_typeorm.Like)(`${typeFilter}/%`);
766
- where = Object.keys(mediaWhere).length > 0 ? mediaWhere : {};
767
- } else if (search) {
816
+ if (search) {
768
817
  where = buildSearchWhereClause(repo, search);
769
818
  }
770
819
  const intFilterKeys = ["productId", "attributeId", "taxId"];
@@ -785,6 +834,7 @@ function createCrudHandler(dataSource, entityMap, options) {
785
834
  where = extraWhere;
786
835
  }
787
836
  }
837
+ where = mergeDeletedFalseWhere(repo, where);
788
838
  const [data, total] = await repo.findAndCount({
789
839
  skip,
790
840
  take: limit,
@@ -804,6 +854,38 @@ function createCrudHandler(dataSource, entityMap, options) {
804
854
  if (!body || typeof body !== "object" || Object.keys(body).length === 0) {
805
855
  return json({ error: "Invalid request payload" }, { status: 400 });
806
856
  }
857
+ if (resource === "media") {
858
+ const b = body;
859
+ const kind = b.kind === "folder" ? "folder" : "file";
860
+ b.kind = kind;
861
+ const fn = String(b.filename ?? "").trim().slice(0, 255);
862
+ if (!fn) return json({ error: "filename required" }, { status: 400 });
863
+ b.filename = fn;
864
+ let pid = null;
865
+ if (b.parentId != null && b.parentId !== "") {
866
+ const n = Number(b.parentId);
867
+ if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
868
+ pid = n;
869
+ }
870
+ b.parentId = pid;
871
+ const mediaRepo = dataSource.getRepository(entityMap.media);
872
+ if (pid != null) {
873
+ const parent = await mediaRepo.findOne({ where: { id: pid } });
874
+ if (!parent || parent.kind !== "folder") {
875
+ return json({ error: "parent must be a folder" }, { status: 400 });
876
+ }
877
+ }
878
+ if (kind === "folder") {
879
+ b.url = null;
880
+ b.mimeType = "inode/directory";
881
+ b.size = 0;
882
+ } else {
883
+ if (!b.url || typeof b.url !== "string") return json({ error: "url required for files" }, { status: 400 });
884
+ if (!b.mimeType || typeof b.mimeType !== "string") {
885
+ b.mimeType = "application/octet-stream";
886
+ }
887
+ }
888
+ }
807
889
  const repo = dataSource.getRepository(entity);
808
890
  sanitizeBodyForEntity(repo, body);
809
891
  const created = await repo.save(repo.create(body));
@@ -920,15 +1002,16 @@ function createCrudHandler(dataSource, entityMap, options) {
920
1002
  };
921
1003
  }
922
1004
  function createCrudByIdHandler(dataSource, entityMap, options) {
923
- const { requireAuth, json, requireEntityPermission: reqPerm, getCms } = options;
1005
+ const { requireAuth, json, requireEntityPermission: reqPerm, getCms, getDeletedByUserId } = options;
924
1006
  const syncContactRowToErp = makeContactErpSync(dataSource, entityMap, getCms);
925
1007
  async function authz(req, resource, action) {
926
1008
  const authError = await requireAuth(req);
927
1009
  if (authError) return authError;
928
- if (reqPerm) {
929
- const pe = await reqPerm(req, resource, action);
930
- if (pe) return pe;
1010
+ if (!reqPerm) {
1011
+ return json({ error: "Forbidden", reason: "entity_rbac_required", entity: resource, action }, { status: 403 });
931
1012
  }
1013
+ const pe = await reqPerm(req, resource, action);
1014
+ if (pe) return pe;
932
1015
  return null;
933
1016
  }
934
1017
  return {
@@ -940,7 +1023,7 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
940
1023
  const repo = dataSource.getRepository(entity);
941
1024
  if (resource === "orders") {
942
1025
  const order = await repo.findOne({
943
- where: { id: Number(id) },
1026
+ where: { id: Number(id), deleted: false },
944
1027
  relations: ["contact", "billingAddress", "shippingAddress", "items", "items.product", "items.product.collection", "payments"]
945
1028
  });
946
1029
  if (!order) return json({ message: "Not found" }, { status: 404 });
@@ -952,7 +1035,7 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
952
1035
  }
953
1036
  if (resource === "contacts") {
954
1037
  const contact = await repo.findOne({
955
- where: { id: Number(id) },
1038
+ where: { id: Number(id), deleted: false },
956
1039
  relations: ["form_submissions", "form_submissions.form", "orders", "payments", "addresses"]
957
1040
  });
958
1041
  if (!contact) return json({ message: "Not found" }, { status: 404 });
@@ -974,7 +1057,7 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
974
1057
  }
975
1058
  if (resource === "payments") {
976
1059
  const payment = await repo.findOne({
977
- where: { id: Number(id) },
1060
+ where: { id: Number(id), deleted: false },
978
1061
  relations: ["order", "order.contact", "contact"]
979
1062
  });
980
1063
  if (!payment) return json({ message: "Not found" }, { status: 404 });
@@ -982,12 +1065,13 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
982
1065
  }
983
1066
  if (resource === "blogs") {
984
1067
  const blog = await repo.findOne({
985
- where: { id: Number(id) },
1068
+ where: { id: Number(id), deleted: false },
986
1069
  relations: ["category", "seo", "tags"]
987
1070
  });
988
1071
  return blog ? json(blog) : json({ message: "Not found" }, { status: 404 });
989
1072
  }
990
- const item = await repo.findOne({ where: { id: Number(id) } });
1073
+ const idWhere = entityHasSoftDelete(repo) ? { id: Number(id), deleted: false } : { id: Number(id) };
1074
+ const item = await repo.findOne({ where: idWhere });
991
1075
  return item ? json(item) : json({ message: "Not found" }, { status: 404 });
992
1076
  },
993
1077
  async PUT(req, resource, id) {
@@ -999,7 +1083,9 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
999
1083
  const repo = dataSource.getRepository(entity);
1000
1084
  const numericId = Number(id);
1001
1085
  if (resource === "blogs" && rawBody && typeof rawBody === "object" && entityMap.categories && entityMap.seos && entityMap.tags) {
1002
- const existing = await repo.findOne({ where: { id: numericId } });
1086
+ const existing = await repo.findOne({
1087
+ where: { id: numericId, deleted: false }
1088
+ });
1003
1089
  if (!existing) return json({ message: "Not found" }, { status: 404 });
1004
1090
  const updatePayload2 = pickColumnUpdates(repo, rawBody);
1005
1091
  if ("category" in rawBody) {
@@ -1075,7 +1161,18 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1075
1161
  });
1076
1162
  return updated2 ? json(updated2) : json({ message: "Not found" }, { status: 404 });
1077
1163
  }
1164
+ if (entityHasSoftDelete(repo)) {
1165
+ const cur = await repo.findOne({
1166
+ where: { id: numericId, deleted: false }
1167
+ });
1168
+ if (!cur) return json({ message: "Not found" }, { status: 404 });
1169
+ }
1078
1170
  const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
1171
+ if (resource === "media") {
1172
+ const u = updatePayload;
1173
+ delete u.parentId;
1174
+ delete u.kind;
1175
+ }
1079
1176
  if (Object.keys(updatePayload).length > 0) {
1080
1177
  sanitizeBodyForEntity(repo, updatePayload);
1081
1178
  await repo.update(numericId, updatePayload);
@@ -1096,7 +1193,24 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
1096
1193
  const entity = entityMap[resource];
1097
1194
  if (!entity) return json({ error: "Invalid resource" }, { status: 400 });
1098
1195
  const repo = dataSource.getRepository(entity);
1099
- const result = await repo.delete(Number(id));
1196
+ const numericId = Number(id);
1197
+ if (entityHasSoftDelete(repo)) {
1198
+ const existing = await repo.findOne({
1199
+ where: { id: numericId, deleted: false }
1200
+ });
1201
+ if (!existing) return json({ message: "Not found" }, { status: 404 });
1202
+ let deletedBy = null;
1203
+ if (getDeletedByUserId) {
1204
+ try {
1205
+ deletedBy = await getDeletedByUserId(req);
1206
+ } catch {
1207
+ deletedBy = null;
1208
+ }
1209
+ }
1210
+ await repo.update(numericId, buildSoftDeletePayload(repo.metadata, deletedBy));
1211
+ return json({ message: "Deleted successfully" }, { status: 200 });
1212
+ }
1213
+ const result = await repo.delete(numericId);
1100
1214
  if (result.affected === 0) return json({ message: "Not found" }, { status: 404 });
1101
1215
  return json({ message: "Deleted successfully" }, { status: 200 });
1102
1216
  }
@@ -1250,7 +1364,7 @@ function createUserAuthApiRouter(config) {
1250
1364
  }
1251
1365
 
1252
1366
  // src/api/cms-handlers.ts
1253
- var import_typeorm3 = require("typeorm");
1367
+ var import_typeorm4 = require("typeorm");
1254
1368
  init_email_queue();
1255
1369
  init_erp_queue();
1256
1370
 
@@ -1270,6 +1384,194 @@ async function assertCaptchaOk(getCms, body, req, json) {
1270
1384
  return json({ error: result.message }, { status: result.status });
1271
1385
  }
1272
1386
 
1387
+ // src/lib/media-folder-path.ts
1388
+ function sanitizeMediaFolderPath(input) {
1389
+ if (input == null) return "";
1390
+ if (typeof input !== "string") return "";
1391
+ const segments = input.replace(/\\/g, "/").split("/").map((s) => s.trim()).filter(Boolean).filter((s) => s !== ".." && s !== ".");
1392
+ const joined = segments.join("/");
1393
+ return joined.length > 512 ? joined.slice(0, 512) : joined;
1394
+ }
1395
+ function sanitizeStorageSegment(name) {
1396
+ const s = name.replace(/[/\\]/g, "-").trim().slice(0, 255);
1397
+ return s || "item";
1398
+ }
1399
+
1400
+ // src/lib/media-parent-path.ts
1401
+ async function relativePathFromMediaParentId(dataSource, entityMap, parentId) {
1402
+ if (parentId == null) return "";
1403
+ const repo = dataSource.getRepository(entityMap.media);
1404
+ const segments = [];
1405
+ let id = parentId;
1406
+ for (let d = 0; d < 64 && id != null; d++) {
1407
+ const row = await repo.findOne({ where: { id } });
1408
+ if (!row) break;
1409
+ const m = row;
1410
+ if (m.kind !== "folder") break;
1411
+ segments.unshift(sanitizeStorageSegment(m.filename));
1412
+ id = m.parentId ?? null;
1413
+ }
1414
+ return segments.join("/");
1415
+ }
1416
+
1417
+ // src/lib/media-zip-extract.ts
1418
+ var import_typeorm3 = require("typeorm");
1419
+ var ZIP_MIME_TYPES = /* @__PURE__ */ new Set(["application/zip", "application/x-zip-compressed"]);
1420
+ var MAX_ENTRIES = 2e3;
1421
+ var MAX_TOTAL_UNCOMPRESSED = 80 * 1024 * 1024;
1422
+ function isZipMedia(mime, filename) {
1423
+ if (mime && ZIP_MIME_TYPES.has(mime)) return true;
1424
+ return filename.toLowerCase().endsWith(".zip");
1425
+ }
1426
+ async function readBufferFromPublicUrl(url) {
1427
+ if (url.startsWith("http://") || url.startsWith("https://")) {
1428
+ const r = await fetch(url);
1429
+ if (!r.ok) throw new Error("Failed to download file");
1430
+ return Buffer.from(await r.arrayBuffer());
1431
+ }
1432
+ if (url.startsWith("/")) {
1433
+ const { readFile } = await import("fs/promises");
1434
+ const { join } = await import("path");
1435
+ const rel = url.replace(/^\/+/, "");
1436
+ return readFile(join(process.cwd(), "public", rel));
1437
+ }
1438
+ throw new Error("Unsupported media URL");
1439
+ }
1440
+ function sanitizeZipPath(entryName) {
1441
+ const norm = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
1442
+ for (const seg of norm) {
1443
+ if (seg === ".." || seg === ".") return null;
1444
+ }
1445
+ return norm;
1446
+ }
1447
+ function shouldSkipEntry(parts) {
1448
+ if (parts[0] === "__MACOSX") return true;
1449
+ const last = parts[parts.length - 1];
1450
+ if (last === ".DS_Store") return true;
1451
+ return false;
1452
+ }
1453
+ function guessMimeType(fileName) {
1454
+ const lower = fileName.toLowerCase();
1455
+ if (lower.endsWith(".png")) return "image/png";
1456
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
1457
+ if (lower.endsWith(".gif")) return "image/gif";
1458
+ if (lower.endsWith(".webp")) return "image/webp";
1459
+ if (lower.endsWith(".svg")) return "image/svg+xml";
1460
+ if (lower.endsWith(".pdf")) return "application/pdf";
1461
+ if (lower.endsWith(".txt")) return "text/plain";
1462
+ if (lower.endsWith(".json")) return "application/json";
1463
+ if (lower.endsWith(".zip")) return "application/zip";
1464
+ return "application/octet-stream";
1465
+ }
1466
+ async function findOrCreateFolder(dataSource, entityMap, parentId, name) {
1467
+ const safe = sanitizeStorageSegment(name);
1468
+ const repo = dataSource.getRepository(entityMap.media);
1469
+ const where = parentId == null ? { kind: "folder", filename: safe, parentId: (0, import_typeorm3.IsNull)() } : { kind: "folder", filename: safe, parentId };
1470
+ const existing = await repo.findOne({ where });
1471
+ if (existing) return existing.id;
1472
+ const row = await repo.save(
1473
+ repo.create({
1474
+ kind: "folder",
1475
+ parentId,
1476
+ filename: safe,
1477
+ url: null,
1478
+ mimeType: "inode/directory",
1479
+ size: 0,
1480
+ alt: null,
1481
+ isPublic: false,
1482
+ deleted: false
1483
+ })
1484
+ );
1485
+ return row.id;
1486
+ }
1487
+ async function ensureFolderChain(dataSource, entityMap, rootParentId, pathSegments) {
1488
+ let pid = rootParentId;
1489
+ for (const seg of pathSegments) {
1490
+ if (!seg) continue;
1491
+ pid = await findOrCreateFolder(dataSource, entityMap, pid, seg);
1492
+ }
1493
+ return pid;
1494
+ }
1495
+ async function extractZipMediaIntoParentTree(opts) {
1496
+ const { dataSource, entityMap, zipMediaRow } = opts;
1497
+ const row = zipMediaRow;
1498
+ if (row.kind !== "file" || !row.url) throw new Error("Not a file");
1499
+ if (!isZipMedia(row.mimeType, row.filename)) throw new Error("Not a zip archive");
1500
+ const buffer = await readBufferFromPublicUrl(row.url);
1501
+ const { default: AdmZip } = await import("adm-zip");
1502
+ const zip = new AdmZip(buffer);
1503
+ const entries = zip.getEntries();
1504
+ if (entries.length > MAX_ENTRIES) throw new Error(`Too many zip entries (max ${MAX_ENTRIES})`);
1505
+ const rootParentId = row.parentId;
1506
+ const items = [];
1507
+ let totalUncompressed = 0;
1508
+ for (const e of entries) {
1509
+ const raw = e.entryName;
1510
+ const parts = sanitizeZipPath(raw);
1511
+ if (!parts || shouldSkipEntry(parts)) continue;
1512
+ const isDir = e.isDirectory || /\/$/.test(raw);
1513
+ let data = null;
1514
+ if (!isDir) {
1515
+ data = e.getData();
1516
+ totalUncompressed += data.length;
1517
+ if (totalUncompressed > MAX_TOTAL_UNCOMPRESSED) {
1518
+ throw new Error(`Uncompressed content exceeds limit (${MAX_TOTAL_UNCOMPRESSED} bytes)`);
1519
+ }
1520
+ }
1521
+ items.push({ parts, isDir, data });
1522
+ }
1523
+ items.sort((a, b) => {
1524
+ const da = a.parts.length;
1525
+ const db = b.parts.length;
1526
+ if (da !== db) return da - db;
1527
+ return a.parts.join("/").localeCompare(b.parts.join("/"));
1528
+ });
1529
+ let files = 0;
1530
+ let folderEntries = 0;
1531
+ const repo = dataSource.getRepository(entityMap.media);
1532
+ for (const it of items) {
1533
+ if (it.isDir) {
1534
+ await ensureFolderChain(dataSource, entityMap, rootParentId, it.parts);
1535
+ folderEntries++;
1536
+ continue;
1537
+ }
1538
+ const fileName = it.parts[it.parts.length - 1];
1539
+ const dirParts = it.parts.slice(0, -1);
1540
+ const parentFolderId = await ensureFolderChain(dataSource, entityMap, rootParentId, dirParts);
1541
+ const buf = it.data;
1542
+ const relBase = await relativePathFromMediaParentId(dataSource, entityMap, parentFolderId);
1543
+ const relativeUnderUploads = relBase ? `${relBase}/${fileName}` : fileName;
1544
+ const contentType = guessMimeType(fileName);
1545
+ let publicUrl;
1546
+ if (opts.storage) {
1547
+ publicUrl = await opts.storage.upload(buf, `uploads/${relativeUnderUploads}`, contentType);
1548
+ } else {
1549
+ const fs = await import("fs/promises");
1550
+ const pathMod = await import("path");
1551
+ const dir = pathMod.join(process.cwd(), opts.localUploadDir);
1552
+ const filePath = pathMod.join(dir, relativeUnderUploads);
1553
+ await fs.mkdir(pathMod.dirname(filePath), { recursive: true });
1554
+ await fs.writeFile(filePath, buf);
1555
+ publicUrl = `/${opts.localUploadDir.replace(/^\/+/, "").replace(/\\/g, "/")}/${relativeUnderUploads.replace(/\\/g, "/")}`;
1556
+ }
1557
+ await repo.save(
1558
+ repo.create({
1559
+ kind: "file",
1560
+ parentId: parentFolderId,
1561
+ filename: fileName,
1562
+ url: publicUrl,
1563
+ mimeType: contentType,
1564
+ size: buf.length,
1565
+ alt: null,
1566
+ isPublic: false,
1567
+ deleted: false
1568
+ })
1569
+ );
1570
+ files++;
1571
+ }
1572
+ return { files, folderEntries };
1573
+ }
1574
+
1273
1575
  // src/api/cms-handlers.ts
1274
1576
  function createDashboardStatsHandler(config) {
1275
1577
  const { dataSource, entityMap, json, requireAuth, requirePermission, requireEntityPermission } = config;
@@ -1287,26 +1589,209 @@ function createDashboardStatsHandler(config) {
1287
1589
  try {
1288
1590
  const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
1289
1591
  const repo = (name) => entityMap[name] ? dataSource.getRepository(entityMap[name]) : void 0;
1290
- const [contactsCount, formsCount, formSubmissionsCount, usersCount, blogsCount, recentContacts, recentSubmissions] = await Promise.all([
1592
+ const [contactsCount, formsCount, formSubmissionsCount, usersCount, blogsCount, recentContacts, recentSubmissions, contactTypeRows] = await Promise.all([
1291
1593
  repo("contacts")?.count() ?? 0,
1292
1594
  repo("forms")?.count({ where: { deleted: false } }) ?? 0,
1293
1595
  repo("form_submissions")?.count() ?? 0,
1294
1596
  repo("users")?.count({ where: { deleted: false } }) ?? 0,
1295
1597
  repo("blogs")?.count({ where: { deleted: false } }) ?? 0,
1296
- repo("contacts")?.count({ where: { createdAt: (0, import_typeorm3.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0,
1297
- repo("form_submissions")?.count({ where: { createdAt: (0, import_typeorm3.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0
1598
+ repo("contacts")?.count({ where: { createdAt: (0, import_typeorm4.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0,
1599
+ repo("form_submissions")?.count({ where: { createdAt: (0, import_typeorm4.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0,
1600
+ repo("contacts")?.createQueryBuilder("c").select("COALESCE(NULLIF(TRIM(c.type), ''), 'unknown')", "type").addSelect("COUNT(*)", "count").where("c.deleted = :deleted", { deleted: false }).groupBy("COALESCE(NULLIF(TRIM(c.type), ''), 'unknown')").getRawMany() ?? []
1298
1601
  ]);
1299
1602
  return json({
1300
1603
  contacts: { total: contactsCount, recent: recentContacts },
1301
1604
  forms: { total: formsCount, submissions: formSubmissionsCount, recentSubmissions },
1302
1605
  users: usersCount,
1303
- blogs: blogsCount
1606
+ blogs: blogsCount,
1607
+ contactTypes: (contactTypeRows ?? []).map((row) => ({
1608
+ type: row.type || "unknown",
1609
+ count: Number(row.count || 0)
1610
+ }))
1304
1611
  });
1305
1612
  } catch (err) {
1306
1613
  return json({ error: "Failed to fetch dashboard stats" }, { status: 500 });
1307
1614
  }
1308
1615
  };
1309
1616
  }
1617
+ function toNum(v) {
1618
+ const n = typeof v === "number" ? v : Number(v ?? 0);
1619
+ return Number.isFinite(n) ? n : 0;
1620
+ }
1621
+ function toIsoDate(d) {
1622
+ return d.toISOString().slice(0, 10);
1623
+ }
1624
+ function createEcommerceAnalyticsHandler(config) {
1625
+ const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
1626
+ return async function GET(req) {
1627
+ const authErr = await requireAuth(req);
1628
+ if (authErr) return authErr;
1629
+ if (requireEntityPermission) {
1630
+ const pe = await requireEntityPermission(req, "analytics", "read");
1631
+ if (pe) return pe;
1632
+ }
1633
+ if (!entityMap.orders || !entityMap.order_items || !entityMap.payments || !entityMap.products) {
1634
+ return json({ error: "Store analytics unavailable" }, { status: 404 });
1635
+ }
1636
+ try {
1637
+ const url = new URL(req.url);
1638
+ const rawDays = parseInt(url.searchParams.get("days") || "30", 10);
1639
+ const days = Number.isFinite(rawDays) ? Math.min(365, Math.max(7, rawDays)) : 30;
1640
+ const end = /* @__PURE__ */ new Date();
1641
+ const start = new Date(end.getTime() - days * 24 * 60 * 60 * 1e3);
1642
+ const orderRepo = dataSource.getRepository(entityMap.orders);
1643
+ const paymentRepo = dataSource.getRepository(entityMap.payments);
1644
+ const itemRepo = dataSource.getRepository(entityMap.order_items);
1645
+ const productRepo = dataSource.getRepository(entityMap.products);
1646
+ const [salesOrders, returnOrders, replacementOrders, payments, products] = await Promise.all([
1647
+ orderRepo.find({
1648
+ where: { deleted: false, createdAt: (0, import_typeorm4.MoreThanOrEqual)(start), orderKind: "sale", status: (0, import_typeorm4.In)(["confirmed", "processing", "completed"]) },
1649
+ select: ["id", "contactId", "createdAt", "subtotal", "discount", "tax", "total", "status"]
1650
+ }),
1651
+ orderRepo.find({
1652
+ where: { deleted: false, createdAt: (0, import_typeorm4.MoreThanOrEqual)(start), orderKind: "return" },
1653
+ select: ["id", "createdAt", "total"]
1654
+ }),
1655
+ orderRepo.find({
1656
+ where: { deleted: false, createdAt: (0, import_typeorm4.MoreThanOrEqual)(start), orderKind: "replacement" },
1657
+ select: ["id", "createdAt", "total"]
1658
+ }),
1659
+ paymentRepo.find({
1660
+ where: { deleted: false, createdAt: (0, import_typeorm4.MoreThanOrEqual)(start) },
1661
+ select: ["id", "status", "method", "amount", "createdAt"]
1662
+ }),
1663
+ productRepo.find({
1664
+ where: { deleted: false },
1665
+ select: ["id", "name", "quantity"]
1666
+ })
1667
+ ]);
1668
+ const saleOrderIds = salesOrders.map((o) => o.id);
1669
+ const orderItems = saleOrderIds.length ? await itemRepo.find({
1670
+ where: { orderId: (0, import_typeorm4.In)(saleOrderIds) },
1671
+ select: ["id", "orderId", "productId", "quantity", "total"]
1672
+ }) : [];
1673
+ const grossSales = salesOrders.reduce((sum, o) => sum + toNum(o.subtotal), 0);
1674
+ const discounts = salesOrders.reduce((sum, o) => sum + toNum(o.discount), 0);
1675
+ const taxes = salesOrders.reduce((sum, o) => sum + toNum(o.tax), 0);
1676
+ const returnsValue = returnOrders.reduce((sum, o) => sum + toNum(o.total), 0);
1677
+ const replacementsValue = replacementOrders.reduce((sum, o) => sum + toNum(o.total), 0);
1678
+ const netSales = grossSales - discounts - returnsValue;
1679
+ const ordersCount = salesOrders.length;
1680
+ const aov = ordersCount > 0 ? netSales / ordersCount : 0;
1681
+ const returnRate = ordersCount > 0 ? returnOrders.length / ordersCount * 100 : 0;
1682
+ const salesByDate = /* @__PURE__ */ new Map();
1683
+ const returnsByDate = /* @__PURE__ */ new Map();
1684
+ for (const o of salesOrders) {
1685
+ const key = toIsoDate(new Date(o.createdAt));
1686
+ const row = salesByDate.get(key) ?? { value: 0, orders: 0 };
1687
+ row.value += toNum(o.total);
1688
+ row.orders += 1;
1689
+ salesByDate.set(key, row);
1690
+ }
1691
+ for (const o of returnOrders) {
1692
+ const key = toIsoDate(new Date(o.createdAt));
1693
+ const row = returnsByDate.get(key) ?? { value: 0, count: 0 };
1694
+ row.value += toNum(o.total);
1695
+ row.count += 1;
1696
+ returnsByDate.set(key, row);
1697
+ }
1698
+ const salesOverTime = [];
1699
+ const returnsTrend = [];
1700
+ for (let i = days - 1; i >= 0; i--) {
1701
+ const d = new Date(end.getTime() - i * 24 * 60 * 60 * 1e3);
1702
+ const key = toIsoDate(d);
1703
+ const sales = salesByDate.get(key) ?? { value: 0, orders: 0 };
1704
+ const returns = returnsByDate.get(key) ?? { value: 0, count: 0 };
1705
+ salesOverTime.push({ date: key, value: Number(sales.value.toFixed(2)), orders: sales.orders });
1706
+ returnsTrend.push({ date: key, value: Number(returns.value.toFixed(2)), count: returns.count });
1707
+ }
1708
+ const productNameMap = /* @__PURE__ */ new Map();
1709
+ for (const p of products) productNameMap.set(Number(p.id), (p.name || `Product #${p.id}`).trim());
1710
+ const productAgg = /* @__PURE__ */ new Map();
1711
+ for (const item of orderItems) {
1712
+ const productId = Number(item.productId);
1713
+ const productName = productNameMap.get(productId) || `Product #${productId}`;
1714
+ const row = productAgg.get(productId) ?? { name: productName, units: 0, sales: 0 };
1715
+ row.units += toNum(item.quantity);
1716
+ row.sales += toNum(item.total);
1717
+ productAgg.set(productId, row);
1718
+ }
1719
+ const topProducts = Array.from(productAgg.values()).sort((a, b) => b.sales - a.sales).slice(0, 5).map((p) => ({ ...p, sales: Number(p.sales.toFixed(2)) }));
1720
+ const allSaleOrderContactIds = Array.from(new Set(salesOrders.map((o) => Number(o.contactId)).filter((n) => Number.isInteger(n) && n > 0)));
1721
+ const allTimeCounts = allSaleOrderContactIds.length ? await orderRepo.createQueryBuilder("o").select("o.contactId", "contactId").addSelect("COUNT(*)", "total").where("o.deleted = :deleted", { deleted: false }).andWhere("o.orderKind = :orderKind", { orderKind: "sale" }).andWhere("o.contactId IN (:...contactIds)", { contactIds: allSaleOrderContactIds }).groupBy("o.contactId").getRawMany() : [];
1722
+ const countMap = /* @__PURE__ */ new Map();
1723
+ for (const c of allTimeCounts) countMap.set(Number(c.contactId), Number(c.total));
1724
+ const purchasingCustomers = allSaleOrderContactIds.length;
1725
+ const returningCustomers = allSaleOrderContactIds.filter((id) => (countMap.get(id) ?? 0) > 1).length;
1726
+ const newCustomers = Math.max(0, purchasingCustomers - returningCustomers);
1727
+ const returningCustomerRate = purchasingCustomers > 0 ? returningCustomers / purchasingCustomers * 100 : 0;
1728
+ const totalPayments = payments.length;
1729
+ const completedPayments = payments.filter((p) => p.status === "completed").length;
1730
+ const failedPayments = payments.filter((p) => p.status === "failed").length;
1731
+ const paymentSuccessRate = totalPayments > 0 ? completedPayments / totalPayments * 100 : 0;
1732
+ const paymentMethodMap = /* @__PURE__ */ new Map();
1733
+ for (const p of payments) {
1734
+ const method = (p.method || "unknown").toLowerCase();
1735
+ const row = paymentMethodMap.get(method) ?? { method, count: 0, amount: 0 };
1736
+ row.count += 1;
1737
+ row.amount += toNum(p.amount);
1738
+ paymentMethodMap.set(method, row);
1739
+ }
1740
+ const paymentMethods = Array.from(paymentMethodMap.values()).sort((a, b) => b.count - a.count).map((p) => ({ ...p, amount: Number(p.amount.toFixed(2)) }));
1741
+ const totalInventory = products.reduce((sum, p) => sum + toNum(p.quantity), 0);
1742
+ const outOfStockCount = products.filter((p) => toNum(p.quantity) <= 0).length;
1743
+ const lowStockCount = products.filter((p) => toNum(p.quantity) > 0 && toNum(p.quantity) <= 5).length;
1744
+ const inventoryRisk = {
1745
+ outOfStockCount,
1746
+ lowStockCount,
1747
+ totalInventory
1748
+ };
1749
+ return json({
1750
+ rangeDays: days,
1751
+ kpis: {
1752
+ netSales: Number(netSales.toFixed(2)),
1753
+ grossSales: Number(grossSales.toFixed(2)),
1754
+ ordersPlaced: ordersCount,
1755
+ averageOrderValue: Number(aov.toFixed(2)),
1756
+ returningCustomerRate: Number(returningCustomerRate.toFixed(2)),
1757
+ returnRate: Number(returnRate.toFixed(2)),
1758
+ returnValue: Number(returnsValue.toFixed(2)),
1759
+ discounts: Number(discounts.toFixed(2)),
1760
+ taxes: Number(taxes.toFixed(2)),
1761
+ paymentSuccessRate: Number(paymentSuccessRate.toFixed(2))
1762
+ },
1763
+ salesOverTime,
1764
+ topProducts,
1765
+ customerMix: {
1766
+ newCustomers,
1767
+ returningCustomers,
1768
+ repeatPurchaseRate: Number(returningCustomerRate.toFixed(2))
1769
+ },
1770
+ returnsTrend,
1771
+ paymentPerformance: {
1772
+ successCount: completedPayments,
1773
+ failedCount: failedPayments,
1774
+ successRate: Number(paymentSuccessRate.toFixed(2)),
1775
+ methods: paymentMethods
1776
+ },
1777
+ conversionProxy: {
1778
+ sessions: 0,
1779
+ checkoutStarted: 0,
1780
+ ordersPlaced: ordersCount
1781
+ },
1782
+ salesBreakdown: {
1783
+ sales: { count: ordersCount, value: Number(grossSales.toFixed(2)) },
1784
+ returns: { count: returnOrders.length, value: Number(returnsValue.toFixed(2)) },
1785
+ replacements: { count: replacementOrders.length, value: Number(replacementsValue.toFixed(2)) }
1786
+ },
1787
+ geoPerformance: [],
1788
+ inventoryRisk
1789
+ });
1790
+ } catch {
1791
+ return json({ error: "Failed to fetch ecommerce analytics" }, { status: 500 });
1792
+ }
1793
+ };
1794
+ }
1310
1795
  function createAnalyticsHandlers(config) {
1311
1796
  const { json, getAnalyticsData, getPropertyId, getPermissions } = config;
1312
1797
  return {
@@ -1338,8 +1823,27 @@ function createAnalyticsHandlers(config) {
1338
1823
  };
1339
1824
  }
1340
1825
  function createUploadHandler(config) {
1341
- const { json, requireAuth, requireEntityPermission, storage, localUploadDir = "public/uploads", allowedTypes, maxSizeBytes = 10 * 1024 * 1024 } = config;
1342
- const allowed = allowedTypes ?? ["image/jpeg", "image/png", "image/gif", "image/webp", "application/pdf", "text/plain"];
1826
+ const {
1827
+ json,
1828
+ requireAuth,
1829
+ requireEntityPermission,
1830
+ storage,
1831
+ localUploadDir = "public/uploads",
1832
+ allowedTypes,
1833
+ maxSizeBytes = 10 * 1024 * 1024,
1834
+ dataSource,
1835
+ entityMap
1836
+ } = config;
1837
+ const allowed = allowedTypes ?? [
1838
+ "image/jpeg",
1839
+ "image/png",
1840
+ "image/gif",
1841
+ "image/webp",
1842
+ "application/pdf",
1843
+ "text/plain",
1844
+ "application/zip",
1845
+ "application/x-zip-compressed"
1846
+ ];
1343
1847
  return async function POST(req) {
1344
1848
  const authErr = await requireAuth(req);
1345
1849
  if (authErr) return authErr;
@@ -1352,28 +1856,92 @@ function createUploadHandler(config) {
1352
1856
  const file = formData.get("file");
1353
1857
  if (!file) return json({ error: "No file uploaded" }, { status: 400 });
1354
1858
  if (!allowed.includes(file.type)) return json({ error: "File type not allowed" }, { status: 400 });
1355
- if (file.size > maxSizeBytes) return json({ error: "File size exceeds limit" }, { status: 400 });
1859
+ const defaultMax = 10 * 1024 * 1024;
1860
+ const maxZipBytes = 80 * 1024 * 1024;
1861
+ const baseMax = maxSizeBytes ?? defaultMax;
1862
+ const effectiveMax = file.type === "application/zip" || file.type === "application/x-zip-compressed" ? Math.max(baseMax, maxZipBytes) : baseMax;
1863
+ if (file.size > effectiveMax) return json({ error: "File size exceeds limit" }, { status: 400 });
1864
+ const parentRaw = formData.get("parentId");
1865
+ let parentId = null;
1866
+ if (parentRaw != null && String(parentRaw).trim() !== "") {
1867
+ const n = Number(parentRaw);
1868
+ if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
1869
+ parentId = n;
1870
+ }
1871
+ let folder = "";
1872
+ if (parentId != null) {
1873
+ if (!dataSource || !entityMap?.media) {
1874
+ return json({ error: "Upload handler needs dataSource and entityMap for folder uploads" }, { status: 400 });
1875
+ }
1876
+ const repo = dataSource.getRepository(entityMap.media);
1877
+ const p = await repo.findOne({ where: { id: parentId } });
1878
+ if (!p || p.kind !== "folder") {
1879
+ return json({ error: "parent must be a folder" }, { status: 400 });
1880
+ }
1881
+ folder = await relativePathFromMediaParentId(dataSource, entityMap, parentId);
1882
+ } else {
1883
+ const folderRawLegacy = formData.get("folder") ?? formData.get("folderPath");
1884
+ if (folderRawLegacy && typeof folderRawLegacy === "string" && folderRawLegacy.trim()) {
1885
+ folder = sanitizeMediaFolderPath(folderRawLegacy);
1886
+ }
1887
+ }
1356
1888
  const buffer = Buffer.from(await file.arrayBuffer());
1357
1889
  const fileName = `${Date.now()}-${file.name}`;
1358
1890
  const contentType = file.type || "application/octet-stream";
1891
+ const relativeUnderUploads = folder ? `${folder}/${fileName}` : fileName;
1359
1892
  const raw = typeof storage === "function" ? storage() : storage;
1360
1893
  const storageService = raw instanceof Promise ? await raw : raw;
1361
1894
  if (storageService) {
1362
- const fileUrl = await storageService.upload(buffer, `uploads/${fileName}`, contentType);
1363
- return json({ filePath: fileUrl });
1895
+ const fileUrl = await storageService.upload(buffer, `uploads/${relativeUnderUploads}`, contentType);
1896
+ return json({ filePath: fileUrl, parentId });
1364
1897
  }
1365
1898
  const fs = await import("fs/promises");
1366
1899
  const path = await import("path");
1367
1900
  const dir = path.join(process.cwd(), localUploadDir);
1368
- await fs.mkdir(dir, { recursive: true });
1369
- const filePath = path.join(dir, fileName);
1901
+ const filePath = path.join(dir, relativeUnderUploads);
1902
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
1370
1903
  await fs.writeFile(filePath, buffer);
1371
- return json({ filePath: `/${localUploadDir.replace(/^\/+/, "").replace(/\\/g, "/")}/${fileName}` });
1904
+ const urlRel = `${localUploadDir.replace(/^\/+/, "").replace(/\\/g, "/")}/${relativeUnderUploads.replace(/\\/g, "/")}`;
1905
+ return json({ filePath: `/${urlRel}`, parentId });
1372
1906
  } catch (err) {
1373
1907
  return json({ error: "File upload failed" }, { status: 500 });
1374
1908
  }
1375
1909
  };
1376
1910
  }
1911
+ function createMediaZipExtractHandler(config) {
1912
+ const { json, requireAuth, requireEntityPermission, storage, localUploadDir = "public/uploads", dataSource, entityMap } = config;
1913
+ return async function POST(_req, zipMediaId) {
1914
+ const authErr = await requireAuth(_req);
1915
+ if (authErr) return authErr;
1916
+ if (requireEntityPermission) {
1917
+ const pe = await requireEntityPermission(_req, "media", "create");
1918
+ if (pe) return pe;
1919
+ }
1920
+ if (!dataSource || !entityMap?.media) {
1921
+ return json({ error: "Media extract requires dataSource and entityMap" }, { status: 500 });
1922
+ }
1923
+ const id = Number(zipMediaId);
1924
+ if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
1925
+ const repo = dataSource.getRepository(entityMap.media);
1926
+ const row = await repo.findOne({ where: { id } });
1927
+ if (!row) return json({ error: "Not found" }, { status: 404 });
1928
+ try {
1929
+ const raw = typeof storage === "function" ? storage() : storage;
1930
+ const storageService = raw instanceof Promise ? await raw : raw;
1931
+ const result = await extractZipMediaIntoParentTree({
1932
+ dataSource,
1933
+ entityMap,
1934
+ zipMediaRow: row,
1935
+ storage: storageService,
1936
+ localUploadDir
1937
+ });
1938
+ return json({ ok: true, ...result });
1939
+ } catch (e) {
1940
+ const msg = e instanceof Error ? e.message : "Extract failed";
1941
+ return json({ error: msg }, { status: 400 });
1942
+ }
1943
+ };
1944
+ }
1377
1945
  function createBlogBySlugHandler(config) {
1378
1946
  const { dataSource, entityMap, json } = config;
1379
1947
  return async function GET(_req, slug) {
@@ -1750,7 +2318,7 @@ function createFormSubmissionHandler(config) {
1750
2318
  };
1751
2319
  }
1752
2320
  function createUsersApiHandlers(config) {
1753
- const { dataSource, entityMap, json, requireAuth, requireEntityPermission, baseUrl, getCms, getCompanyDetails } = config;
2321
+ const { dataSource, entityMap, json, requireAuth, requireEntityPermission, baseUrl, getCms, getCompanyDetails, getSessionUser } = config;
1754
2322
  async function trySendInviteEmail(toEmail, inviteLink, inviteeName) {
1755
2323
  if (!getCms) return;
1756
2324
  try {
@@ -1786,7 +2354,10 @@ function createUsersApiHandlers(config) {
1786
2354
  const sortField = url.searchParams.get("sortField") || "createdAt";
1787
2355
  const sortOrder = url.searchParams.get("sortOrder") === "desc" ? "DESC" : "ASC";
1788
2356
  const search = url.searchParams.get("search");
1789
- const where = search ? [{ name: (0, import_typeorm3.ILike)(`%${search}%`) }, { email: (0, import_typeorm3.ILike)(`%${search}%`) }] : {};
2357
+ const where = search ? [
2358
+ { name: (0, import_typeorm4.ILike)(`%${search}%`), deleted: false },
2359
+ { email: (0, import_typeorm4.ILike)(`%${search}%`), deleted: false }
2360
+ ] : { deleted: false };
1790
2361
  const [data, total] = await userRepo().findAndCount({
1791
2362
  skip,
1792
2363
  take: limit,
@@ -1851,7 +2422,7 @@ function createUsersApiHandlers(config) {
1851
2422
  }
1852
2423
  try {
1853
2424
  const user = await userRepo().findOne({
1854
- where: { id: parseInt(id, 10) },
2425
+ where: { id: parseInt(id, 10), deleted: false },
1855
2426
  relations: ["group"],
1856
2427
  select: ["id", "name", "email", "blocked", "createdAt", "updatedAt", "groupId"]
1857
2428
  });
@@ -1869,11 +2440,14 @@ function createUsersApiHandlers(config) {
1869
2440
  if (pe) return pe;
1870
2441
  }
1871
2442
  try {
2443
+ const uid = parseInt(id, 10);
2444
+ const existing = await userRepo().findOne({ where: { id: uid, deleted: false } });
2445
+ if (!existing) return json({ error: "Not found" }, { status: 404 });
1872
2446
  const body = await req.json();
1873
2447
  const { password: _p, ...safe } = body;
1874
- await userRepo().update(parseInt(id, 10), safe);
2448
+ await userRepo().update(uid, safe);
1875
2449
  const updated = await userRepo().findOne({
1876
- where: { id: parseInt(id, 10) },
2450
+ where: { id: uid, deleted: false },
1877
2451
  relations: ["group"],
1878
2452
  select: ["id", "name", "email", "blocked", "createdAt", "updatedAt", "groupId"]
1879
2453
  });
@@ -1890,8 +2464,23 @@ function createUsersApiHandlers(config) {
1890
2464
  if (pe) return pe;
1891
2465
  }
1892
2466
  try {
1893
- const r = await userRepo().delete(parseInt(id, 10));
1894
- if (r.affected === 0) return json({ error: "User not found" }, { status: 404 });
2467
+ const uid = parseInt(id, 10);
2468
+ const existing = await userRepo().findOne({ where: { id: uid, deleted: false } });
2469
+ if (!existing) return json({ error: "User not found" }, { status: 404 });
2470
+ let deletedBy = null;
2471
+ if (getSessionUser) {
2472
+ try {
2473
+ const u = await getSessionUser();
2474
+ if (u?.id) {
2475
+ const n = Number(u.id);
2476
+ if (Number.isFinite(n)) deletedBy = n;
2477
+ }
2478
+ } catch {
2479
+ }
2480
+ }
2481
+ const payload = { deleted: true, deletedAt: /* @__PURE__ */ new Date() };
2482
+ if (deletedBy != null) payload.deletedBy = deletedBy;
2483
+ await userRepo().update(uid, payload);
1895
2484
  return json({ message: "User deleted successfully" });
1896
2485
  } catch {
1897
2486
  return json({ error: "Server Error" }, { status: 500 });
@@ -1905,7 +2494,10 @@ function createUsersApiHandlers(config) {
1905
2494
  if (pe) return pe;
1906
2495
  }
1907
2496
  try {
1908
- const user = await userRepo().findOne({ where: { id: parseInt(id, 10) }, select: ["email", "name"] });
2497
+ const user = await userRepo().findOne({
2498
+ where: { id: parseInt(id, 10), deleted: false },
2499
+ select: ["email", "name"]
2500
+ });
1909
2501
  if (!user) return json({ error: "User not found" }, { status: 404 });
1910
2502
  const emailToken = Buffer.from(user.email).toString("base64");
1911
2503
  const inviteLink = `${baseUrl}/admin/invite?token=${emailToken}`;
@@ -2146,7 +2738,7 @@ function createChatHandlers(config) {
2146
2738
  if (contextParts.length === 0) {
2147
2739
  const terms = getQueryTerms(message);
2148
2740
  if (terms.length > 0) {
2149
- const conditions = terms.map((t) => ({ content: (0, import_typeorm3.ILike)(`%${t}%`) }));
2741
+ const conditions = terms.map((t) => ({ content: (0, import_typeorm4.ILike)(`%${t}%`) }));
2150
2742
  const chunks = await chunkRepo().find({
2151
2743
  where: conditions,
2152
2744
  take: KB_CHUNK_LIMIT,
@@ -2515,6 +3107,7 @@ function createCmsApiHandler(config) {
2515
3107
  getCms,
2516
3108
  userAuth: userAuthConfig,
2517
3109
  dashboard,
3110
+ ecommerceAnalytics,
2518
3111
  analytics: analyticsConfig,
2519
3112
  upload,
2520
3113
  blogBySlug,
@@ -2527,9 +3120,10 @@ function createCmsApiHandler(config) {
2527
3120
  userProfile,
2528
3121
  settings: settingsConfig,
2529
3122
  chat: chatConfig,
2530
- requireEntityPermission: reqEntityPerm,
3123
+ requireEntityPermission: userRequireEntityPermission,
2531
3124
  getSessionUser
2532
3125
  } = config;
3126
+ const requireEntityPermissionEffective = userRequireEntityPermission ?? (async (_req, entity, action) => config.json({ error: "Forbidden", reason: "entity_rbac_required", entity, action }, { status: 403 }));
2533
3127
  const analytics = analyticsConfig ?? (getCms ? {
2534
3128
  json: config.json,
2535
3129
  requireAuth: async () => null,
@@ -2567,12 +3161,20 @@ function createCmsApiHandler(config) {
2567
3161
  const crudOpts = {
2568
3162
  requireAuth: config.requireAuth,
2569
3163
  json: config.json,
2570
- requireEntityPermission: reqEntityPerm,
2571
- getCms
3164
+ requireEntityPermission: requireEntityPermissionEffective,
3165
+ getCms,
3166
+ ...getSessionUser ? {
3167
+ getDeletedByUserId: async () => {
3168
+ const u = await getSessionUser();
3169
+ if (!u?.id) return null;
3170
+ const n = Number(u.id);
3171
+ return Number.isFinite(n) ? n : null;
3172
+ }
3173
+ } : {}
2572
3174
  };
2573
3175
  const crud = createCrudHandler(dataSource, entityMap, crudOpts);
2574
3176
  const crudById = createCrudByIdHandler(dataSource, entityMap, crudOpts);
2575
- const mergePerm = (c) => !c ? void 0 : reqEntityPerm ? { ...c, requireEntityPermission: reqEntityPerm } : c;
3177
+ const mergePerm = (c) => !c ? void 0 : { ...c, requireEntityPermission: requireEntityPermissionEffective };
2576
3178
  const adminRoles = getSessionUser && createAdminRolesHandlers({
2577
3179
  dataSource,
2578
3180
  entityMap,
@@ -2581,8 +3183,23 @@ function createCmsApiHandler(config) {
2581
3183
  });
2582
3184
  const userAuthRouter = userAuth ? createUserAuthApiRouter(userAuth) : null;
2583
3185
  const dashboardGet = dashboard ? createDashboardStatsHandler(mergePerm(dashboard) ?? dashboard) : null;
3186
+ const ecommerceAnalyticsResolved = mergePerm(
3187
+ ecommerceAnalytics ?? {
3188
+ dataSource,
3189
+ entityMap,
3190
+ json: config.json,
3191
+ requireAuth: config.requireAuth
3192
+ }
3193
+ );
3194
+ const ecommerceAnalyticsGet = createEcommerceAnalyticsHandler(ecommerceAnalyticsResolved);
2584
3195
  const analyticsHandlers = analytics ? createAnalyticsHandlers(analytics) : null;
2585
- const uploadPost = upload ? createUploadHandler(mergePerm(upload) ?? upload) : null;
3196
+ const uploadMerged = upload ? {
3197
+ ...mergePerm(upload) ?? upload,
3198
+ dataSource: upload.dataSource ?? dataSource,
3199
+ entityMap: upload.entityMap ?? entityMap
3200
+ } : null;
3201
+ const uploadPost = uploadMerged ? createUploadHandler(uploadMerged) : null;
3202
+ const zipExtractPost = uploadMerged ? createMediaZipExtractHandler(uploadMerged) : null;
2586
3203
  const blogBySlugGet = blogBySlug ? createBlogBySlugHandler(blogBySlug) : null;
2587
3204
  const formBySlugGet = formBySlug ? createFormBySlugHandler(formBySlug) : null;
2588
3205
  const formSaveHandlers = formSaveConfig ? createFormSaveHandlers(mergePerm(formSaveConfig) ?? formSaveConfig) : null;
@@ -2592,7 +3209,11 @@ function createCmsApiHandler(config) {
2592
3209
  const usersApiMerged = usersApi && getCms ? {
2593
3210
  ...usersApi,
2594
3211
  getCms: usersApi.getCms ?? getCms,
2595
- getCompanyDetails: usersApi.getCompanyDetails ?? config.getCompanyDetails
3212
+ getCompanyDetails: usersApi.getCompanyDetails ?? config.getCompanyDetails,
3213
+ ...getSessionUser ? { getSessionUser: usersApi.getSessionUser ?? getSessionUser } : {}
3214
+ } : usersApi ? {
3215
+ ...usersApi,
3216
+ ...getSessionUser ? { getSessionUser: usersApi.getSessionUser ?? getSessionUser } : {}
2596
3217
  } : usersApi;
2597
3218
  const usersHandlers = usersApiMerged ? createUsersApiHandlers(mergePerm(usersApiMerged) ?? usersApiMerged) : null;
2598
3219
  const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
@@ -2603,7 +3224,7 @@ function createCmsApiHandler(config) {
2603
3224
  entityMap,
2604
3225
  json: config.json,
2605
3226
  requireAuth: config.requireAuth,
2606
- requireEntityPermission: reqEntityPerm
3227
+ requireEntityPermission: requireEntityPermissionEffective
2607
3228
  });
2608
3229
  const chatHandlers = chatConfig ? createChatHandlers(chatConfig) : null;
2609
3230
  function resolveResource(segment) {
@@ -2612,12 +3233,10 @@ function createCmsApiHandler(config) {
2612
3233
  }
2613
3234
  return {
2614
3235
  async handle(method, path, req) {
2615
- const perm = reqEntityPerm;
2616
3236
  async function analyticsGate() {
2617
3237
  const a = await config.requireAuth(req);
2618
3238
  if (a) return a;
2619
- if (perm) return perm(req, "analytics", "read");
2620
- return null;
3239
+ return requireEntityPermissionEffective(req, "analytics", "read");
2621
3240
  }
2622
3241
  if (path[0] === "admin" && path[1] === "roles") {
2623
3242
  if (!adminRoles) return config.json({ error: "Not found" }, { status: 404 });
@@ -2631,6 +3250,11 @@ function createCmsApiHandler(config) {
2631
3250
  if (path[0] === "dashboard" && path[1] === "stats" && path.length === 2 && method === "GET" && dashboardGet) {
2632
3251
  return dashboardGet(req);
2633
3252
  }
3253
+ if (path[0] === "dashboard" && path[1] === "ecommerce" && path.length === 2 && method === "GET" && ecommerceAnalyticsGet) {
3254
+ const g = await analyticsGate();
3255
+ if (g) return g;
3256
+ return ecommerceAnalyticsGet(req);
3257
+ }
2634
3258
  if (path[0] === "analytics" && analyticsHandlers) {
2635
3259
  if (path.length === 1 && method === "GET") {
2636
3260
  const g = await analyticsGate();
@@ -2649,6 +3273,9 @@ function createCmsApiHandler(config) {
2649
3273
  }
2650
3274
  }
2651
3275
  if (path[0] === "upload" && path.length === 1 && method === "POST" && uploadPost) return uploadPost(req);
3276
+ if (path[0] === "media" && path[1] === "extract" && path.length === 3 && method === "POST" && zipExtractPost) {
3277
+ return zipExtractPost(req, path[2]);
3278
+ }
2652
3279
  if (path[0] === "blogs" && path[1] === "slug" && path.length === 3 && method === "GET" && blogBySlugGet) {
2653
3280
  return blogBySlugGet(req, path[2]);
2654
3281
  }
@@ -2693,19 +3320,17 @@ function createCmsApiHandler(config) {
2693
3320
  const group = path[1];
2694
3321
  const isPublic = settingsConfig?.publicGetGroups?.includes(group);
2695
3322
  if (method === "GET") {
2696
- if (!isPublic && perm) {
3323
+ if (!isPublic) {
2697
3324
  const a = await config.requireAuth(req);
2698
3325
  if (a) return a;
2699
- const pe = await perm(req, "settings", "read");
3326
+ const pe = await requireEntityPermissionEffective(req, "settings", "read");
2700
3327
  if (pe) return pe;
2701
3328
  }
2702
3329
  return settingsHandlers.GET(req, group);
2703
3330
  }
2704
3331
  if (method === "PUT") {
2705
- if (perm) {
2706
- const pe = await perm(req, "settings", "update");
2707
- if (pe) return pe;
2708
- }
3332
+ const pe = await requireEntityPermissionEffective(req, "settings", "update");
3333
+ if (pe) return pe;
2709
3334
  return settingsHandlers.PUT(req, group);
2710
3335
  }
2711
3336
  }
@@ -2721,10 +3346,8 @@ function createCmsApiHandler(config) {
2721
3346
  if (path[0] === "orders" && path.length === 3 && path[2] === "invoice" && method === "GET" && getCms) {
2722
3347
  const a = await config.requireAuth(req);
2723
3348
  if (a) return a;
2724
- if (perm) {
2725
- const pe = await perm(req, "orders", "read");
2726
- if (pe) return pe;
2727
- }
3349
+ const pe = await requireEntityPermissionEffective(req, "orders", "read");
3350
+ if (pe) return pe;
2728
3351
  const cms = await getCms();
2729
3352
  const { streamOrderInvoicePdf: streamOrderInvoicePdf2 } = await Promise.resolve().then(() => (init_erp_order_invoice(), erp_order_invoice_exports));
2730
3353
  const oid = Number(path[1]);
@@ -2734,10 +3357,8 @@ function createCmsApiHandler(config) {
2734
3357
  if (path[0] === "orders" && path.length === 3 && path[2] === "repost-erp" && getCms) {
2735
3358
  const a = await config.requireAuth(req);
2736
3359
  if (a) return a;
2737
- if (perm) {
2738
- const pe = await perm(req, "orders", method === "GET" ? "read" : "update");
2739
- if (pe) return pe;
2740
- }
3360
+ const pe = await requireEntityPermissionEffective(req, "orders", method === "GET" ? "read" : "update");
3361
+ if (pe) return pe;
2741
3362
  const oid = Number(path[1]);
2742
3363
  if (!Number.isFinite(oid)) return config.json({ error: "Invalid id" }, { status: 400 });
2743
3364
  const cms = await getCms();
@@ -2786,7 +3407,7 @@ function createCmsApiHandler(config) {
2786
3407
  }
2787
3408
 
2788
3409
  // src/api/storefront-handlers.ts
2789
- var import_typeorm5 = require("typeorm");
3410
+ var import_typeorm6 = require("typeorm");
2790
3411
 
2791
3412
  // src/lib/is-valid-signup-email.ts
2792
3413
  var MAX_EMAIL = 254;
@@ -3046,7 +3667,7 @@ async function queueSms(cms, payload) {
3046
3667
 
3047
3668
  // src/lib/otp-challenge.ts
3048
3669
  var import_crypto = require("crypto");
3049
- var import_typeorm4 = require("typeorm");
3670
+ var import_typeorm5 = require("typeorm");
3050
3671
  var OTP_TTL_MS = 10 * 60 * 1e3;
3051
3672
  var MAX_SENDS_PER_HOUR = 5;
3052
3673
  var MAX_VERIFY_ATTEMPTS = 8;
@@ -3080,7 +3701,7 @@ function normalizePhoneE164(raw, defaultCountryCode) {
3080
3701
  async function countRecentOtpSends(dataSource, entityMap, purpose, identifier, since) {
3081
3702
  const repo = dataSource.getRepository(entityMap.otp_challenges);
3082
3703
  return repo.count({
3083
- where: { purpose, identifier, createdAt: (0, import_typeorm4.MoreThan)(since) }
3704
+ where: { purpose, identifier, createdAt: (0, import_typeorm5.MoreThan)(since) }
3084
3705
  });
3085
3706
  }
3086
3707
  async function createOtpChallenge(dataSource, entityMap, input) {
@@ -3094,7 +3715,7 @@ async function createOtpChallenge(dataSource, entityMap, input) {
3094
3715
  await repo.delete({
3095
3716
  purpose,
3096
3717
  identifier,
3097
- consumedAt: (0, import_typeorm4.IsNull)()
3718
+ consumedAt: (0, import_typeorm5.IsNull)()
3098
3719
  });
3099
3720
  const expiresAt = new Date(Date.now() + OTP_TTL_MS);
3100
3721
  const codeHash = hashOtpCode(code, purpose, identifier, pepper);
@@ -3115,7 +3736,7 @@ async function verifyAndConsumeOtpChallenge(dataSource, entityMap, input) {
3115
3736
  const { purpose, identifier, code, pepper } = input;
3116
3737
  const repo = dataSource.getRepository(entityMap.otp_challenges);
3117
3738
  const row = await repo.findOne({
3118
- where: { purpose, identifier, consumedAt: (0, import_typeorm4.IsNull)() },
3739
+ where: { purpose, identifier, consumedAt: (0, import_typeorm5.IsNull)() },
3119
3740
  order: { id: "DESC" }
3120
3741
  });
3121
3742
  if (!row) {
@@ -3349,7 +3970,7 @@ function createStorefrontApiHandler(config) {
3349
3970
  const u = await userRepo().findOne({ where: { id: userId } });
3350
3971
  if (!u) return null;
3351
3972
  const unclaimed = await contactRepo().findOne({
3352
- where: { email: u.email, userId: (0, import_typeorm5.IsNull)(), deleted: false }
3973
+ where: { email: u.email, userId: (0, import_typeorm6.IsNull)(), deleted: false }
3353
3974
  });
3354
3975
  if (unclaimed) {
3355
3976
  await contactRepo().update(unclaimed.id, { userId });
@@ -4390,7 +5011,7 @@ function createStorefrontApiHandler(config) {
4390
5011
  const previewByOrder = {};
4391
5012
  if (orderIds.length) {
4392
5013
  const oItems = await orderItemRepo().find({
4393
- where: { orderId: (0, import_typeorm5.In)(orderIds) },
5014
+ where: { orderId: (0, import_typeorm6.In)(orderIds) },
4394
5015
  relations: ["product"],
4395
5016
  order: { id: "ASC" }
4396
5017
  });
@@ -4523,9 +5144,11 @@ function createStorefrontApiHandler(config) {
4523
5144
  createCrudByIdHandler,
4524
5145
  createCrudHandler,
4525
5146
  createDashboardStatsHandler,
5147
+ createEcommerceAnalyticsHandler,
4526
5148
  createForgotPasswordHandler,
4527
5149
  createFormBySlugHandler,
4528
5150
  createInviteAcceptHandler,
5151
+ createMediaZipExtractHandler,
4529
5152
  createSetPasswordHandler,
4530
5153
  createSettingsApiHandlers,
4531
5154
  createStorefrontApiHandler,