@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.
- package/README.md +739 -724
- package/dist/admin.cjs +1840 -741
- package/dist/admin.cjs.map +1 -1
- package/dist/admin.d.cts +4 -0
- package/dist/admin.d.ts +4 -0
- package/dist/admin.js +1795 -681
- package/dist/admin.js.map +1 -1
- package/dist/api.cjs +700 -77
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.js +696 -75
- package/dist/api.js.map +1 -1
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/cli.cjs +21 -6
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +21 -6
- package/dist/cli.js.map +1 -1
- package/dist/hooks.cjs +159 -0
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.cts +24 -1
- package/dist/hooks.d.ts +24 -1
- package/dist/hooks.js +165 -0
- package/dist/hooks.js.map +1 -1
- package/dist/{index-BQnqJ7EO.d.cts → index-D2C1O9b4.d.cts} +22 -3
- package/dist/{index-BiagwMjV.d.ts → index-GMn7-9PX.d.ts} +22 -3
- package/dist/index.cjs +5334 -4336
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -11
- package/dist/index.d.ts +89 -11
- package/dist/index.js +5333 -4352
- package/dist/index.js.map +1 -1
- package/dist/migrations/1772178563554-InitialSchema.ts +304 -304
- package/dist/migrations/1772178563555-ChatAndKnowledgeBase.ts +55 -55
- package/dist/migrations/1772178563556-KnowledgeBaseVector.ts +16 -16
- package/dist/migrations/1774300000000-RbacSeedGroupsAndPermissionUnique.ts +24 -24
- package/dist/migrations/1774300000001-SeedAdministratorUsersPermission.ts +35 -35
- package/dist/migrations/1774400000000-CustomerAdminAccessContactUser.ts +37 -37
- package/dist/migrations/1774400000001-StorefrontCartWishlist.ts +100 -100
- package/dist/migrations/1774400000002-WishlistGuestId.ts +29 -29
- package/dist/migrations/1774500000000-ProductCollectionHsn.ts +15 -15
- package/dist/migrations/1774600000000-OrderKindParentOrderNumber.ts +36 -36
- package/dist/migrations/1774800000000-OtpChallengesUserPhone.ts +41 -41
- package/dist/migrations/1774900000000-MessageTemplates.ts +39 -39
- package/dist/migrations/1775000000000-ProductUomTypeOrderItemSnapshots.ts +29 -29
- package/dist/migrations/1775200000000-MediaDriveFolders.ts +38 -0
- package/dist/migrations/README.md +3 -3
- package/dist/theme.cjs.map +1 -1
- package/dist/theme.js.map +1 -1
- package/package.json +20 -15
- package/src/admin/admin.css +72 -72
package/dist/api.js
CHANGED
|
@@ -397,7 +397,7 @@ var init_paid_order_erp = __esm({
|
|
|
397
397
|
});
|
|
398
398
|
|
|
399
399
|
// src/api/crud.ts
|
|
400
|
-
import {
|
|
400
|
+
import { Brackets, ILike, MoreThan } from "typeorm";
|
|
401
401
|
|
|
402
402
|
// src/plugins/erp/erp-contact-sync.ts
|
|
403
403
|
init_erp_queue();
|
|
@@ -520,6 +520,28 @@ function buildSearchWhereClause(repo, search) {
|
|
|
520
520
|
if (ors.length === 0) return {};
|
|
521
521
|
return ors.length === 1 ? ors[0] : ors;
|
|
522
522
|
}
|
|
523
|
+
function entityHasSoftDelete(repo) {
|
|
524
|
+
return repo.metadata.columns.some((c) => c.propertyName === "deleted");
|
|
525
|
+
}
|
|
526
|
+
function mergeDeletedFalseWhere(repo, where) {
|
|
527
|
+
if (!entityHasSoftDelete(repo)) return where;
|
|
528
|
+
const d = { deleted: false };
|
|
529
|
+
if (Array.isArray(where)) {
|
|
530
|
+
if (where.length === 0) return [d];
|
|
531
|
+
return where.map((w) => ({ ...w, ...d }));
|
|
532
|
+
}
|
|
533
|
+
return Object.keys(where).length > 0 ? { ...where, ...d } : d;
|
|
534
|
+
}
|
|
535
|
+
function buildSoftDeletePayload(meta, deletedBy) {
|
|
536
|
+
const payload = { deleted: true };
|
|
537
|
+
if (meta.columns.some((c) => c.propertyName === "deletedAt")) {
|
|
538
|
+
payload.deletedAt = /* @__PURE__ */ new Date();
|
|
539
|
+
}
|
|
540
|
+
if (deletedBy != null && meta.columns.some((c) => c.propertyName === "deletedBy")) {
|
|
541
|
+
payload.deletedBy = deletedBy;
|
|
542
|
+
}
|
|
543
|
+
return payload;
|
|
544
|
+
}
|
|
523
545
|
function makeContactErpSync(dataSource, entityMap, getCms) {
|
|
524
546
|
return async function syncContactRowToErp(row) {
|
|
525
547
|
if (!getCms) return;
|
|
@@ -544,10 +566,11 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
544
566
|
async function authz(req, resource, action) {
|
|
545
567
|
const authError = await requireAuth(req);
|
|
546
568
|
if (authError) return authError;
|
|
547
|
-
if (reqPerm) {
|
|
548
|
-
|
|
549
|
-
if (pe) return pe;
|
|
569
|
+
if (!reqPerm) {
|
|
570
|
+
return json({ error: "Forbidden", reason: "entity_rbac_required", entity: resource, action }, { status: 403 });
|
|
550
571
|
}
|
|
572
|
+
const pe = await reqPerm(req, resource, action);
|
|
573
|
+
if (pe) return pe;
|
|
551
574
|
return null;
|
|
552
575
|
}
|
|
553
576
|
return {
|
|
@@ -583,7 +606,7 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
583
606
|
return json({ total: 0, page, limit, totalPages: 0, data: [] });
|
|
584
607
|
}
|
|
585
608
|
}
|
|
586
|
-
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);
|
|
609
|
+
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);
|
|
587
610
|
if (search && typeof search === "string" && search.trim()) {
|
|
588
611
|
const term = `%${search.trim()}%`;
|
|
589
612
|
qb.andWhere(
|
|
@@ -621,7 +644,7 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
621
644
|
const dateTo = searchParams.get("dateTo")?.trim();
|
|
622
645
|
const methodFilter = searchParams.get("method")?.trim();
|
|
623
646
|
const orderNumberParam = searchParams.get("orderNumber")?.trim();
|
|
624
|
-
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);
|
|
647
|
+
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);
|
|
625
648
|
if (search && typeof search === "string" && search.trim()) {
|
|
626
649
|
const term = `%${search.trim()}%`;
|
|
627
650
|
qb.andWhere(
|
|
@@ -652,7 +675,7 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
652
675
|
const repo2 = dataSource.getRepository(entity);
|
|
653
676
|
const statusFilter = searchParams.get("status")?.trim();
|
|
654
677
|
const inventory = searchParams.get("inventory")?.trim();
|
|
655
|
-
const productWhere = {};
|
|
678
|
+
const productWhere = { deleted: false };
|
|
656
679
|
if (statusFilter) productWhere.status = statusFilter;
|
|
657
680
|
if (inventory === "in_stock") productWhere.quantity = MoreThan(0);
|
|
658
681
|
if (inventory === "out_of_stock") productWhere.quantity = 0;
|
|
@@ -675,7 +698,7 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
675
698
|
const typeFilter2 = searchParams.get("type")?.trim();
|
|
676
699
|
const orderIdParam = searchParams.get("orderId")?.trim();
|
|
677
700
|
const includeSummary = searchParams.get("includeSummary") === "1";
|
|
678
|
-
const qb = repo2.createQueryBuilder("contact").orderBy(`contact.${sortField2}`, sortOrderContacts).skip(skip).take(limit);
|
|
701
|
+
const qb = repo2.createQueryBuilder("contact").andWhere("contact.deleted = :contactDel", { contactDel: false }).orderBy(`contact.${sortField2}`, sortOrderContacts).skip(skip).take(limit);
|
|
679
702
|
if (search && typeof search === "string" && search.trim()) {
|
|
680
703
|
const term = `%${search.trim()}%`;
|
|
681
704
|
qb.andWhere("(contact.name ILIKE :term OR contact.email ILIKE :term OR contact.phone ILIKE :term)", { term });
|
|
@@ -710,14 +733,38 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
710
733
|
const repo = dataSource.getRepository(entity);
|
|
711
734
|
const typeFilter = searchParams.get("type");
|
|
712
735
|
const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
|
|
736
|
+
if (resource === "media") {
|
|
737
|
+
const qb = repo.createQueryBuilder("m");
|
|
738
|
+
const parentIdParam = searchParams.get("parentId");
|
|
739
|
+
if (parentIdParam != null && parentIdParam !== "") {
|
|
740
|
+
const n = Number(parentIdParam);
|
|
741
|
+
if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
|
|
742
|
+
qb.where("m.deleted = :mediaDel AND m.parentId = :pid", { mediaDel: false, pid: n });
|
|
743
|
+
} else {
|
|
744
|
+
qb.where("m.deleted = :mediaDel AND m.parentId IS NULL", { mediaDel: false });
|
|
745
|
+
}
|
|
746
|
+
if (search && typeof search === "string" && search.trim()) {
|
|
747
|
+
qb.andWhere("m.filename ILIKE :search", { search: `%${search.trim()}%` });
|
|
748
|
+
}
|
|
749
|
+
if (typeFilter) {
|
|
750
|
+
qb.andWhere(
|
|
751
|
+
new Brackets((sq) => {
|
|
752
|
+
sq.where("m.kind = :folderKind", { folderKind: "folder" }).orWhere("m.mimeType LIKE :mtp", {
|
|
753
|
+
mtp: `${typeFilter}/%`
|
|
754
|
+
});
|
|
755
|
+
})
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
const allowedSort = ["filename", "createdAt", "id"];
|
|
759
|
+
const sf = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "filename";
|
|
760
|
+
const so = sortOrder === "DESC" ? "DESC" : "ASC";
|
|
761
|
+
qb.orderBy("CASE WHEN m.kind = :fk THEN 0 ELSE 1 END", "ASC").addOrderBy(`m.${sf}`, so).setParameter("fk", "folder").skip(skip).take(limit);
|
|
762
|
+
const [data2, total2] = await qb.getManyAndCount();
|
|
763
|
+
return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
|
|
764
|
+
}
|
|
713
765
|
const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
|
|
714
766
|
let where = {};
|
|
715
|
-
if (
|
|
716
|
-
const mediaWhere = {};
|
|
717
|
-
if (search) mediaWhere.filename = ILike(`%${search}%`);
|
|
718
|
-
if (typeFilter) mediaWhere.mimeType = Like(`${typeFilter}/%`);
|
|
719
|
-
where = Object.keys(mediaWhere).length > 0 ? mediaWhere : {};
|
|
720
|
-
} else if (search) {
|
|
767
|
+
if (search) {
|
|
721
768
|
where = buildSearchWhereClause(repo, search);
|
|
722
769
|
}
|
|
723
770
|
const intFilterKeys = ["productId", "attributeId", "taxId"];
|
|
@@ -738,6 +785,7 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
738
785
|
where = extraWhere;
|
|
739
786
|
}
|
|
740
787
|
}
|
|
788
|
+
where = mergeDeletedFalseWhere(repo, where);
|
|
741
789
|
const [data, total] = await repo.findAndCount({
|
|
742
790
|
skip,
|
|
743
791
|
take: limit,
|
|
@@ -757,6 +805,38 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
757
805
|
if (!body || typeof body !== "object" || Object.keys(body).length === 0) {
|
|
758
806
|
return json({ error: "Invalid request payload" }, { status: 400 });
|
|
759
807
|
}
|
|
808
|
+
if (resource === "media") {
|
|
809
|
+
const b = body;
|
|
810
|
+
const kind = b.kind === "folder" ? "folder" : "file";
|
|
811
|
+
b.kind = kind;
|
|
812
|
+
const fn = String(b.filename ?? "").trim().slice(0, 255);
|
|
813
|
+
if (!fn) return json({ error: "filename required" }, { status: 400 });
|
|
814
|
+
b.filename = fn;
|
|
815
|
+
let pid = null;
|
|
816
|
+
if (b.parentId != null && b.parentId !== "") {
|
|
817
|
+
const n = Number(b.parentId);
|
|
818
|
+
if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
|
|
819
|
+
pid = n;
|
|
820
|
+
}
|
|
821
|
+
b.parentId = pid;
|
|
822
|
+
const mediaRepo = dataSource.getRepository(entityMap.media);
|
|
823
|
+
if (pid != null) {
|
|
824
|
+
const parent = await mediaRepo.findOne({ where: { id: pid } });
|
|
825
|
+
if (!parent || parent.kind !== "folder") {
|
|
826
|
+
return json({ error: "parent must be a folder" }, { status: 400 });
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (kind === "folder") {
|
|
830
|
+
b.url = null;
|
|
831
|
+
b.mimeType = "inode/directory";
|
|
832
|
+
b.size = 0;
|
|
833
|
+
} else {
|
|
834
|
+
if (!b.url || typeof b.url !== "string") return json({ error: "url required for files" }, { status: 400 });
|
|
835
|
+
if (!b.mimeType || typeof b.mimeType !== "string") {
|
|
836
|
+
b.mimeType = "application/octet-stream";
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
760
840
|
const repo = dataSource.getRepository(entity);
|
|
761
841
|
sanitizeBodyForEntity(repo, body);
|
|
762
842
|
const created = await repo.save(repo.create(body));
|
|
@@ -873,15 +953,16 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
873
953
|
};
|
|
874
954
|
}
|
|
875
955
|
function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
876
|
-
const { requireAuth, json, requireEntityPermission: reqPerm, getCms } = options;
|
|
956
|
+
const { requireAuth, json, requireEntityPermission: reqPerm, getCms, getDeletedByUserId } = options;
|
|
877
957
|
const syncContactRowToErp = makeContactErpSync(dataSource, entityMap, getCms);
|
|
878
958
|
async function authz(req, resource, action) {
|
|
879
959
|
const authError = await requireAuth(req);
|
|
880
960
|
if (authError) return authError;
|
|
881
|
-
if (reqPerm) {
|
|
882
|
-
|
|
883
|
-
if (pe) return pe;
|
|
961
|
+
if (!reqPerm) {
|
|
962
|
+
return json({ error: "Forbidden", reason: "entity_rbac_required", entity: resource, action }, { status: 403 });
|
|
884
963
|
}
|
|
964
|
+
const pe = await reqPerm(req, resource, action);
|
|
965
|
+
if (pe) return pe;
|
|
885
966
|
return null;
|
|
886
967
|
}
|
|
887
968
|
return {
|
|
@@ -893,7 +974,7 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
893
974
|
const repo = dataSource.getRepository(entity);
|
|
894
975
|
if (resource === "orders") {
|
|
895
976
|
const order = await repo.findOne({
|
|
896
|
-
where: { id: Number(id) },
|
|
977
|
+
where: { id: Number(id), deleted: false },
|
|
897
978
|
relations: ["contact", "billingAddress", "shippingAddress", "items", "items.product", "items.product.collection", "payments"]
|
|
898
979
|
});
|
|
899
980
|
if (!order) return json({ message: "Not found" }, { status: 404 });
|
|
@@ -905,7 +986,7 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
905
986
|
}
|
|
906
987
|
if (resource === "contacts") {
|
|
907
988
|
const contact = await repo.findOne({
|
|
908
|
-
where: { id: Number(id) },
|
|
989
|
+
where: { id: Number(id), deleted: false },
|
|
909
990
|
relations: ["form_submissions", "form_submissions.form", "orders", "payments", "addresses"]
|
|
910
991
|
});
|
|
911
992
|
if (!contact) return json({ message: "Not found" }, { status: 404 });
|
|
@@ -927,7 +1008,7 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
927
1008
|
}
|
|
928
1009
|
if (resource === "payments") {
|
|
929
1010
|
const payment = await repo.findOne({
|
|
930
|
-
where: { id: Number(id) },
|
|
1011
|
+
where: { id: Number(id), deleted: false },
|
|
931
1012
|
relations: ["order", "order.contact", "contact"]
|
|
932
1013
|
});
|
|
933
1014
|
if (!payment) return json({ message: "Not found" }, { status: 404 });
|
|
@@ -935,12 +1016,13 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
935
1016
|
}
|
|
936
1017
|
if (resource === "blogs") {
|
|
937
1018
|
const blog = await repo.findOne({
|
|
938
|
-
where: { id: Number(id) },
|
|
1019
|
+
where: { id: Number(id), deleted: false },
|
|
939
1020
|
relations: ["category", "seo", "tags"]
|
|
940
1021
|
});
|
|
941
1022
|
return blog ? json(blog) : json({ message: "Not found" }, { status: 404 });
|
|
942
1023
|
}
|
|
943
|
-
const
|
|
1024
|
+
const idWhere = entityHasSoftDelete(repo) ? { id: Number(id), deleted: false } : { id: Number(id) };
|
|
1025
|
+
const item = await repo.findOne({ where: idWhere });
|
|
944
1026
|
return item ? json(item) : json({ message: "Not found" }, { status: 404 });
|
|
945
1027
|
},
|
|
946
1028
|
async PUT(req, resource, id) {
|
|
@@ -952,7 +1034,9 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
952
1034
|
const repo = dataSource.getRepository(entity);
|
|
953
1035
|
const numericId = Number(id);
|
|
954
1036
|
if (resource === "blogs" && rawBody && typeof rawBody === "object" && entityMap.categories && entityMap.seos && entityMap.tags) {
|
|
955
|
-
const existing = await repo.findOne({
|
|
1037
|
+
const existing = await repo.findOne({
|
|
1038
|
+
where: { id: numericId, deleted: false }
|
|
1039
|
+
});
|
|
956
1040
|
if (!existing) return json({ message: "Not found" }, { status: 404 });
|
|
957
1041
|
const updatePayload2 = pickColumnUpdates(repo, rawBody);
|
|
958
1042
|
if ("category" in rawBody) {
|
|
@@ -1028,7 +1112,18 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
1028
1112
|
});
|
|
1029
1113
|
return updated2 ? json(updated2) : json({ message: "Not found" }, { status: 404 });
|
|
1030
1114
|
}
|
|
1115
|
+
if (entityHasSoftDelete(repo)) {
|
|
1116
|
+
const cur = await repo.findOne({
|
|
1117
|
+
where: { id: numericId, deleted: false }
|
|
1118
|
+
});
|
|
1119
|
+
if (!cur) return json({ message: "Not found" }, { status: 404 });
|
|
1120
|
+
}
|
|
1031
1121
|
const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
|
|
1122
|
+
if (resource === "media") {
|
|
1123
|
+
const u = updatePayload;
|
|
1124
|
+
delete u.parentId;
|
|
1125
|
+
delete u.kind;
|
|
1126
|
+
}
|
|
1032
1127
|
if (Object.keys(updatePayload).length > 0) {
|
|
1033
1128
|
sanitizeBodyForEntity(repo, updatePayload);
|
|
1034
1129
|
await repo.update(numericId, updatePayload);
|
|
@@ -1049,7 +1144,24 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
1049
1144
|
const entity = entityMap[resource];
|
|
1050
1145
|
if (!entity) return json({ error: "Invalid resource" }, { status: 400 });
|
|
1051
1146
|
const repo = dataSource.getRepository(entity);
|
|
1052
|
-
const
|
|
1147
|
+
const numericId = Number(id);
|
|
1148
|
+
if (entityHasSoftDelete(repo)) {
|
|
1149
|
+
const existing = await repo.findOne({
|
|
1150
|
+
where: { id: numericId, deleted: false }
|
|
1151
|
+
});
|
|
1152
|
+
if (!existing) return json({ message: "Not found" }, { status: 404 });
|
|
1153
|
+
let deletedBy = null;
|
|
1154
|
+
if (getDeletedByUserId) {
|
|
1155
|
+
try {
|
|
1156
|
+
deletedBy = await getDeletedByUserId(req);
|
|
1157
|
+
} catch {
|
|
1158
|
+
deletedBy = null;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
await repo.update(numericId, buildSoftDeletePayload(repo.metadata, deletedBy));
|
|
1162
|
+
return json({ message: "Deleted successfully" }, { status: 200 });
|
|
1163
|
+
}
|
|
1164
|
+
const result = await repo.delete(numericId);
|
|
1053
1165
|
if (result.affected === 0) return json({ message: "Not found" }, { status: 404 });
|
|
1054
1166
|
return json({ message: "Deleted successfully" }, { status: 200 });
|
|
1055
1167
|
}
|
|
@@ -1205,7 +1317,7 @@ function createUserAuthApiRouter(config) {
|
|
|
1205
1317
|
// src/api/cms-handlers.ts
|
|
1206
1318
|
init_email_queue();
|
|
1207
1319
|
init_erp_queue();
|
|
1208
|
-
import { MoreThanOrEqual, ILike as ILike2 } from "typeorm";
|
|
1320
|
+
import { MoreThanOrEqual, ILike as ILike2, In } from "typeorm";
|
|
1209
1321
|
|
|
1210
1322
|
// src/plugins/captcha/assert.ts
|
|
1211
1323
|
async function assertCaptchaOk(getCms, body, req, json) {
|
|
@@ -1223,6 +1335,194 @@ async function assertCaptchaOk(getCms, body, req, json) {
|
|
|
1223
1335
|
return json({ error: result.message }, { status: result.status });
|
|
1224
1336
|
}
|
|
1225
1337
|
|
|
1338
|
+
// src/lib/media-folder-path.ts
|
|
1339
|
+
function sanitizeMediaFolderPath(input) {
|
|
1340
|
+
if (input == null) return "";
|
|
1341
|
+
if (typeof input !== "string") return "";
|
|
1342
|
+
const segments = input.replace(/\\/g, "/").split("/").map((s) => s.trim()).filter(Boolean).filter((s) => s !== ".." && s !== ".");
|
|
1343
|
+
const joined = segments.join("/");
|
|
1344
|
+
return joined.length > 512 ? joined.slice(0, 512) : joined;
|
|
1345
|
+
}
|
|
1346
|
+
function sanitizeStorageSegment(name) {
|
|
1347
|
+
const s = name.replace(/[/\\]/g, "-").trim().slice(0, 255);
|
|
1348
|
+
return s || "item";
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/lib/media-parent-path.ts
|
|
1352
|
+
async function relativePathFromMediaParentId(dataSource, entityMap, parentId) {
|
|
1353
|
+
if (parentId == null) return "";
|
|
1354
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1355
|
+
const segments = [];
|
|
1356
|
+
let id = parentId;
|
|
1357
|
+
for (let d = 0; d < 64 && id != null; d++) {
|
|
1358
|
+
const row = await repo.findOne({ where: { id } });
|
|
1359
|
+
if (!row) break;
|
|
1360
|
+
const m = row;
|
|
1361
|
+
if (m.kind !== "folder") break;
|
|
1362
|
+
segments.unshift(sanitizeStorageSegment(m.filename));
|
|
1363
|
+
id = m.parentId ?? null;
|
|
1364
|
+
}
|
|
1365
|
+
return segments.join("/");
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// src/lib/media-zip-extract.ts
|
|
1369
|
+
import { IsNull as IsNull2 } from "typeorm";
|
|
1370
|
+
var ZIP_MIME_TYPES = /* @__PURE__ */ new Set(["application/zip", "application/x-zip-compressed"]);
|
|
1371
|
+
var MAX_ENTRIES = 2e3;
|
|
1372
|
+
var MAX_TOTAL_UNCOMPRESSED = 80 * 1024 * 1024;
|
|
1373
|
+
function isZipMedia(mime, filename) {
|
|
1374
|
+
if (mime && ZIP_MIME_TYPES.has(mime)) return true;
|
|
1375
|
+
return filename.toLowerCase().endsWith(".zip");
|
|
1376
|
+
}
|
|
1377
|
+
async function readBufferFromPublicUrl(url) {
|
|
1378
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
1379
|
+
const r = await fetch(url);
|
|
1380
|
+
if (!r.ok) throw new Error("Failed to download file");
|
|
1381
|
+
return Buffer.from(await r.arrayBuffer());
|
|
1382
|
+
}
|
|
1383
|
+
if (url.startsWith("/")) {
|
|
1384
|
+
const { readFile } = await import("fs/promises");
|
|
1385
|
+
const { join } = await import("path");
|
|
1386
|
+
const rel = url.replace(/^\/+/, "");
|
|
1387
|
+
return readFile(join(process.cwd(), "public", rel));
|
|
1388
|
+
}
|
|
1389
|
+
throw new Error("Unsupported media URL");
|
|
1390
|
+
}
|
|
1391
|
+
function sanitizeZipPath(entryName) {
|
|
1392
|
+
const norm = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
1393
|
+
for (const seg of norm) {
|
|
1394
|
+
if (seg === ".." || seg === ".") return null;
|
|
1395
|
+
}
|
|
1396
|
+
return norm;
|
|
1397
|
+
}
|
|
1398
|
+
function shouldSkipEntry(parts) {
|
|
1399
|
+
if (parts[0] === "__MACOSX") return true;
|
|
1400
|
+
const last = parts[parts.length - 1];
|
|
1401
|
+
if (last === ".DS_Store") return true;
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
1404
|
+
function guessMimeType(fileName) {
|
|
1405
|
+
const lower = fileName.toLowerCase();
|
|
1406
|
+
if (lower.endsWith(".png")) return "image/png";
|
|
1407
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
1408
|
+
if (lower.endsWith(".gif")) return "image/gif";
|
|
1409
|
+
if (lower.endsWith(".webp")) return "image/webp";
|
|
1410
|
+
if (lower.endsWith(".svg")) return "image/svg+xml";
|
|
1411
|
+
if (lower.endsWith(".pdf")) return "application/pdf";
|
|
1412
|
+
if (lower.endsWith(".txt")) return "text/plain";
|
|
1413
|
+
if (lower.endsWith(".json")) return "application/json";
|
|
1414
|
+
if (lower.endsWith(".zip")) return "application/zip";
|
|
1415
|
+
return "application/octet-stream";
|
|
1416
|
+
}
|
|
1417
|
+
async function findOrCreateFolder(dataSource, entityMap, parentId, name) {
|
|
1418
|
+
const safe = sanitizeStorageSegment(name);
|
|
1419
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1420
|
+
const where = parentId == null ? { kind: "folder", filename: safe, parentId: IsNull2() } : { kind: "folder", filename: safe, parentId };
|
|
1421
|
+
const existing = await repo.findOne({ where });
|
|
1422
|
+
if (existing) return existing.id;
|
|
1423
|
+
const row = await repo.save(
|
|
1424
|
+
repo.create({
|
|
1425
|
+
kind: "folder",
|
|
1426
|
+
parentId,
|
|
1427
|
+
filename: safe,
|
|
1428
|
+
url: null,
|
|
1429
|
+
mimeType: "inode/directory",
|
|
1430
|
+
size: 0,
|
|
1431
|
+
alt: null,
|
|
1432
|
+
isPublic: false,
|
|
1433
|
+
deleted: false
|
|
1434
|
+
})
|
|
1435
|
+
);
|
|
1436
|
+
return row.id;
|
|
1437
|
+
}
|
|
1438
|
+
async function ensureFolderChain(dataSource, entityMap, rootParentId, pathSegments) {
|
|
1439
|
+
let pid = rootParentId;
|
|
1440
|
+
for (const seg of pathSegments) {
|
|
1441
|
+
if (!seg) continue;
|
|
1442
|
+
pid = await findOrCreateFolder(dataSource, entityMap, pid, seg);
|
|
1443
|
+
}
|
|
1444
|
+
return pid;
|
|
1445
|
+
}
|
|
1446
|
+
async function extractZipMediaIntoParentTree(opts) {
|
|
1447
|
+
const { dataSource, entityMap, zipMediaRow } = opts;
|
|
1448
|
+
const row = zipMediaRow;
|
|
1449
|
+
if (row.kind !== "file" || !row.url) throw new Error("Not a file");
|
|
1450
|
+
if (!isZipMedia(row.mimeType, row.filename)) throw new Error("Not a zip archive");
|
|
1451
|
+
const buffer = await readBufferFromPublicUrl(row.url);
|
|
1452
|
+
const { default: AdmZip } = await import("adm-zip");
|
|
1453
|
+
const zip = new AdmZip(buffer);
|
|
1454
|
+
const entries = zip.getEntries();
|
|
1455
|
+
if (entries.length > MAX_ENTRIES) throw new Error(`Too many zip entries (max ${MAX_ENTRIES})`);
|
|
1456
|
+
const rootParentId = row.parentId;
|
|
1457
|
+
const items = [];
|
|
1458
|
+
let totalUncompressed = 0;
|
|
1459
|
+
for (const e of entries) {
|
|
1460
|
+
const raw = e.entryName;
|
|
1461
|
+
const parts = sanitizeZipPath(raw);
|
|
1462
|
+
if (!parts || shouldSkipEntry(parts)) continue;
|
|
1463
|
+
const isDir = e.isDirectory || /\/$/.test(raw);
|
|
1464
|
+
let data = null;
|
|
1465
|
+
if (!isDir) {
|
|
1466
|
+
data = e.getData();
|
|
1467
|
+
totalUncompressed += data.length;
|
|
1468
|
+
if (totalUncompressed > MAX_TOTAL_UNCOMPRESSED) {
|
|
1469
|
+
throw new Error(`Uncompressed content exceeds limit (${MAX_TOTAL_UNCOMPRESSED} bytes)`);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
items.push({ parts, isDir, data });
|
|
1473
|
+
}
|
|
1474
|
+
items.sort((a, b) => {
|
|
1475
|
+
const da = a.parts.length;
|
|
1476
|
+
const db = b.parts.length;
|
|
1477
|
+
if (da !== db) return da - db;
|
|
1478
|
+
return a.parts.join("/").localeCompare(b.parts.join("/"));
|
|
1479
|
+
});
|
|
1480
|
+
let files = 0;
|
|
1481
|
+
let folderEntries = 0;
|
|
1482
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1483
|
+
for (const it of items) {
|
|
1484
|
+
if (it.isDir) {
|
|
1485
|
+
await ensureFolderChain(dataSource, entityMap, rootParentId, it.parts);
|
|
1486
|
+
folderEntries++;
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
const fileName = it.parts[it.parts.length - 1];
|
|
1490
|
+
const dirParts = it.parts.slice(0, -1);
|
|
1491
|
+
const parentFolderId = await ensureFolderChain(dataSource, entityMap, rootParentId, dirParts);
|
|
1492
|
+
const buf = it.data;
|
|
1493
|
+
const relBase = await relativePathFromMediaParentId(dataSource, entityMap, parentFolderId);
|
|
1494
|
+
const relativeUnderUploads = relBase ? `${relBase}/${fileName}` : fileName;
|
|
1495
|
+
const contentType = guessMimeType(fileName);
|
|
1496
|
+
let publicUrl;
|
|
1497
|
+
if (opts.storage) {
|
|
1498
|
+
publicUrl = await opts.storage.upload(buf, `uploads/${relativeUnderUploads}`, contentType);
|
|
1499
|
+
} else {
|
|
1500
|
+
const fs = await import("fs/promises");
|
|
1501
|
+
const pathMod = await import("path");
|
|
1502
|
+
const dir = pathMod.join(process.cwd(), opts.localUploadDir);
|
|
1503
|
+
const filePath = pathMod.join(dir, relativeUnderUploads);
|
|
1504
|
+
await fs.mkdir(pathMod.dirname(filePath), { recursive: true });
|
|
1505
|
+
await fs.writeFile(filePath, buf);
|
|
1506
|
+
publicUrl = `/${opts.localUploadDir.replace(/^\/+/, "").replace(/\\/g, "/")}/${relativeUnderUploads.replace(/\\/g, "/")}`;
|
|
1507
|
+
}
|
|
1508
|
+
await repo.save(
|
|
1509
|
+
repo.create({
|
|
1510
|
+
kind: "file",
|
|
1511
|
+
parentId: parentFolderId,
|
|
1512
|
+
filename: fileName,
|
|
1513
|
+
url: publicUrl,
|
|
1514
|
+
mimeType: contentType,
|
|
1515
|
+
size: buf.length,
|
|
1516
|
+
alt: null,
|
|
1517
|
+
isPublic: false,
|
|
1518
|
+
deleted: false
|
|
1519
|
+
})
|
|
1520
|
+
);
|
|
1521
|
+
files++;
|
|
1522
|
+
}
|
|
1523
|
+
return { files, folderEntries };
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1226
1526
|
// src/api/cms-handlers.ts
|
|
1227
1527
|
function createDashboardStatsHandler(config) {
|
|
1228
1528
|
const { dataSource, entityMap, json, requireAuth, requirePermission, requireEntityPermission } = config;
|
|
@@ -1240,26 +1540,209 @@ function createDashboardStatsHandler(config) {
|
|
|
1240
1540
|
try {
|
|
1241
1541
|
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
|
|
1242
1542
|
const repo = (name) => entityMap[name] ? dataSource.getRepository(entityMap[name]) : void 0;
|
|
1243
|
-
const [contactsCount, formsCount, formSubmissionsCount, usersCount, blogsCount, recentContacts, recentSubmissions] = await Promise.all([
|
|
1543
|
+
const [contactsCount, formsCount, formSubmissionsCount, usersCount, blogsCount, recentContacts, recentSubmissions, contactTypeRows] = await Promise.all([
|
|
1244
1544
|
repo("contacts")?.count() ?? 0,
|
|
1245
1545
|
repo("forms")?.count({ where: { deleted: false } }) ?? 0,
|
|
1246
1546
|
repo("form_submissions")?.count() ?? 0,
|
|
1247
1547
|
repo("users")?.count({ where: { deleted: false } }) ?? 0,
|
|
1248
1548
|
repo("blogs")?.count({ where: { deleted: false } }) ?? 0,
|
|
1249
1549
|
repo("contacts")?.count({ where: { createdAt: MoreThanOrEqual(sevenDaysAgo) } }) ?? 0,
|
|
1250
|
-
repo("form_submissions")?.count({ where: { createdAt: MoreThanOrEqual(sevenDaysAgo) } }) ?? 0
|
|
1550
|
+
repo("form_submissions")?.count({ where: { createdAt: MoreThanOrEqual(sevenDaysAgo) } }) ?? 0,
|
|
1551
|
+
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() ?? []
|
|
1251
1552
|
]);
|
|
1252
1553
|
return json({
|
|
1253
1554
|
contacts: { total: contactsCount, recent: recentContacts },
|
|
1254
1555
|
forms: { total: formsCount, submissions: formSubmissionsCount, recentSubmissions },
|
|
1255
1556
|
users: usersCount,
|
|
1256
|
-
blogs: blogsCount
|
|
1557
|
+
blogs: blogsCount,
|
|
1558
|
+
contactTypes: (contactTypeRows ?? []).map((row) => ({
|
|
1559
|
+
type: row.type || "unknown",
|
|
1560
|
+
count: Number(row.count || 0)
|
|
1561
|
+
}))
|
|
1257
1562
|
});
|
|
1258
1563
|
} catch (err) {
|
|
1259
1564
|
return json({ error: "Failed to fetch dashboard stats" }, { status: 500 });
|
|
1260
1565
|
}
|
|
1261
1566
|
};
|
|
1262
1567
|
}
|
|
1568
|
+
function toNum(v) {
|
|
1569
|
+
const n = typeof v === "number" ? v : Number(v ?? 0);
|
|
1570
|
+
return Number.isFinite(n) ? n : 0;
|
|
1571
|
+
}
|
|
1572
|
+
function toIsoDate(d) {
|
|
1573
|
+
return d.toISOString().slice(0, 10);
|
|
1574
|
+
}
|
|
1575
|
+
function createEcommerceAnalyticsHandler(config) {
|
|
1576
|
+
const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
|
|
1577
|
+
return async function GET(req) {
|
|
1578
|
+
const authErr = await requireAuth(req);
|
|
1579
|
+
if (authErr) return authErr;
|
|
1580
|
+
if (requireEntityPermission) {
|
|
1581
|
+
const pe = await requireEntityPermission(req, "analytics", "read");
|
|
1582
|
+
if (pe) return pe;
|
|
1583
|
+
}
|
|
1584
|
+
if (!entityMap.orders || !entityMap.order_items || !entityMap.payments || !entityMap.products) {
|
|
1585
|
+
return json({ error: "Store analytics unavailable" }, { status: 404 });
|
|
1586
|
+
}
|
|
1587
|
+
try {
|
|
1588
|
+
const url = new URL(req.url);
|
|
1589
|
+
const rawDays = parseInt(url.searchParams.get("days") || "30", 10);
|
|
1590
|
+
const days = Number.isFinite(rawDays) ? Math.min(365, Math.max(7, rawDays)) : 30;
|
|
1591
|
+
const end = /* @__PURE__ */ new Date();
|
|
1592
|
+
const start = new Date(end.getTime() - days * 24 * 60 * 60 * 1e3);
|
|
1593
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
1594
|
+
const paymentRepo = dataSource.getRepository(entityMap.payments);
|
|
1595
|
+
const itemRepo = dataSource.getRepository(entityMap.order_items);
|
|
1596
|
+
const productRepo = dataSource.getRepository(entityMap.products);
|
|
1597
|
+
const [salesOrders, returnOrders, replacementOrders, payments, products] = await Promise.all([
|
|
1598
|
+
orderRepo.find({
|
|
1599
|
+
where: { deleted: false, createdAt: MoreThanOrEqual(start), orderKind: "sale", status: In(["confirmed", "processing", "completed"]) },
|
|
1600
|
+
select: ["id", "contactId", "createdAt", "subtotal", "discount", "tax", "total", "status"]
|
|
1601
|
+
}),
|
|
1602
|
+
orderRepo.find({
|
|
1603
|
+
where: { deleted: false, createdAt: MoreThanOrEqual(start), orderKind: "return" },
|
|
1604
|
+
select: ["id", "createdAt", "total"]
|
|
1605
|
+
}),
|
|
1606
|
+
orderRepo.find({
|
|
1607
|
+
where: { deleted: false, createdAt: MoreThanOrEqual(start), orderKind: "replacement" },
|
|
1608
|
+
select: ["id", "createdAt", "total"]
|
|
1609
|
+
}),
|
|
1610
|
+
paymentRepo.find({
|
|
1611
|
+
where: { deleted: false, createdAt: MoreThanOrEqual(start) },
|
|
1612
|
+
select: ["id", "status", "method", "amount", "createdAt"]
|
|
1613
|
+
}),
|
|
1614
|
+
productRepo.find({
|
|
1615
|
+
where: { deleted: false },
|
|
1616
|
+
select: ["id", "name", "quantity"]
|
|
1617
|
+
})
|
|
1618
|
+
]);
|
|
1619
|
+
const saleOrderIds = salesOrders.map((o) => o.id);
|
|
1620
|
+
const orderItems = saleOrderIds.length ? await itemRepo.find({
|
|
1621
|
+
where: { orderId: In(saleOrderIds) },
|
|
1622
|
+
select: ["id", "orderId", "productId", "quantity", "total"]
|
|
1623
|
+
}) : [];
|
|
1624
|
+
const grossSales = salesOrders.reduce((sum, o) => sum + toNum(o.subtotal), 0);
|
|
1625
|
+
const discounts = salesOrders.reduce((sum, o) => sum + toNum(o.discount), 0);
|
|
1626
|
+
const taxes = salesOrders.reduce((sum, o) => sum + toNum(o.tax), 0);
|
|
1627
|
+
const returnsValue = returnOrders.reduce((sum, o) => sum + toNum(o.total), 0);
|
|
1628
|
+
const replacementsValue = replacementOrders.reduce((sum, o) => sum + toNum(o.total), 0);
|
|
1629
|
+
const netSales = grossSales - discounts - returnsValue;
|
|
1630
|
+
const ordersCount = salesOrders.length;
|
|
1631
|
+
const aov = ordersCount > 0 ? netSales / ordersCount : 0;
|
|
1632
|
+
const returnRate = ordersCount > 0 ? returnOrders.length / ordersCount * 100 : 0;
|
|
1633
|
+
const salesByDate = /* @__PURE__ */ new Map();
|
|
1634
|
+
const returnsByDate = /* @__PURE__ */ new Map();
|
|
1635
|
+
for (const o of salesOrders) {
|
|
1636
|
+
const key = toIsoDate(new Date(o.createdAt));
|
|
1637
|
+
const row = salesByDate.get(key) ?? { value: 0, orders: 0 };
|
|
1638
|
+
row.value += toNum(o.total);
|
|
1639
|
+
row.orders += 1;
|
|
1640
|
+
salesByDate.set(key, row);
|
|
1641
|
+
}
|
|
1642
|
+
for (const o of returnOrders) {
|
|
1643
|
+
const key = toIsoDate(new Date(o.createdAt));
|
|
1644
|
+
const row = returnsByDate.get(key) ?? { value: 0, count: 0 };
|
|
1645
|
+
row.value += toNum(o.total);
|
|
1646
|
+
row.count += 1;
|
|
1647
|
+
returnsByDate.set(key, row);
|
|
1648
|
+
}
|
|
1649
|
+
const salesOverTime = [];
|
|
1650
|
+
const returnsTrend = [];
|
|
1651
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
1652
|
+
const d = new Date(end.getTime() - i * 24 * 60 * 60 * 1e3);
|
|
1653
|
+
const key = toIsoDate(d);
|
|
1654
|
+
const sales = salesByDate.get(key) ?? { value: 0, orders: 0 };
|
|
1655
|
+
const returns = returnsByDate.get(key) ?? { value: 0, count: 0 };
|
|
1656
|
+
salesOverTime.push({ date: key, value: Number(sales.value.toFixed(2)), orders: sales.orders });
|
|
1657
|
+
returnsTrend.push({ date: key, value: Number(returns.value.toFixed(2)), count: returns.count });
|
|
1658
|
+
}
|
|
1659
|
+
const productNameMap = /* @__PURE__ */ new Map();
|
|
1660
|
+
for (const p of products) productNameMap.set(Number(p.id), (p.name || `Product #${p.id}`).trim());
|
|
1661
|
+
const productAgg = /* @__PURE__ */ new Map();
|
|
1662
|
+
for (const item of orderItems) {
|
|
1663
|
+
const productId = Number(item.productId);
|
|
1664
|
+
const productName = productNameMap.get(productId) || `Product #${productId}`;
|
|
1665
|
+
const row = productAgg.get(productId) ?? { name: productName, units: 0, sales: 0 };
|
|
1666
|
+
row.units += toNum(item.quantity);
|
|
1667
|
+
row.sales += toNum(item.total);
|
|
1668
|
+
productAgg.set(productId, row);
|
|
1669
|
+
}
|
|
1670
|
+
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)) }));
|
|
1671
|
+
const allSaleOrderContactIds = Array.from(new Set(salesOrders.map((o) => Number(o.contactId)).filter((n) => Number.isInteger(n) && n > 0)));
|
|
1672
|
+
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() : [];
|
|
1673
|
+
const countMap = /* @__PURE__ */ new Map();
|
|
1674
|
+
for (const c of allTimeCounts) countMap.set(Number(c.contactId), Number(c.total));
|
|
1675
|
+
const purchasingCustomers = allSaleOrderContactIds.length;
|
|
1676
|
+
const returningCustomers = allSaleOrderContactIds.filter((id) => (countMap.get(id) ?? 0) > 1).length;
|
|
1677
|
+
const newCustomers = Math.max(0, purchasingCustomers - returningCustomers);
|
|
1678
|
+
const returningCustomerRate = purchasingCustomers > 0 ? returningCustomers / purchasingCustomers * 100 : 0;
|
|
1679
|
+
const totalPayments = payments.length;
|
|
1680
|
+
const completedPayments = payments.filter((p) => p.status === "completed").length;
|
|
1681
|
+
const failedPayments = payments.filter((p) => p.status === "failed").length;
|
|
1682
|
+
const paymentSuccessRate = totalPayments > 0 ? completedPayments / totalPayments * 100 : 0;
|
|
1683
|
+
const paymentMethodMap = /* @__PURE__ */ new Map();
|
|
1684
|
+
for (const p of payments) {
|
|
1685
|
+
const method = (p.method || "unknown").toLowerCase();
|
|
1686
|
+
const row = paymentMethodMap.get(method) ?? { method, count: 0, amount: 0 };
|
|
1687
|
+
row.count += 1;
|
|
1688
|
+
row.amount += toNum(p.amount);
|
|
1689
|
+
paymentMethodMap.set(method, row);
|
|
1690
|
+
}
|
|
1691
|
+
const paymentMethods = Array.from(paymentMethodMap.values()).sort((a, b) => b.count - a.count).map((p) => ({ ...p, amount: Number(p.amount.toFixed(2)) }));
|
|
1692
|
+
const totalInventory = products.reduce((sum, p) => sum + toNum(p.quantity), 0);
|
|
1693
|
+
const outOfStockCount = products.filter((p) => toNum(p.quantity) <= 0).length;
|
|
1694
|
+
const lowStockCount = products.filter((p) => toNum(p.quantity) > 0 && toNum(p.quantity) <= 5).length;
|
|
1695
|
+
const inventoryRisk = {
|
|
1696
|
+
outOfStockCount,
|
|
1697
|
+
lowStockCount,
|
|
1698
|
+
totalInventory
|
|
1699
|
+
};
|
|
1700
|
+
return json({
|
|
1701
|
+
rangeDays: days,
|
|
1702
|
+
kpis: {
|
|
1703
|
+
netSales: Number(netSales.toFixed(2)),
|
|
1704
|
+
grossSales: Number(grossSales.toFixed(2)),
|
|
1705
|
+
ordersPlaced: ordersCount,
|
|
1706
|
+
averageOrderValue: Number(aov.toFixed(2)),
|
|
1707
|
+
returningCustomerRate: Number(returningCustomerRate.toFixed(2)),
|
|
1708
|
+
returnRate: Number(returnRate.toFixed(2)),
|
|
1709
|
+
returnValue: Number(returnsValue.toFixed(2)),
|
|
1710
|
+
discounts: Number(discounts.toFixed(2)),
|
|
1711
|
+
taxes: Number(taxes.toFixed(2)),
|
|
1712
|
+
paymentSuccessRate: Number(paymentSuccessRate.toFixed(2))
|
|
1713
|
+
},
|
|
1714
|
+
salesOverTime,
|
|
1715
|
+
topProducts,
|
|
1716
|
+
customerMix: {
|
|
1717
|
+
newCustomers,
|
|
1718
|
+
returningCustomers,
|
|
1719
|
+
repeatPurchaseRate: Number(returningCustomerRate.toFixed(2))
|
|
1720
|
+
},
|
|
1721
|
+
returnsTrend,
|
|
1722
|
+
paymentPerformance: {
|
|
1723
|
+
successCount: completedPayments,
|
|
1724
|
+
failedCount: failedPayments,
|
|
1725
|
+
successRate: Number(paymentSuccessRate.toFixed(2)),
|
|
1726
|
+
methods: paymentMethods
|
|
1727
|
+
},
|
|
1728
|
+
conversionProxy: {
|
|
1729
|
+
sessions: 0,
|
|
1730
|
+
checkoutStarted: 0,
|
|
1731
|
+
ordersPlaced: ordersCount
|
|
1732
|
+
},
|
|
1733
|
+
salesBreakdown: {
|
|
1734
|
+
sales: { count: ordersCount, value: Number(grossSales.toFixed(2)) },
|
|
1735
|
+
returns: { count: returnOrders.length, value: Number(returnsValue.toFixed(2)) },
|
|
1736
|
+
replacements: { count: replacementOrders.length, value: Number(replacementsValue.toFixed(2)) }
|
|
1737
|
+
},
|
|
1738
|
+
geoPerformance: [],
|
|
1739
|
+
inventoryRisk
|
|
1740
|
+
});
|
|
1741
|
+
} catch {
|
|
1742
|
+
return json({ error: "Failed to fetch ecommerce analytics" }, { status: 500 });
|
|
1743
|
+
}
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1263
1746
|
function createAnalyticsHandlers(config) {
|
|
1264
1747
|
const { json, getAnalyticsData, getPropertyId, getPermissions } = config;
|
|
1265
1748
|
return {
|
|
@@ -1291,8 +1774,27 @@ function createAnalyticsHandlers(config) {
|
|
|
1291
1774
|
};
|
|
1292
1775
|
}
|
|
1293
1776
|
function createUploadHandler(config) {
|
|
1294
|
-
const {
|
|
1295
|
-
|
|
1777
|
+
const {
|
|
1778
|
+
json,
|
|
1779
|
+
requireAuth,
|
|
1780
|
+
requireEntityPermission,
|
|
1781
|
+
storage,
|
|
1782
|
+
localUploadDir = "public/uploads",
|
|
1783
|
+
allowedTypes,
|
|
1784
|
+
maxSizeBytes = 10 * 1024 * 1024,
|
|
1785
|
+
dataSource,
|
|
1786
|
+
entityMap
|
|
1787
|
+
} = config;
|
|
1788
|
+
const allowed = allowedTypes ?? [
|
|
1789
|
+
"image/jpeg",
|
|
1790
|
+
"image/png",
|
|
1791
|
+
"image/gif",
|
|
1792
|
+
"image/webp",
|
|
1793
|
+
"application/pdf",
|
|
1794
|
+
"text/plain",
|
|
1795
|
+
"application/zip",
|
|
1796
|
+
"application/x-zip-compressed"
|
|
1797
|
+
];
|
|
1296
1798
|
return async function POST(req) {
|
|
1297
1799
|
const authErr = await requireAuth(req);
|
|
1298
1800
|
if (authErr) return authErr;
|
|
@@ -1305,28 +1807,92 @@ function createUploadHandler(config) {
|
|
|
1305
1807
|
const file = formData.get("file");
|
|
1306
1808
|
if (!file) return json({ error: "No file uploaded" }, { status: 400 });
|
|
1307
1809
|
if (!allowed.includes(file.type)) return json({ error: "File type not allowed" }, { status: 400 });
|
|
1308
|
-
|
|
1810
|
+
const defaultMax = 10 * 1024 * 1024;
|
|
1811
|
+
const maxZipBytes = 80 * 1024 * 1024;
|
|
1812
|
+
const baseMax = maxSizeBytes ?? defaultMax;
|
|
1813
|
+
const effectiveMax = file.type === "application/zip" || file.type === "application/x-zip-compressed" ? Math.max(baseMax, maxZipBytes) : baseMax;
|
|
1814
|
+
if (file.size > effectiveMax) return json({ error: "File size exceeds limit" }, { status: 400 });
|
|
1815
|
+
const parentRaw = formData.get("parentId");
|
|
1816
|
+
let parentId = null;
|
|
1817
|
+
if (parentRaw != null && String(parentRaw).trim() !== "") {
|
|
1818
|
+
const n = Number(parentRaw);
|
|
1819
|
+
if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
|
|
1820
|
+
parentId = n;
|
|
1821
|
+
}
|
|
1822
|
+
let folder = "";
|
|
1823
|
+
if (parentId != null) {
|
|
1824
|
+
if (!dataSource || !entityMap?.media) {
|
|
1825
|
+
return json({ error: "Upload handler needs dataSource and entityMap for folder uploads" }, { status: 400 });
|
|
1826
|
+
}
|
|
1827
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1828
|
+
const p = await repo.findOne({ where: { id: parentId } });
|
|
1829
|
+
if (!p || p.kind !== "folder") {
|
|
1830
|
+
return json({ error: "parent must be a folder" }, { status: 400 });
|
|
1831
|
+
}
|
|
1832
|
+
folder = await relativePathFromMediaParentId(dataSource, entityMap, parentId);
|
|
1833
|
+
} else {
|
|
1834
|
+
const folderRawLegacy = formData.get("folder") ?? formData.get("folderPath");
|
|
1835
|
+
if (folderRawLegacy && typeof folderRawLegacy === "string" && folderRawLegacy.trim()) {
|
|
1836
|
+
folder = sanitizeMediaFolderPath(folderRawLegacy);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1309
1839
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
1310
1840
|
const fileName = `${Date.now()}-${file.name}`;
|
|
1311
1841
|
const contentType = file.type || "application/octet-stream";
|
|
1842
|
+
const relativeUnderUploads = folder ? `${folder}/${fileName}` : fileName;
|
|
1312
1843
|
const raw = typeof storage === "function" ? storage() : storage;
|
|
1313
1844
|
const storageService = raw instanceof Promise ? await raw : raw;
|
|
1314
1845
|
if (storageService) {
|
|
1315
|
-
const fileUrl = await storageService.upload(buffer, `uploads/${
|
|
1316
|
-
return json({ filePath: fileUrl });
|
|
1846
|
+
const fileUrl = await storageService.upload(buffer, `uploads/${relativeUnderUploads}`, contentType);
|
|
1847
|
+
return json({ filePath: fileUrl, parentId });
|
|
1317
1848
|
}
|
|
1318
1849
|
const fs = await import("fs/promises");
|
|
1319
1850
|
const path = await import("path");
|
|
1320
1851
|
const dir = path.join(process.cwd(), localUploadDir);
|
|
1321
|
-
|
|
1322
|
-
|
|
1852
|
+
const filePath = path.join(dir, relativeUnderUploads);
|
|
1853
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
1323
1854
|
await fs.writeFile(filePath, buffer);
|
|
1324
|
-
|
|
1855
|
+
const urlRel = `${localUploadDir.replace(/^\/+/, "").replace(/\\/g, "/")}/${relativeUnderUploads.replace(/\\/g, "/")}`;
|
|
1856
|
+
return json({ filePath: `/${urlRel}`, parentId });
|
|
1325
1857
|
} catch (err) {
|
|
1326
1858
|
return json({ error: "File upload failed" }, { status: 500 });
|
|
1327
1859
|
}
|
|
1328
1860
|
};
|
|
1329
1861
|
}
|
|
1862
|
+
function createMediaZipExtractHandler(config) {
|
|
1863
|
+
const { json, requireAuth, requireEntityPermission, storage, localUploadDir = "public/uploads", dataSource, entityMap } = config;
|
|
1864
|
+
return async function POST(_req, zipMediaId) {
|
|
1865
|
+
const authErr = await requireAuth(_req);
|
|
1866
|
+
if (authErr) return authErr;
|
|
1867
|
+
if (requireEntityPermission) {
|
|
1868
|
+
const pe = await requireEntityPermission(_req, "media", "create");
|
|
1869
|
+
if (pe) return pe;
|
|
1870
|
+
}
|
|
1871
|
+
if (!dataSource || !entityMap?.media) {
|
|
1872
|
+
return json({ error: "Media extract requires dataSource and entityMap" }, { status: 500 });
|
|
1873
|
+
}
|
|
1874
|
+
const id = Number(zipMediaId);
|
|
1875
|
+
if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
|
|
1876
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1877
|
+
const row = await repo.findOne({ where: { id } });
|
|
1878
|
+
if (!row) return json({ error: "Not found" }, { status: 404 });
|
|
1879
|
+
try {
|
|
1880
|
+
const raw = typeof storage === "function" ? storage() : storage;
|
|
1881
|
+
const storageService = raw instanceof Promise ? await raw : raw;
|
|
1882
|
+
const result = await extractZipMediaIntoParentTree({
|
|
1883
|
+
dataSource,
|
|
1884
|
+
entityMap,
|
|
1885
|
+
zipMediaRow: row,
|
|
1886
|
+
storage: storageService,
|
|
1887
|
+
localUploadDir
|
|
1888
|
+
});
|
|
1889
|
+
return json({ ok: true, ...result });
|
|
1890
|
+
} catch (e) {
|
|
1891
|
+
const msg = e instanceof Error ? e.message : "Extract failed";
|
|
1892
|
+
return json({ error: msg }, { status: 400 });
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1330
1896
|
function createBlogBySlugHandler(config) {
|
|
1331
1897
|
const { dataSource, entityMap, json } = config;
|
|
1332
1898
|
return async function GET(_req, slug) {
|
|
@@ -1703,7 +2269,7 @@ function createFormSubmissionHandler(config) {
|
|
|
1703
2269
|
};
|
|
1704
2270
|
}
|
|
1705
2271
|
function createUsersApiHandlers(config) {
|
|
1706
|
-
const { dataSource, entityMap, json, requireAuth, requireEntityPermission, baseUrl, getCms, getCompanyDetails } = config;
|
|
2272
|
+
const { dataSource, entityMap, json, requireAuth, requireEntityPermission, baseUrl, getCms, getCompanyDetails, getSessionUser } = config;
|
|
1707
2273
|
async function trySendInviteEmail(toEmail, inviteLink, inviteeName) {
|
|
1708
2274
|
if (!getCms) return;
|
|
1709
2275
|
try {
|
|
@@ -1739,7 +2305,10 @@ function createUsersApiHandlers(config) {
|
|
|
1739
2305
|
const sortField = url.searchParams.get("sortField") || "createdAt";
|
|
1740
2306
|
const sortOrder = url.searchParams.get("sortOrder") === "desc" ? "DESC" : "ASC";
|
|
1741
2307
|
const search = url.searchParams.get("search");
|
|
1742
|
-
const where = search ? [
|
|
2308
|
+
const where = search ? [
|
|
2309
|
+
{ name: ILike2(`%${search}%`), deleted: false },
|
|
2310
|
+
{ email: ILike2(`%${search}%`), deleted: false }
|
|
2311
|
+
] : { deleted: false };
|
|
1743
2312
|
const [data, total] = await userRepo().findAndCount({
|
|
1744
2313
|
skip,
|
|
1745
2314
|
take: limit,
|
|
@@ -1804,7 +2373,7 @@ function createUsersApiHandlers(config) {
|
|
|
1804
2373
|
}
|
|
1805
2374
|
try {
|
|
1806
2375
|
const user = await userRepo().findOne({
|
|
1807
|
-
where: { id: parseInt(id, 10) },
|
|
2376
|
+
where: { id: parseInt(id, 10), deleted: false },
|
|
1808
2377
|
relations: ["group"],
|
|
1809
2378
|
select: ["id", "name", "email", "blocked", "createdAt", "updatedAt", "groupId"]
|
|
1810
2379
|
});
|
|
@@ -1822,11 +2391,14 @@ function createUsersApiHandlers(config) {
|
|
|
1822
2391
|
if (pe) return pe;
|
|
1823
2392
|
}
|
|
1824
2393
|
try {
|
|
2394
|
+
const uid = parseInt(id, 10);
|
|
2395
|
+
const existing = await userRepo().findOne({ where: { id: uid, deleted: false } });
|
|
2396
|
+
if (!existing) return json({ error: "Not found" }, { status: 404 });
|
|
1825
2397
|
const body = await req.json();
|
|
1826
2398
|
const { password: _p, ...safe } = body;
|
|
1827
|
-
await userRepo().update(
|
|
2399
|
+
await userRepo().update(uid, safe);
|
|
1828
2400
|
const updated = await userRepo().findOne({
|
|
1829
|
-
where: { id:
|
|
2401
|
+
where: { id: uid, deleted: false },
|
|
1830
2402
|
relations: ["group"],
|
|
1831
2403
|
select: ["id", "name", "email", "blocked", "createdAt", "updatedAt", "groupId"]
|
|
1832
2404
|
});
|
|
@@ -1843,8 +2415,23 @@ function createUsersApiHandlers(config) {
|
|
|
1843
2415
|
if (pe) return pe;
|
|
1844
2416
|
}
|
|
1845
2417
|
try {
|
|
1846
|
-
const
|
|
1847
|
-
|
|
2418
|
+
const uid = parseInt(id, 10);
|
|
2419
|
+
const existing = await userRepo().findOne({ where: { id: uid, deleted: false } });
|
|
2420
|
+
if (!existing) return json({ error: "User not found" }, { status: 404 });
|
|
2421
|
+
let deletedBy = null;
|
|
2422
|
+
if (getSessionUser) {
|
|
2423
|
+
try {
|
|
2424
|
+
const u = await getSessionUser();
|
|
2425
|
+
if (u?.id) {
|
|
2426
|
+
const n = Number(u.id);
|
|
2427
|
+
if (Number.isFinite(n)) deletedBy = n;
|
|
2428
|
+
}
|
|
2429
|
+
} catch {
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
const payload = { deleted: true, deletedAt: /* @__PURE__ */ new Date() };
|
|
2433
|
+
if (deletedBy != null) payload.deletedBy = deletedBy;
|
|
2434
|
+
await userRepo().update(uid, payload);
|
|
1848
2435
|
return json({ message: "User deleted successfully" });
|
|
1849
2436
|
} catch {
|
|
1850
2437
|
return json({ error: "Server Error" }, { status: 500 });
|
|
@@ -1858,7 +2445,10 @@ function createUsersApiHandlers(config) {
|
|
|
1858
2445
|
if (pe) return pe;
|
|
1859
2446
|
}
|
|
1860
2447
|
try {
|
|
1861
|
-
const user = await userRepo().findOne({
|
|
2448
|
+
const user = await userRepo().findOne({
|
|
2449
|
+
where: { id: parseInt(id, 10), deleted: false },
|
|
2450
|
+
select: ["email", "name"]
|
|
2451
|
+
});
|
|
1862
2452
|
if (!user) return json({ error: "User not found" }, { status: 404 });
|
|
1863
2453
|
const emailToken = Buffer.from(user.email).toString("base64");
|
|
1864
2454
|
const inviteLink = `${baseUrl}/admin/invite?token=${emailToken}`;
|
|
@@ -2468,6 +3058,7 @@ function createCmsApiHandler(config) {
|
|
|
2468
3058
|
getCms,
|
|
2469
3059
|
userAuth: userAuthConfig,
|
|
2470
3060
|
dashboard,
|
|
3061
|
+
ecommerceAnalytics,
|
|
2471
3062
|
analytics: analyticsConfig,
|
|
2472
3063
|
upload,
|
|
2473
3064
|
blogBySlug,
|
|
@@ -2480,9 +3071,10 @@ function createCmsApiHandler(config) {
|
|
|
2480
3071
|
userProfile,
|
|
2481
3072
|
settings: settingsConfig,
|
|
2482
3073
|
chat: chatConfig,
|
|
2483
|
-
requireEntityPermission:
|
|
3074
|
+
requireEntityPermission: userRequireEntityPermission,
|
|
2484
3075
|
getSessionUser
|
|
2485
3076
|
} = config;
|
|
3077
|
+
const requireEntityPermissionEffective = userRequireEntityPermission ?? (async (_req, entity, action) => config.json({ error: "Forbidden", reason: "entity_rbac_required", entity, action }, { status: 403 }));
|
|
2486
3078
|
const analytics = analyticsConfig ?? (getCms ? {
|
|
2487
3079
|
json: config.json,
|
|
2488
3080
|
requireAuth: async () => null,
|
|
@@ -2520,12 +3112,20 @@ function createCmsApiHandler(config) {
|
|
|
2520
3112
|
const crudOpts = {
|
|
2521
3113
|
requireAuth: config.requireAuth,
|
|
2522
3114
|
json: config.json,
|
|
2523
|
-
requireEntityPermission:
|
|
2524
|
-
getCms
|
|
3115
|
+
requireEntityPermission: requireEntityPermissionEffective,
|
|
3116
|
+
getCms,
|
|
3117
|
+
...getSessionUser ? {
|
|
3118
|
+
getDeletedByUserId: async () => {
|
|
3119
|
+
const u = await getSessionUser();
|
|
3120
|
+
if (!u?.id) return null;
|
|
3121
|
+
const n = Number(u.id);
|
|
3122
|
+
return Number.isFinite(n) ? n : null;
|
|
3123
|
+
}
|
|
3124
|
+
} : {}
|
|
2525
3125
|
};
|
|
2526
3126
|
const crud = createCrudHandler(dataSource, entityMap, crudOpts);
|
|
2527
3127
|
const crudById = createCrudByIdHandler(dataSource, entityMap, crudOpts);
|
|
2528
|
-
const mergePerm = (c) => !c ? void 0 :
|
|
3128
|
+
const mergePerm = (c) => !c ? void 0 : { ...c, requireEntityPermission: requireEntityPermissionEffective };
|
|
2529
3129
|
const adminRoles = getSessionUser && createAdminRolesHandlers({
|
|
2530
3130
|
dataSource,
|
|
2531
3131
|
entityMap,
|
|
@@ -2534,8 +3134,23 @@ function createCmsApiHandler(config) {
|
|
|
2534
3134
|
});
|
|
2535
3135
|
const userAuthRouter = userAuth ? createUserAuthApiRouter(userAuth) : null;
|
|
2536
3136
|
const dashboardGet = dashboard ? createDashboardStatsHandler(mergePerm(dashboard) ?? dashboard) : null;
|
|
3137
|
+
const ecommerceAnalyticsResolved = mergePerm(
|
|
3138
|
+
ecommerceAnalytics ?? {
|
|
3139
|
+
dataSource,
|
|
3140
|
+
entityMap,
|
|
3141
|
+
json: config.json,
|
|
3142
|
+
requireAuth: config.requireAuth
|
|
3143
|
+
}
|
|
3144
|
+
);
|
|
3145
|
+
const ecommerceAnalyticsGet = createEcommerceAnalyticsHandler(ecommerceAnalyticsResolved);
|
|
2537
3146
|
const analyticsHandlers = analytics ? createAnalyticsHandlers(analytics) : null;
|
|
2538
|
-
const
|
|
3147
|
+
const uploadMerged = upload ? {
|
|
3148
|
+
...mergePerm(upload) ?? upload,
|
|
3149
|
+
dataSource: upload.dataSource ?? dataSource,
|
|
3150
|
+
entityMap: upload.entityMap ?? entityMap
|
|
3151
|
+
} : null;
|
|
3152
|
+
const uploadPost = uploadMerged ? createUploadHandler(uploadMerged) : null;
|
|
3153
|
+
const zipExtractPost = uploadMerged ? createMediaZipExtractHandler(uploadMerged) : null;
|
|
2539
3154
|
const blogBySlugGet = blogBySlug ? createBlogBySlugHandler(blogBySlug) : null;
|
|
2540
3155
|
const formBySlugGet = formBySlug ? createFormBySlugHandler(formBySlug) : null;
|
|
2541
3156
|
const formSaveHandlers = formSaveConfig ? createFormSaveHandlers(mergePerm(formSaveConfig) ?? formSaveConfig) : null;
|
|
@@ -2545,7 +3160,11 @@ function createCmsApiHandler(config) {
|
|
|
2545
3160
|
const usersApiMerged = usersApi && getCms ? {
|
|
2546
3161
|
...usersApi,
|
|
2547
3162
|
getCms: usersApi.getCms ?? getCms,
|
|
2548
|
-
getCompanyDetails: usersApi.getCompanyDetails ?? config.getCompanyDetails
|
|
3163
|
+
getCompanyDetails: usersApi.getCompanyDetails ?? config.getCompanyDetails,
|
|
3164
|
+
...getSessionUser ? { getSessionUser: usersApi.getSessionUser ?? getSessionUser } : {}
|
|
3165
|
+
} : usersApi ? {
|
|
3166
|
+
...usersApi,
|
|
3167
|
+
...getSessionUser ? { getSessionUser: usersApi.getSessionUser ?? getSessionUser } : {}
|
|
2549
3168
|
} : usersApi;
|
|
2550
3169
|
const usersHandlers = usersApiMerged ? createUsersApiHandlers(mergePerm(usersApiMerged) ?? usersApiMerged) : null;
|
|
2551
3170
|
const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
|
|
@@ -2556,7 +3175,7 @@ function createCmsApiHandler(config) {
|
|
|
2556
3175
|
entityMap,
|
|
2557
3176
|
json: config.json,
|
|
2558
3177
|
requireAuth: config.requireAuth,
|
|
2559
|
-
requireEntityPermission:
|
|
3178
|
+
requireEntityPermission: requireEntityPermissionEffective
|
|
2560
3179
|
});
|
|
2561
3180
|
const chatHandlers = chatConfig ? createChatHandlers(chatConfig) : null;
|
|
2562
3181
|
function resolveResource(segment) {
|
|
@@ -2565,12 +3184,10 @@ function createCmsApiHandler(config) {
|
|
|
2565
3184
|
}
|
|
2566
3185
|
return {
|
|
2567
3186
|
async handle(method, path, req) {
|
|
2568
|
-
const perm = reqEntityPerm;
|
|
2569
3187
|
async function analyticsGate() {
|
|
2570
3188
|
const a = await config.requireAuth(req);
|
|
2571
3189
|
if (a) return a;
|
|
2572
|
-
|
|
2573
|
-
return null;
|
|
3190
|
+
return requireEntityPermissionEffective(req, "analytics", "read");
|
|
2574
3191
|
}
|
|
2575
3192
|
if (path[0] === "admin" && path[1] === "roles") {
|
|
2576
3193
|
if (!adminRoles) return config.json({ error: "Not found" }, { status: 404 });
|
|
@@ -2584,6 +3201,11 @@ function createCmsApiHandler(config) {
|
|
|
2584
3201
|
if (path[0] === "dashboard" && path[1] === "stats" && path.length === 2 && method === "GET" && dashboardGet) {
|
|
2585
3202
|
return dashboardGet(req);
|
|
2586
3203
|
}
|
|
3204
|
+
if (path[0] === "dashboard" && path[1] === "ecommerce" && path.length === 2 && method === "GET" && ecommerceAnalyticsGet) {
|
|
3205
|
+
const g = await analyticsGate();
|
|
3206
|
+
if (g) return g;
|
|
3207
|
+
return ecommerceAnalyticsGet(req);
|
|
3208
|
+
}
|
|
2587
3209
|
if (path[0] === "analytics" && analyticsHandlers) {
|
|
2588
3210
|
if (path.length === 1 && method === "GET") {
|
|
2589
3211
|
const g = await analyticsGate();
|
|
@@ -2602,6 +3224,9 @@ function createCmsApiHandler(config) {
|
|
|
2602
3224
|
}
|
|
2603
3225
|
}
|
|
2604
3226
|
if (path[0] === "upload" && path.length === 1 && method === "POST" && uploadPost) return uploadPost(req);
|
|
3227
|
+
if (path[0] === "media" && path[1] === "extract" && path.length === 3 && method === "POST" && zipExtractPost) {
|
|
3228
|
+
return zipExtractPost(req, path[2]);
|
|
3229
|
+
}
|
|
2605
3230
|
if (path[0] === "blogs" && path[1] === "slug" && path.length === 3 && method === "GET" && blogBySlugGet) {
|
|
2606
3231
|
return blogBySlugGet(req, path[2]);
|
|
2607
3232
|
}
|
|
@@ -2646,19 +3271,17 @@ function createCmsApiHandler(config) {
|
|
|
2646
3271
|
const group = path[1];
|
|
2647
3272
|
const isPublic = settingsConfig?.publicGetGroups?.includes(group);
|
|
2648
3273
|
if (method === "GET") {
|
|
2649
|
-
if (!isPublic
|
|
3274
|
+
if (!isPublic) {
|
|
2650
3275
|
const a = await config.requireAuth(req);
|
|
2651
3276
|
if (a) return a;
|
|
2652
|
-
const pe = await
|
|
3277
|
+
const pe = await requireEntityPermissionEffective(req, "settings", "read");
|
|
2653
3278
|
if (pe) return pe;
|
|
2654
3279
|
}
|
|
2655
3280
|
return settingsHandlers.GET(req, group);
|
|
2656
3281
|
}
|
|
2657
3282
|
if (method === "PUT") {
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
if (pe) return pe;
|
|
2661
|
-
}
|
|
3283
|
+
const pe = await requireEntityPermissionEffective(req, "settings", "update");
|
|
3284
|
+
if (pe) return pe;
|
|
2662
3285
|
return settingsHandlers.PUT(req, group);
|
|
2663
3286
|
}
|
|
2664
3287
|
}
|
|
@@ -2674,10 +3297,8 @@ function createCmsApiHandler(config) {
|
|
|
2674
3297
|
if (path[0] === "orders" && path.length === 3 && path[2] === "invoice" && method === "GET" && getCms) {
|
|
2675
3298
|
const a = await config.requireAuth(req);
|
|
2676
3299
|
if (a) return a;
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
if (pe) return pe;
|
|
2680
|
-
}
|
|
3300
|
+
const pe = await requireEntityPermissionEffective(req, "orders", "read");
|
|
3301
|
+
if (pe) return pe;
|
|
2681
3302
|
const cms = await getCms();
|
|
2682
3303
|
const { streamOrderInvoicePdf: streamOrderInvoicePdf2 } = await Promise.resolve().then(() => (init_erp_order_invoice(), erp_order_invoice_exports));
|
|
2683
3304
|
const oid = Number(path[1]);
|
|
@@ -2687,10 +3308,8 @@ function createCmsApiHandler(config) {
|
|
|
2687
3308
|
if (path[0] === "orders" && path.length === 3 && path[2] === "repost-erp" && getCms) {
|
|
2688
3309
|
const a = await config.requireAuth(req);
|
|
2689
3310
|
if (a) return a;
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
if (pe) return pe;
|
|
2693
|
-
}
|
|
3311
|
+
const pe = await requireEntityPermissionEffective(req, "orders", method === "GET" ? "read" : "update");
|
|
3312
|
+
if (pe) return pe;
|
|
2694
3313
|
const oid = Number(path[1]);
|
|
2695
3314
|
if (!Number.isFinite(oid)) return config.json({ error: "Invalid id" }, { status: 400 });
|
|
2696
3315
|
const cms = await getCms();
|
|
@@ -2739,7 +3358,7 @@ function createCmsApiHandler(config) {
|
|
|
2739
3358
|
}
|
|
2740
3359
|
|
|
2741
3360
|
// src/api/storefront-handlers.ts
|
|
2742
|
-
import { In, IsNull as
|
|
3361
|
+
import { In as In2, IsNull as IsNull4 } from "typeorm";
|
|
2743
3362
|
|
|
2744
3363
|
// src/lib/is-valid-signup-email.ts
|
|
2745
3364
|
var MAX_EMAIL = 254;
|
|
@@ -2999,7 +3618,7 @@ async function queueSms(cms, payload) {
|
|
|
2999
3618
|
|
|
3000
3619
|
// src/lib/otp-challenge.ts
|
|
3001
3620
|
import { createHmac, randomInt, timingSafeEqual } from "crypto";
|
|
3002
|
-
import { IsNull as
|
|
3621
|
+
import { IsNull as IsNull3, MoreThan as MoreThan2 } from "typeorm";
|
|
3003
3622
|
var OTP_TTL_MS = 10 * 60 * 1e3;
|
|
3004
3623
|
var MAX_SENDS_PER_HOUR = 5;
|
|
3005
3624
|
var MAX_VERIFY_ATTEMPTS = 8;
|
|
@@ -3047,7 +3666,7 @@ async function createOtpChallenge(dataSource, entityMap, input) {
|
|
|
3047
3666
|
await repo.delete({
|
|
3048
3667
|
purpose,
|
|
3049
3668
|
identifier,
|
|
3050
|
-
consumedAt:
|
|
3669
|
+
consumedAt: IsNull3()
|
|
3051
3670
|
});
|
|
3052
3671
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
3053
3672
|
const codeHash = hashOtpCode(code, purpose, identifier, pepper);
|
|
@@ -3068,7 +3687,7 @@ async function verifyAndConsumeOtpChallenge(dataSource, entityMap, input) {
|
|
|
3068
3687
|
const { purpose, identifier, code, pepper } = input;
|
|
3069
3688
|
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
3070
3689
|
const row = await repo.findOne({
|
|
3071
|
-
where: { purpose, identifier, consumedAt:
|
|
3690
|
+
where: { purpose, identifier, consumedAt: IsNull3() },
|
|
3072
3691
|
order: { id: "DESC" }
|
|
3073
3692
|
});
|
|
3074
3693
|
if (!row) {
|
|
@@ -3302,7 +3921,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3302
3921
|
const u = await userRepo().findOne({ where: { id: userId } });
|
|
3303
3922
|
if (!u) return null;
|
|
3304
3923
|
const unclaimed = await contactRepo().findOne({
|
|
3305
|
-
where: { email: u.email, userId:
|
|
3924
|
+
where: { email: u.email, userId: IsNull4(), deleted: false }
|
|
3306
3925
|
});
|
|
3307
3926
|
if (unclaimed) {
|
|
3308
3927
|
await contactRepo().update(unclaimed.id, { userId });
|
|
@@ -4343,7 +4962,7 @@ function createStorefrontApiHandler(config) {
|
|
|
4343
4962
|
const previewByOrder = {};
|
|
4344
4963
|
if (orderIds.length) {
|
|
4345
4964
|
const oItems = await orderItemRepo().find({
|
|
4346
|
-
where: { orderId:
|
|
4965
|
+
where: { orderId: In2(orderIds) },
|
|
4347
4966
|
relations: ["product"],
|
|
4348
4967
|
order: { id: "ASC" }
|
|
4349
4968
|
});
|
|
@@ -4475,9 +5094,11 @@ export {
|
|
|
4475
5094
|
createCrudByIdHandler,
|
|
4476
5095
|
createCrudHandler,
|
|
4477
5096
|
createDashboardStatsHandler,
|
|
5097
|
+
createEcommerceAnalyticsHandler,
|
|
4478
5098
|
createForgotPasswordHandler,
|
|
4479
5099
|
createFormBySlugHandler,
|
|
4480
5100
|
createInviteAcceptHandler,
|
|
5101
|
+
createMediaZipExtractHandler,
|
|
4481
5102
|
createSetPasswordHandler,
|
|
4482
5103
|
createSettingsApiHandlers,
|
|
4483
5104
|
createStorefrontApiHandler,
|