@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.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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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({
|
|
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
|
|
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
|
|
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,
|
|
1297
|
-
repo("form_submissions")?.count({ where: { createdAt: (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 {
|
|
1342
|
-
|
|
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
|
-
|
|
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/${
|
|
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
|
-
|
|
1369
|
-
|
|
1901
|
+
const filePath = path.join(dir, relativeUnderUploads);
|
|
1902
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
1370
1903
|
await fs.writeFile(filePath, buffer);
|
|
1371
|
-
|
|
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 ? [
|
|
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(
|
|
2448
|
+
await userRepo().update(uid, safe);
|
|
1875
2449
|
const updated = await userRepo().findOne({
|
|
1876
|
-
where: { id:
|
|
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
|
|
1894
|
-
|
|
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({
|
|
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,
|
|
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:
|
|
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:
|
|
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 :
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
3323
|
+
if (!isPublic) {
|
|
2697
3324
|
const a = await config.requireAuth(req);
|
|
2698
3325
|
if (a) return a;
|
|
2699
|
-
const pe = await
|
|
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
|
-
|
|
2706
|
-
|
|
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
|
-
|
|
2725
|
-
|
|
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
|
-
|
|
2738
|
-
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|