@infuro/cms-core 1.0.23 → 1.0.25
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/dist/admin.cjs +5161 -3307
- package/dist/admin.cjs.map +1 -1
- package/dist/admin.d.cts +52 -11
- package/dist/admin.d.ts +52 -11
- package/dist/admin.js +5095 -3238
- package/dist/admin.js.map +1 -1
- package/dist/api.cjs +595 -60
- 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 +603 -68
- package/dist/api.js.map +1 -1
- package/dist/auth.cjs +9 -1
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.js +9 -1
- package/dist/auth.js.map +1 -1
- package/dist/cli.cjs +7 -0
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -1
- package/dist/{index-BGAh4fPQ.d.cts → index--vbixpxE.d.cts} +17 -2
- package/dist/{index-Cnwh7B3r.d.ts → index-DMJgi-fy.d.ts} +17 -2
- package/dist/index.cjs +683 -75
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +684 -76
- package/dist/index.js.map +1 -1
- package/package.json +130 -129
package/dist/api.js
CHANGED
|
@@ -17,16 +17,51 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
17
17
|
return result;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
// src/plugins/erp/erp-log.ts
|
|
21
|
+
function logErp(event, detail) {
|
|
22
|
+
if (detail && Object.keys(detail).length) console.info(ERP_LOG, event, detail);
|
|
23
|
+
else console.info(ERP_LOG, event);
|
|
24
|
+
}
|
|
25
|
+
function warnErp(event, detail) {
|
|
26
|
+
console.warn(ERP_LOG, event, detail);
|
|
27
|
+
}
|
|
28
|
+
function errorErp(event, detail) {
|
|
29
|
+
console.error(ERP_LOG, event, detail);
|
|
30
|
+
}
|
|
31
|
+
var ERP_LOG;
|
|
32
|
+
var init_erp_log = __esm({
|
|
33
|
+
"src/plugins/erp/erp-log.ts"() {
|
|
34
|
+
"use strict";
|
|
35
|
+
ERP_LOG = "[webcore:erp]";
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
20
39
|
// src/plugins/erp/erp-queue.ts
|
|
40
|
+
function queuePayloadSummary(payload) {
|
|
41
|
+
if (payload.kind === "order") {
|
|
42
|
+
const o = payload.order;
|
|
43
|
+
return {
|
|
44
|
+
kind: payload.kind,
|
|
45
|
+
platformOrderId: o.platformOrderId ?? o.platformOrderNumber,
|
|
46
|
+
itemCount: Array.isArray(o.items) ? o.items.length : 0
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return { kind: payload.kind };
|
|
50
|
+
}
|
|
21
51
|
async function queueErp(cms, payload) {
|
|
22
52
|
const queue = cms.getPlugin("queue");
|
|
23
|
-
if (!queue)
|
|
53
|
+
if (!queue) {
|
|
54
|
+
warnErp("queue:add_skipped", { reason: "queue_plugin_missing", ...queuePayloadSummary(payload) });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
logErp("queue:add", { job: ERP_QUEUE_NAME, ...queuePayloadSummary(payload) });
|
|
24
58
|
await queue.add(ERP_QUEUE_NAME, payload);
|
|
25
59
|
}
|
|
26
60
|
var ERP_QUEUE_NAME;
|
|
27
61
|
var init_erp_queue = __esm({
|
|
28
62
|
"src/plugins/erp/erp-queue.ts"() {
|
|
29
63
|
"use strict";
|
|
64
|
+
init_erp_log();
|
|
30
65
|
ERP_QUEUE_NAME = "erp";
|
|
31
66
|
}
|
|
32
67
|
});
|
|
@@ -339,21 +374,36 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
|
|
|
339
374
|
const cfgRows = await configRepo.find({ where: { settings: "erp", deleted: false } });
|
|
340
375
|
for (const row of cfgRows) {
|
|
341
376
|
const r = row;
|
|
342
|
-
if (r.key === "enabled" && r.value === "false")
|
|
377
|
+
if (r.key === "enabled" && r.value === "false") {
|
|
378
|
+
logErp("paid-order:skip", { orderId, reason: "erp_config_disabled" });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (!cms.getPlugin("erp")) {
|
|
383
|
+
logErp("paid-order:skip", { orderId, reason: "erp_plugin_missing" });
|
|
384
|
+
return;
|
|
343
385
|
}
|
|
344
|
-
if (!cms.getPlugin("erp")) return;
|
|
345
386
|
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
346
387
|
const ord = await orderRepo.findOne({
|
|
347
388
|
where: { id: orderId },
|
|
348
389
|
relations: ["items", "items.product", "contact", "billingAddress", "shippingAddress", "payments"]
|
|
349
390
|
});
|
|
350
|
-
if (!ord)
|
|
391
|
+
if (!ord) {
|
|
392
|
+
logErp("paid-order:skip", { orderId, reason: "order_not_found" });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
351
395
|
const o = ord;
|
|
352
396
|
const okKind = o.orderKind === void 0 || o.orderKind === null || o.orderKind === "sale";
|
|
353
|
-
if (!okKind)
|
|
397
|
+
if (!okKind) {
|
|
398
|
+
logErp("paid-order:skip", { orderId, reason: "order_kind_not_sale", orderKind: o.orderKind });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
354
401
|
const rawPayments = o.payments ?? [];
|
|
355
402
|
const completedPayments = rawPayments.filter((pay) => pay.status === "completed" && pay.deleted !== true);
|
|
356
|
-
if (!completedPayments.length)
|
|
403
|
+
if (!completedPayments.length) {
|
|
404
|
+
logErp("paid-order:skip", { orderId, reason: "no_completed_payments" });
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
357
407
|
const rawItems = o.items ?? [];
|
|
358
408
|
const lines = rawItems.filter((it) => it.product).map((it) => {
|
|
359
409
|
const p = it.product;
|
|
@@ -372,7 +422,10 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
|
|
|
372
422
|
type: itemType
|
|
373
423
|
};
|
|
374
424
|
});
|
|
375
|
-
if (!lines.length)
|
|
425
|
+
if (!lines.length) {
|
|
426
|
+
logErp("paid-order:skip", { orderId, reason: "no_line_items_with_product" });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
376
429
|
const contact = o.contact;
|
|
377
430
|
const orderTotalMajor = Number(o.total);
|
|
378
431
|
const paymentDtos = completedPayments.length === 1 && Number.isFinite(orderTotalMajor) ? [paymentRowToWebhookDto(completedPayments[0], orderTotalMajor)] : completedPayments.map((pay) => paymentRowToWebhookDto(pay));
|
|
@@ -394,19 +447,34 @@ async function queueErpPaidOrderForOrderId(cms, dataSource, entityMap, orderId)
|
|
|
394
447
|
payments: paymentDtos,
|
|
395
448
|
metadata: { ...baseMeta, source: "storefront" }
|
|
396
449
|
};
|
|
450
|
+
logErp("paid-order:payload_built", {
|
|
451
|
+
orderId,
|
|
452
|
+
platformOrderId: orderDto.platformOrderId,
|
|
453
|
+
status: orderDto.status,
|
|
454
|
+
itemCount: lines.length,
|
|
455
|
+
skus: lines.map((l) => l.sku),
|
|
456
|
+
paymentCount: paymentDtos.length,
|
|
457
|
+
paymentIds: paymentDtos.map((p) => p.id),
|
|
458
|
+
total: orderTotalMajor
|
|
459
|
+
});
|
|
397
460
|
await queueErp(cms, { kind: "order", order: orderDto });
|
|
398
|
-
} catch {
|
|
461
|
+
} catch (e) {
|
|
462
|
+
errorErp("paid-order:enqueue_failed", {
|
|
463
|
+
orderId,
|
|
464
|
+
message: e instanceof Error ? e.message : String(e)
|
|
465
|
+
});
|
|
399
466
|
}
|
|
400
467
|
}
|
|
401
468
|
var init_paid_order_erp = __esm({
|
|
402
469
|
"src/plugins/erp/paid-order-erp.ts"() {
|
|
403
470
|
"use strict";
|
|
471
|
+
init_erp_log();
|
|
404
472
|
init_erp_queue();
|
|
405
473
|
}
|
|
406
474
|
});
|
|
407
475
|
|
|
408
476
|
// src/api/crud.ts
|
|
409
|
-
import { ILike, MoreThan, Not } from "typeorm";
|
|
477
|
+
import { Between, ILike, LessThanOrEqual, MoreThan, MoreThanOrEqual, Not } from "typeorm";
|
|
410
478
|
|
|
411
479
|
// src/plugins/erp/erp-contact-sync.ts
|
|
412
480
|
init_erp_queue();
|
|
@@ -476,6 +544,64 @@ async function queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, produc
|
|
|
476
544
|
}
|
|
477
545
|
}
|
|
478
546
|
|
|
547
|
+
// src/lib/address-geo-validation.ts
|
|
548
|
+
import { Country, State, City } from "country-state-city";
|
|
549
|
+
function norm(s) {
|
|
550
|
+
return typeof s === "string" ? s.trim() : "";
|
|
551
|
+
}
|
|
552
|
+
function resolveCountry(input) {
|
|
553
|
+
const t = input.trim();
|
|
554
|
+
if (!t) return void 0;
|
|
555
|
+
if (t.length === 2) {
|
|
556
|
+
const byCode = Country.getCountryByCode(t.toUpperCase());
|
|
557
|
+
if (byCode) return byCode;
|
|
558
|
+
}
|
|
559
|
+
const lower = t.toLowerCase();
|
|
560
|
+
return Country.getAllCountries().find((c) => c.name.toLowerCase() === lower);
|
|
561
|
+
}
|
|
562
|
+
function resolveState(countryIso, input) {
|
|
563
|
+
const t = input.trim();
|
|
564
|
+
if (!t || !countryIso) return void 0;
|
|
565
|
+
const states = State.getStatesOfCountry(countryIso);
|
|
566
|
+
const lower = t.toLowerCase();
|
|
567
|
+
return states.find((s) => s.isoCode.toLowerCase() === t.toLowerCase() || s.name.toLowerCase() === lower);
|
|
568
|
+
}
|
|
569
|
+
function resolveCity(countryIso, stateIso, input) {
|
|
570
|
+
const t = input.trim();
|
|
571
|
+
if (!t || !countryIso || !stateIso) return void 0;
|
|
572
|
+
const lower = t.toLowerCase();
|
|
573
|
+
const cities = City.getCitiesOfState(countryIso, stateIso);
|
|
574
|
+
return cities.find((c) => c.name.toLowerCase() === lower);
|
|
575
|
+
}
|
|
576
|
+
function assertValidAddressHierarchy(country, state, city) {
|
|
577
|
+
const c = resolveCountry(country);
|
|
578
|
+
if (!c) return { ok: false, error: "Invalid or unknown country." };
|
|
579
|
+
const st = resolveState(c.isoCode, state);
|
|
580
|
+
if (!st) return { ok: false, error: "State or province does not match the selected country." };
|
|
581
|
+
const ct = resolveCity(c.isoCode, st.isoCode, city);
|
|
582
|
+
if (!ct) return { ok: false, error: "City does not match the selected state." };
|
|
583
|
+
return { ok: true, country: c.name, state: st.name, city: ct.name };
|
|
584
|
+
}
|
|
585
|
+
function validateAndNormalizeAddressRow(row) {
|
|
586
|
+
const line1 = norm(row.line1);
|
|
587
|
+
const postalCode = norm(row.postalCode);
|
|
588
|
+
const countryIn = norm(row.country);
|
|
589
|
+
const stateIn = norm(row.state);
|
|
590
|
+
const cityIn = norm(row.city);
|
|
591
|
+
if (!line1) return "Street address (line 1) is required.";
|
|
592
|
+
if (!postalCode) return "Postal code is required.";
|
|
593
|
+
if (!countryIn || !stateIn || !cityIn) return "Country, state, and city are required.";
|
|
594
|
+
const geo = assertValidAddressHierarchy(countryIn, stateIn, cityIn);
|
|
595
|
+
if (!geo.ok) return geo.error;
|
|
596
|
+
row.line1 = line1;
|
|
597
|
+
row.line2 = norm(row.line2) || null;
|
|
598
|
+
row.postalCode = postalCode;
|
|
599
|
+
row.country = geo.country;
|
|
600
|
+
row.state = geo.state;
|
|
601
|
+
row.city = geo.city;
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
|
|
479
605
|
// src/api/crud.ts
|
|
480
606
|
var CRUD_LOG = "[cms-crud]";
|
|
481
607
|
function logCrudClientError(op, detail) {
|
|
@@ -573,6 +699,156 @@ function buildSearchWhereClause(repo, search) {
|
|
|
573
699
|
function entityHasSoftDelete(repo) {
|
|
574
700
|
return repo.metadata.columns.some((c) => c.propertyName === "deleted");
|
|
575
701
|
}
|
|
702
|
+
var LIST_QUERY_RESERVED = /* @__PURE__ */ new Set(["page", "limit", "sortField", "sortOrder", "search"]);
|
|
703
|
+
function dayStartUtc(isoDay) {
|
|
704
|
+
return /* @__PURE__ */ new Date(isoDay + "T00:00:00.000Z");
|
|
705
|
+
}
|
|
706
|
+
function dayEndUtc(isoDay) {
|
|
707
|
+
return /* @__PURE__ */ new Date(isoDay + "T23:59:59.999Z");
|
|
708
|
+
}
|
|
709
|
+
function columnTypeLabel(col) {
|
|
710
|
+
const t = col.type;
|
|
711
|
+
if (typeof t === "string") return t.toLowerCase();
|
|
712
|
+
if (typeof t === "function") return t.name?.toLowerCase?.() ?? "";
|
|
713
|
+
if (t && typeof t === "object" && "name" in t && typeof t.name === "string") {
|
|
714
|
+
return String(t.name).toLowerCase();
|
|
715
|
+
}
|
|
716
|
+
return "";
|
|
717
|
+
}
|
|
718
|
+
function isListDateColumn(col) {
|
|
719
|
+
const tl = columnTypeLabel(col);
|
|
720
|
+
return DATE_COLUMN_TYPES.has(tl) || col.type === Date || TIMESTAMP_PROP_NAMES.has(col.propertyName);
|
|
721
|
+
}
|
|
722
|
+
function isListNumericColumn(col) {
|
|
723
|
+
const tl = columnTypeLabel(col);
|
|
724
|
+
if (col.type === Number) return true;
|
|
725
|
+
const patterns = [
|
|
726
|
+
"int",
|
|
727
|
+
"integer",
|
|
728
|
+
"int2",
|
|
729
|
+
"int4",
|
|
730
|
+
"int8",
|
|
731
|
+
"smallint",
|
|
732
|
+
"bigint",
|
|
733
|
+
"float",
|
|
734
|
+
"double",
|
|
735
|
+
"decimal",
|
|
736
|
+
"numeric",
|
|
737
|
+
"real",
|
|
738
|
+
"tinyint",
|
|
739
|
+
"mediumint"
|
|
740
|
+
];
|
|
741
|
+
if (patterns.some((x) => tl.includes(x))) return true;
|
|
742
|
+
if (tl.includes("unsigned") && (tl.includes("int") || tl.includes("bigint") || tl.includes("small"))) return true;
|
|
743
|
+
if (!tl && /Id$/i.test(col.propertyName) && !TIMESTAMP_PROP_NAMES.has(col.propertyName)) return true;
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
function isListBooleanColumn(col) {
|
|
747
|
+
const tl = columnTypeLabel(col);
|
|
748
|
+
return tl === "boolean" || tl === "bool" || col.type === Boolean;
|
|
749
|
+
}
|
|
750
|
+
function isListStringColumn(col) {
|
|
751
|
+
const tl = columnTypeLabel(col);
|
|
752
|
+
if (isListDateColumn(col) || isListNumericColumn(col) || isListBooleanColumn(col)) return false;
|
|
753
|
+
if (["varchar", "character varying", "text", "citext", "uuid", "char", "character", "enum"].some(
|
|
754
|
+
(x) => tl.includes(x)
|
|
755
|
+
)) {
|
|
756
|
+
return true;
|
|
757
|
+
}
|
|
758
|
+
if (col.type === String) return true;
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
function mergeListWhereAnd(where, patch) {
|
|
762
|
+
if (Object.keys(patch).length === 0) return where;
|
|
763
|
+
if (Array.isArray(where)) {
|
|
764
|
+
if (where.length === 0) return [patch];
|
|
765
|
+
return where.map((w) => ({ ...w, ...patch }));
|
|
766
|
+
}
|
|
767
|
+
if (where && typeof where === "object" && Object.keys(where).length > 0) {
|
|
768
|
+
return { ...where, ...patch };
|
|
769
|
+
}
|
|
770
|
+
return patch;
|
|
771
|
+
}
|
|
772
|
+
function buildListFilterAndFromSearchParams(repo, searchParams) {
|
|
773
|
+
const and = {};
|
|
774
|
+
const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
|
|
775
|
+
for (const col of repo.metadata.columns) {
|
|
776
|
+
const name = col.propertyName;
|
|
777
|
+
if (!columnNames.has(name)) continue;
|
|
778
|
+
if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
|
|
779
|
+
if (!isListDateColumn(col)) continue;
|
|
780
|
+
const from = searchParams.get(`${name}From`)?.trim();
|
|
781
|
+
const to = searchParams.get(`${name}To`)?.trim();
|
|
782
|
+
if (!from && !to) continue;
|
|
783
|
+
if (from && to) {
|
|
784
|
+
and[name] = Between(dayStartUtc(from), dayEndUtc(to));
|
|
785
|
+
} else if (from) {
|
|
786
|
+
and[name] = MoreThanOrEqual(dayStartUtc(from));
|
|
787
|
+
} else if (to) {
|
|
788
|
+
and[name] = LessThanOrEqual(dayEndUtc(to));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
for (const col of repo.metadata.columns) {
|
|
792
|
+
const name = col.propertyName;
|
|
793
|
+
if (!columnNames.has(name)) continue;
|
|
794
|
+
if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
|
|
795
|
+
if (!isListNumericColumn(col)) continue;
|
|
796
|
+
if (Object.prototype.hasOwnProperty.call(and, name)) continue;
|
|
797
|
+
const minRaw = searchParams.get(`${name}Min`)?.trim();
|
|
798
|
+
const maxRaw = searchParams.get(`${name}Max`)?.trim();
|
|
799
|
+
if (!minRaw && !maxRaw) continue;
|
|
800
|
+
const parseNum = (s) => {
|
|
801
|
+
const n = Number(s);
|
|
802
|
+
return Number.isFinite(n) ? n : null;
|
|
803
|
+
};
|
|
804
|
+
const nMin = minRaw ? parseNum(minRaw) : null;
|
|
805
|
+
const nMax = maxRaw ? parseNum(maxRaw) : null;
|
|
806
|
+
if (nMin != null && nMax != null) {
|
|
807
|
+
and[name] = Between(nMin, nMax);
|
|
808
|
+
} else if (nMin != null) {
|
|
809
|
+
and[name] = MoreThanOrEqual(nMin);
|
|
810
|
+
} else if (nMax != null) {
|
|
811
|
+
and[name] = LessThanOrEqual(nMax);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
for (const col of repo.metadata.columns) {
|
|
815
|
+
const name = col.propertyName;
|
|
816
|
+
if (!columnNames.has(name)) continue;
|
|
817
|
+
if (LIST_QUERY_RESERVED.has(name)) continue;
|
|
818
|
+
if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
|
|
819
|
+
if (!isListStringColumn(col)) continue;
|
|
820
|
+
if (Object.prototype.hasOwnProperty.call(and, name)) continue;
|
|
821
|
+
const raw = searchParams.get(name)?.trim();
|
|
822
|
+
if (!raw) continue;
|
|
823
|
+
and[name] = ILike(`%${raw}%`);
|
|
824
|
+
}
|
|
825
|
+
return and;
|
|
826
|
+
}
|
|
827
|
+
function buildExactListParamWhere(repo, searchParams) {
|
|
828
|
+
const extraWhere = {};
|
|
829
|
+
const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
|
|
830
|
+
for (const col of repo.metadata.columns) {
|
|
831
|
+
const name = col.propertyName;
|
|
832
|
+
if (!columnNames.has(name)) continue;
|
|
833
|
+
if (name === "deleted" || name === "deletedAt" || name === "deletedBy") continue;
|
|
834
|
+
if (!isListNumericColumn(col)) continue;
|
|
835
|
+
const v = searchParams.get(name)?.trim();
|
|
836
|
+
if (v == null || v === "") continue;
|
|
837
|
+
const n = Number(v);
|
|
838
|
+
if (!Number.isFinite(n)) continue;
|
|
839
|
+
extraWhere[name] = n;
|
|
840
|
+
}
|
|
841
|
+
for (const col of repo.metadata.columns) {
|
|
842
|
+
if (String(col.type) !== "boolean") continue;
|
|
843
|
+
const name = col.propertyName;
|
|
844
|
+
if (!columnNames.has(name)) continue;
|
|
845
|
+
const raw = searchParams.get(name)?.trim();
|
|
846
|
+
if (raw === "true" || raw === "false") {
|
|
847
|
+
extraWhere[name] = raw === "true";
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return extraWhere;
|
|
851
|
+
}
|
|
576
852
|
function mergeDeletedFalseWhere(repo, where) {
|
|
577
853
|
if (!entityHasSoftDelete(repo)) return where;
|
|
578
854
|
const d = { deleted: false };
|
|
@@ -582,6 +858,12 @@ function mergeDeletedFalseWhere(repo, where) {
|
|
|
582
858
|
}
|
|
583
859
|
return Object.keys(where).length > 0 ? { ...where, ...d } : d;
|
|
584
860
|
}
|
|
861
|
+
function pickDefaultListSortField(columnNames, columns) {
|
|
862
|
+
for (const candidate of ["createdAt", "updatedAt", "id", "name", "sortOrder", "title"]) {
|
|
863
|
+
if (columnNames.has(candidate)) return candidate;
|
|
864
|
+
}
|
|
865
|
+
return columns[0]?.propertyName ?? "id";
|
|
866
|
+
}
|
|
585
867
|
function normalizeProductSku(value) {
|
|
586
868
|
if (value == null) return null;
|
|
587
869
|
const s = String(value).trim();
|
|
@@ -684,6 +966,18 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
684
966
|
if (statusFilter) qb.andWhere("order.status = :status", { status: statusFilter });
|
|
685
967
|
if (dateFrom) qb.andWhere("order.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
|
|
686
968
|
if (dateTo) qb.andWhere("order.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
|
|
969
|
+
const totalMin = searchParams.get("totalMin")?.trim();
|
|
970
|
+
const totalMax = searchParams.get("totalMax")?.trim();
|
|
971
|
+
if (totalMin) {
|
|
972
|
+
const n = Number(totalMin);
|
|
973
|
+
if (Number.isFinite(n)) qb.andWhere("order.total >= :totalMin", { totalMin: n });
|
|
974
|
+
}
|
|
975
|
+
if (totalMax) {
|
|
976
|
+
const n = Number(totalMax);
|
|
977
|
+
if (Number.isFinite(n)) qb.andWhere("order.total <= :totalMax", { totalMax: n });
|
|
978
|
+
}
|
|
979
|
+
const currency = searchParams.get("currency")?.trim();
|
|
980
|
+
if (currency) qb.andWhere("order.currency ILIKE :orderCurrency", { orderCurrency: `%${currency}%` });
|
|
687
981
|
if (orderIdsFromPayment && orderIdsFromPayment.length) qb.andWhere("order.id IN (:...orderIds)", { orderIds: orderIdsFromPayment });
|
|
688
982
|
const [rows, total2] = await qb.getManyAndCount();
|
|
689
983
|
const data2 = rows.map((order) => {
|
|
@@ -722,8 +1016,30 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
722
1016
|
if (statusFilter) qb.andWhere("payment.status = :status", { status: statusFilter });
|
|
723
1017
|
if (dateFrom) qb.andWhere("payment.createdAt >= :dateFrom", { dateFrom: /* @__PURE__ */ new Date(dateFrom + "T00:00:00.000Z") });
|
|
724
1018
|
if (dateTo) qb.andWhere("payment.createdAt <= :dateTo", { dateTo: /* @__PURE__ */ new Date(dateTo + "T23:59:59.999Z") });
|
|
1019
|
+
const paidAtFrom = searchParams.get("paidAtFrom")?.trim();
|
|
1020
|
+
const paidAtTo = searchParams.get("paidAtTo")?.trim();
|
|
1021
|
+
if (paidAtFrom) {
|
|
1022
|
+
qb.andWhere("payment.paidAt >= :paidAtFrom", { paidAtFrom: /* @__PURE__ */ new Date(paidAtFrom + "T00:00:00.000Z") });
|
|
1023
|
+
}
|
|
1024
|
+
if (paidAtTo) {
|
|
1025
|
+
qb.andWhere("payment.paidAt <= :paidAtTo", { paidAtTo: /* @__PURE__ */ new Date(paidAtTo + "T23:59:59.999Z") });
|
|
1026
|
+
}
|
|
725
1027
|
if (methodFilter) qb.andWhere("payment.method = :method", { method: methodFilter });
|
|
726
1028
|
if (orderNumberParam) qb.andWhere("ord.orderNumber ILIKE :orderNumber", { orderNumber: `%${orderNumberParam}%` });
|
|
1029
|
+
const amountMin = searchParams.get("amountMin")?.trim();
|
|
1030
|
+
const amountMax = searchParams.get("amountMax")?.trim();
|
|
1031
|
+
if (amountMin) {
|
|
1032
|
+
const n = Number(amountMin);
|
|
1033
|
+
if (Number.isFinite(n)) qb.andWhere("payment.amount >= :amountMin", { amountMin: n });
|
|
1034
|
+
}
|
|
1035
|
+
if (amountMax) {
|
|
1036
|
+
const n = Number(amountMax);
|
|
1037
|
+
if (Number.isFinite(n)) qb.andWhere("payment.amount <= :amountMax", { amountMax: n });
|
|
1038
|
+
}
|
|
1039
|
+
const extRef = searchParams.get("externalReference")?.trim();
|
|
1040
|
+
if (extRef) {
|
|
1041
|
+
qb.andWhere("payment.externalReference ILIKE :extRef", { extRef: `%${extRef}%` });
|
|
1042
|
+
}
|
|
727
1043
|
const [rows, total2] = await qb.getManyAndCount();
|
|
728
1044
|
const data2 = rows.map((payment) => {
|
|
729
1045
|
const order = payment.order;
|
|
@@ -742,18 +1058,36 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
742
1058
|
const repo2 = dataSource.getRepository(entity);
|
|
743
1059
|
const statusFilter = searchParams.get("status")?.trim();
|
|
744
1060
|
const inventory = searchParams.get("inventory")?.trim();
|
|
745
|
-
const productWhere = {
|
|
1061
|
+
const productWhere = {
|
|
1062
|
+
deleted: false,
|
|
1063
|
+
...buildListFilterAndFromSearchParams(repo2, searchParams)
|
|
1064
|
+
};
|
|
746
1065
|
if (statusFilter) productWhere.status = statusFilter;
|
|
747
1066
|
if (inventory === "in_stock") productWhere.quantity = MoreThan(0);
|
|
748
1067
|
if (inventory === "out_of_stock") productWhere.quantity = 0;
|
|
1068
|
+
for (const key of ["brandId", "categoryId", "collectionId"]) {
|
|
1069
|
+
const raw = searchParams.get(key)?.trim();
|
|
1070
|
+
if (raw) {
|
|
1071
|
+
const n = Number(raw);
|
|
1072
|
+
if (Number.isFinite(n)) productWhere[key] = n;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
const featuredRaw = searchParams.get("featured")?.trim();
|
|
1076
|
+
if (featuredRaw === "true" || featuredRaw === "false") {
|
|
1077
|
+
productWhere.featured = featuredRaw === "true";
|
|
1078
|
+
}
|
|
749
1079
|
if (search && typeof search === "string" && search.trim()) {
|
|
750
1080
|
productWhere.name = ILike(`%${search.trim()}%`);
|
|
751
1081
|
}
|
|
1082
|
+
const productColumnNames = new Set(repo2.metadata.columns.map((c) => c.propertyName));
|
|
1083
|
+
const defaultProductSort = pickDefaultListSortField(productColumnNames, repo2.metadata.columns);
|
|
1084
|
+
const sortParam2 = (searchParams.get("sortField") ?? "").trim();
|
|
1085
|
+
const productSortField = sortParam2 && productColumnNames.has(sortParam2) ? sortParam2 : defaultProductSort;
|
|
752
1086
|
const [data2, total2] = await repo2.findAndCount({
|
|
753
1087
|
where: Object.keys(productWhere).length ? productWhere : void 0,
|
|
754
1088
|
skip,
|
|
755
1089
|
take: limit,
|
|
756
|
-
order: { [
|
|
1090
|
+
order: { [productSortField]: sortOrder }
|
|
757
1091
|
});
|
|
758
1092
|
return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
|
|
759
1093
|
}
|
|
@@ -846,27 +1180,22 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
846
1180
|
}
|
|
847
1181
|
return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
|
|
848
1182
|
}
|
|
849
|
-
const
|
|
1183
|
+
const defaultSortField = pickDefaultListSortField(columnNames, repo.metadata.columns);
|
|
1184
|
+
const sortParam = (searchParams.get("sortField") ?? "").trim();
|
|
1185
|
+
const sortField = sortParam && columnNames.has(sortParam) ? sortParam : defaultSortField;
|
|
850
1186
|
let where = {};
|
|
851
1187
|
if (search) {
|
|
852
1188
|
where = buildSearchWhereClause(repo, search);
|
|
853
1189
|
}
|
|
854
|
-
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
const v = searchParams.get(key);
|
|
858
|
-
if (v != null && v !== "" && columnNames.has(key)) {
|
|
859
|
-
const n = Number(v);
|
|
860
|
-
if (Number.isFinite(n)) extraWhere[key] = n;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
if (Object.keys(extraWhere).length > 0) {
|
|
1190
|
+
where = mergeListWhereAnd(where, buildListFilterAndFromSearchParams(repo, searchParams));
|
|
1191
|
+
const exactParamWhere = buildExactListParamWhere(repo, searchParams);
|
|
1192
|
+
if (Object.keys(exactParamWhere).length > 0) {
|
|
864
1193
|
if (Array.isArray(where)) {
|
|
865
|
-
where = where.map((w) => ({ ...w, ...
|
|
1194
|
+
where = where.map((w) => ({ ...w, ...exactParamWhere }));
|
|
866
1195
|
} else if (where && typeof where === "object" && Object.keys(where).length > 0) {
|
|
867
|
-
where = { ...where, ...
|
|
1196
|
+
where = { ...where, ...exactParamWhere };
|
|
868
1197
|
} else {
|
|
869
|
-
where =
|
|
1198
|
+
where = exactParamWhere;
|
|
870
1199
|
}
|
|
871
1200
|
}
|
|
872
1201
|
where = mergeDeletedFalseWhere(repo, where);
|
|
@@ -946,6 +1275,10 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
946
1275
|
}
|
|
947
1276
|
const repo = dataSource.getRepository(entity);
|
|
948
1277
|
const persistBody = resource === "media" ? body : pickColumnUpdates(repo, body);
|
|
1278
|
+
if (resource === "contacts" && "type" in persistBody) {
|
|
1279
|
+
const t = persistBody.type;
|
|
1280
|
+
if (t === "" || t === "none" || t == null) persistBody.type = null;
|
|
1281
|
+
}
|
|
949
1282
|
if (resource !== "media" && Object.keys(persistBody).length === 0) {
|
|
950
1283
|
logCrudClientError("POST create", {
|
|
951
1284
|
reason: "no_scalar_columns_after_pick",
|
|
@@ -968,6 +1301,17 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
968
1301
|
}
|
|
969
1302
|
}
|
|
970
1303
|
}
|
|
1304
|
+
if (resource === "addresses") {
|
|
1305
|
+
const cid = Number(persistBody.contactId);
|
|
1306
|
+
if (!Number.isFinite(cid)) {
|
|
1307
|
+
return json({ error: "Valid contactId is required." }, { status: 400 });
|
|
1308
|
+
}
|
|
1309
|
+
if (persistBody.tag === "") persistBody.tag = null;
|
|
1310
|
+
const addrErr = validateAndNormalizeAddressRow(persistBody);
|
|
1311
|
+
if (addrErr) {
|
|
1312
|
+
return json({ error: addrErr }, { status: 400 });
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
971
1315
|
sanitizeBodyForEntity(repo, persistBody);
|
|
972
1316
|
let created;
|
|
973
1317
|
try {
|
|
@@ -1285,10 +1629,60 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
1285
1629
|
if (!cur) return json({ message: "Not found" }, { status: 404 });
|
|
1286
1630
|
}
|
|
1287
1631
|
const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
|
|
1632
|
+
if (resource === "contacts" && "type" in updatePayload) {
|
|
1633
|
+
const t = updatePayload.type;
|
|
1634
|
+
if (t === "" || t === "none" || t == null) updatePayload.type = null;
|
|
1635
|
+
}
|
|
1288
1636
|
if (resource === "media") {
|
|
1289
1637
|
const u = updatePayload;
|
|
1290
|
-
delete u.parentId;
|
|
1291
1638
|
delete u.kind;
|
|
1639
|
+
if (rawBody && typeof rawBody === "object" && "parentId" in rawBody) {
|
|
1640
|
+
let pid = null;
|
|
1641
|
+
const p = rawBody.parentId;
|
|
1642
|
+
if (p != null && p !== "") {
|
|
1643
|
+
const n = Number(p);
|
|
1644
|
+
if (!Number.isFinite(n)) {
|
|
1645
|
+
return json({ error: "Invalid parentId" }, { status: 400 });
|
|
1646
|
+
}
|
|
1647
|
+
pid = n;
|
|
1648
|
+
}
|
|
1649
|
+
if (pid != null) {
|
|
1650
|
+
const parent = await repo.findOne({
|
|
1651
|
+
where: { id: pid, deleted: false }
|
|
1652
|
+
});
|
|
1653
|
+
if (!parent || parent.kind !== "folder") {
|
|
1654
|
+
return json({ error: "parent must be a folder" }, { status: 400 });
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
const row = await repo.findOne({
|
|
1658
|
+
where: { id: numericId, deleted: false }
|
|
1659
|
+
});
|
|
1660
|
+
if (!row) return json({ message: "Not found" }, { status: 404 });
|
|
1661
|
+
if (pid === numericId) {
|
|
1662
|
+
return json({ error: "Invalid parentId" }, { status: 400 });
|
|
1663
|
+
}
|
|
1664
|
+
if (row.kind === "folder" && pid != null) {
|
|
1665
|
+
let walk = pid;
|
|
1666
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1667
|
+
while (walk != null) {
|
|
1668
|
+
if (walk === numericId) {
|
|
1669
|
+
return json(
|
|
1670
|
+
{ error: "Cannot move a folder into itself or a descendant folder" },
|
|
1671
|
+
{ status: 400 }
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
if (seen.has(walk)) break;
|
|
1675
|
+
seen.add(walk);
|
|
1676
|
+
const anc = await repo.findOne({
|
|
1677
|
+
where: { id: walk, deleted: false }
|
|
1678
|
+
});
|
|
1679
|
+
walk = anc ? anc.parentId ?? null : null;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
u.parentId = pid;
|
|
1683
|
+
} else {
|
|
1684
|
+
delete u.parentId;
|
|
1685
|
+
}
|
|
1292
1686
|
}
|
|
1293
1687
|
if (resource === "products") {
|
|
1294
1688
|
const currentRow = await repo.findOne({
|
|
@@ -1307,6 +1701,26 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
1307
1701
|
updatePayload.sku = effSku;
|
|
1308
1702
|
}
|
|
1309
1703
|
}
|
|
1704
|
+
if (resource === "addresses" && Object.keys(updatePayload).length > 0) {
|
|
1705
|
+
const currentRow = await repo.findOne({
|
|
1706
|
+
where: { id: numericId }
|
|
1707
|
+
});
|
|
1708
|
+
if (!currentRow) return json({ message: "Not found" }, { status: 404 });
|
|
1709
|
+
const merged = {
|
|
1710
|
+
...currentRow,
|
|
1711
|
+
...updatePayload
|
|
1712
|
+
};
|
|
1713
|
+
if (merged.tag === "") merged.tag = null;
|
|
1714
|
+
const addrErr = validateAndNormalizeAddressRow(merged);
|
|
1715
|
+
if (addrErr) {
|
|
1716
|
+
return json({ error: addrErr }, { status: 400 });
|
|
1717
|
+
}
|
|
1718
|
+
for (const k of Object.keys(updatePayload)) {
|
|
1719
|
+
if (k in merged) {
|
|
1720
|
+
updatePayload[k] = merged[k];
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1310
1724
|
if (Object.keys(updatePayload).length > 0) {
|
|
1311
1725
|
sanitizeBodyForEntity(repo, updatePayload);
|
|
1312
1726
|
await repo.update(numericId, updatePayload);
|
|
@@ -1508,7 +1922,7 @@ function createUserAuthApiRouter(config) {
|
|
|
1508
1922
|
// src/api/cms-handlers.ts
|
|
1509
1923
|
init_email_queue();
|
|
1510
1924
|
init_erp_queue();
|
|
1511
|
-
import { MoreThanOrEqual, ILike as ILike2, In } from "typeorm";
|
|
1925
|
+
import { MoreThanOrEqual as MoreThanOrEqual2, ILike as ILike2, In } from "typeorm";
|
|
1512
1926
|
|
|
1513
1927
|
// src/plugins/captcha/assert.ts
|
|
1514
1928
|
async function assertCaptchaOk(getCms, body, req, json) {
|
|
@@ -1660,11 +2074,11 @@ async function readBufferFromPublicUrl(url) {
|
|
|
1660
2074
|
throw new Error("Unsupported media URL");
|
|
1661
2075
|
}
|
|
1662
2076
|
function sanitizeZipPath(entryName) {
|
|
1663
|
-
const
|
|
1664
|
-
for (const seg of
|
|
2077
|
+
const norm2 = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
2078
|
+
for (const seg of norm2) {
|
|
1665
2079
|
if (seg === ".." || seg === ".") return null;
|
|
1666
2080
|
}
|
|
1667
|
-
return
|
|
2081
|
+
return norm2;
|
|
1668
2082
|
}
|
|
1669
2083
|
function shouldSkipEntry(parts) {
|
|
1670
2084
|
if (parts[0] === "__MACOSX") return true;
|
|
@@ -1894,9 +2308,9 @@ function createDashboardStatsHandler(config) {
|
|
|
1894
2308
|
repo("users")?.count({ where: { deleted: false } }) ?? 0,
|
|
1895
2309
|
repo("blogs")?.count({ where: { deleted: false } }) ?? 0,
|
|
1896
2310
|
repo("contacts")?.count({
|
|
1897
|
-
where: { deleted: false, createdAt:
|
|
2311
|
+
where: { deleted: false, createdAt: MoreThanOrEqual2(sevenDaysAgo) }
|
|
1898
2312
|
}) ?? 0,
|
|
1899
|
-
repo("form_submissions")?.count({ where: { createdAt:
|
|
2313
|
+
repo("form_submissions")?.count({ where: { createdAt: MoreThanOrEqual2(sevenDaysAgo) } }) ?? 0,
|
|
1900
2314
|
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() ?? []
|
|
1901
2315
|
]);
|
|
1902
2316
|
return json({
|
|
@@ -1945,19 +2359,19 @@ function createEcommerceAnalyticsHandler(config) {
|
|
|
1945
2359
|
const productRepo = dataSource.getRepository(entityMap.products);
|
|
1946
2360
|
const [salesOrders, returnOrders, replacementOrders, payments, products] = await Promise.all([
|
|
1947
2361
|
orderRepo.find({
|
|
1948
|
-
where: { deleted: false, createdAt:
|
|
2362
|
+
where: { deleted: false, createdAt: MoreThanOrEqual2(start), orderKind: "sale", status: In(["confirmed", "processing", "completed"]) },
|
|
1949
2363
|
select: ["id", "contactId", "createdAt", "subtotal", "discount", "tax", "total", "status"]
|
|
1950
2364
|
}),
|
|
1951
2365
|
orderRepo.find({
|
|
1952
|
-
where: { deleted: false, createdAt:
|
|
2366
|
+
where: { deleted: false, createdAt: MoreThanOrEqual2(start), orderKind: "return" },
|
|
1953
2367
|
select: ["id", "createdAt", "total"]
|
|
1954
2368
|
}),
|
|
1955
2369
|
orderRepo.find({
|
|
1956
|
-
where: { deleted: false, createdAt:
|
|
2370
|
+
where: { deleted: false, createdAt: MoreThanOrEqual2(start), orderKind: "replacement" },
|
|
1957
2371
|
select: ["id", "createdAt", "total"]
|
|
1958
2372
|
}),
|
|
1959
2373
|
paymentRepo.find({
|
|
1960
|
-
where: { deleted: false, createdAt:
|
|
2374
|
+
where: { deleted: false, createdAt: MoreThanOrEqual2(start) },
|
|
1961
2375
|
select: ["id", "status", "method", "amount", "createdAt"]
|
|
1962
2376
|
}),
|
|
1963
2377
|
productRepo.find({
|
|
@@ -2684,18 +3098,37 @@ function createUsersApiHandlers(config) {
|
|
|
2684
3098
|
try {
|
|
2685
3099
|
const body = await req.json();
|
|
2686
3100
|
if (!body?.name || !body?.email) return json({ error: "Name and email are required" }, { status: 400 });
|
|
2687
|
-
const
|
|
2688
|
-
|
|
3101
|
+
const email = body.email;
|
|
3102
|
+
const existing = await userRepo().findOne({ where: { email } });
|
|
3103
|
+
if (existing && !existing.deleted) {
|
|
3104
|
+
return json({ error: "User with this email already exists" }, { status: 400 });
|
|
3105
|
+
}
|
|
2689
3106
|
const groupRepo = dataSource.getRepository(entityMap.user_groups);
|
|
2690
3107
|
const customerG = await groupRepo.findOne({ where: { name: "Customer", deleted: false } });
|
|
2691
3108
|
const gid = body.groupId ?? null;
|
|
2692
3109
|
const isCustomer = !!(customerG && gid === customerG.id);
|
|
2693
3110
|
const adminAccess = isCustomer ? false : body.adminAccess === false ? false : true;
|
|
2694
3111
|
const blocked = body.blocked === true || body.blocked === "true" || body.blocked === 1 || body.blocked === "1";
|
|
2695
|
-
const newUser = await
|
|
3112
|
+
const newUser = existing?.deleted ? await (async () => {
|
|
3113
|
+
await userRepo().update(existing.id, {
|
|
3114
|
+
deleted: false,
|
|
3115
|
+
deletedAt: null,
|
|
3116
|
+
deletedBy: null,
|
|
3117
|
+
name: body.name,
|
|
3118
|
+
email,
|
|
3119
|
+
password: null,
|
|
3120
|
+
blocked,
|
|
3121
|
+
groupId: gid,
|
|
3122
|
+
adminAccess,
|
|
3123
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3124
|
+
});
|
|
3125
|
+
const row = await userRepo().findOne({ where: { id: existing.id } });
|
|
3126
|
+
if (!row) throw new Error("user missing after restore");
|
|
3127
|
+
return row;
|
|
3128
|
+
})() : await userRepo().save(
|
|
2696
3129
|
userRepo().create({
|
|
2697
3130
|
name: body.name,
|
|
2698
|
-
email
|
|
3131
|
+
email,
|
|
2699
3132
|
password: null,
|
|
2700
3133
|
blocked,
|
|
2701
3134
|
groupId: gid,
|
|
@@ -2848,21 +3281,110 @@ function createUserAvatarHandler(config) {
|
|
|
2848
3281
|
}
|
|
2849
3282
|
};
|
|
2850
3283
|
}
|
|
3284
|
+
var PROFILE_EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2851
3285
|
function createUserProfileHandler(config) {
|
|
2852
|
-
const { dataSource, entityMap, json, getSession } = config;
|
|
2853
|
-
|
|
3286
|
+
const { dataSource, entityMap, json, getSession, onProfileUpdated } = config;
|
|
3287
|
+
async function loadCurrentUser() {
|
|
2854
3288
|
const session = await getSession();
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
3289
|
+
const su = session?.user;
|
|
3290
|
+
if (!su?.email && su?.id == null) {
|
|
3291
|
+
return { ok: false, response: json({ error: "Unauthorized" }, { status: 401 }) };
|
|
3292
|
+
}
|
|
3293
|
+
const userRepo = dataSource.getRepository(entityMap.users);
|
|
3294
|
+
let user = null;
|
|
3295
|
+
const uidRaw = su.id != null ? String(su.id).trim() : "";
|
|
3296
|
+
const uid = uidRaw && /^\d+$/.test(uidRaw) ? parseInt(uidRaw, 10) : NaN;
|
|
3297
|
+
if (Number.isFinite(uid) && uid > 0) {
|
|
3298
|
+
user = await userRepo.findOne({
|
|
3299
|
+
where: { id: uid, deleted: false },
|
|
3300
|
+
select: ["id", "name", "email", "phone", "createdAt"]
|
|
3301
|
+
});
|
|
3302
|
+
}
|
|
3303
|
+
if (!user && su.email) {
|
|
3304
|
+
const em = String(su.email).trim().toLowerCase();
|
|
3305
|
+
if (em) {
|
|
3306
|
+
user = await userRepo.findOne({
|
|
3307
|
+
where: { email: em, deleted: false },
|
|
3308
|
+
select: ["id", "name", "email", "phone", "createdAt"]
|
|
3309
|
+
});
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
if (!user) return { ok: false, response: json({ error: "Not found" }, { status: 404 }) };
|
|
3313
|
+
return { ok: true, user };
|
|
3314
|
+
}
|
|
3315
|
+
return {
|
|
3316
|
+
async GET(_req) {
|
|
3317
|
+
try {
|
|
3318
|
+
const r = await loadCurrentUser();
|
|
3319
|
+
if (!r.ok) return r.response;
|
|
3320
|
+
const u = r.user;
|
|
3321
|
+
return json({
|
|
3322
|
+
id: u.id,
|
|
3323
|
+
name: u.name ?? "",
|
|
3324
|
+
email: u.email ?? "",
|
|
3325
|
+
phone: u.phone ?? null,
|
|
3326
|
+
createdAt: u.createdAt instanceof Date ? u.createdAt.toISOString() : u.createdAt ?? void 0
|
|
3327
|
+
});
|
|
3328
|
+
} catch {
|
|
3329
|
+
return json({ error: "Internal server error" }, { status: 500 });
|
|
3330
|
+
}
|
|
3331
|
+
},
|
|
3332
|
+
async PUT(req) {
|
|
3333
|
+
try {
|
|
3334
|
+
const r = await loadCurrentUser();
|
|
3335
|
+
if (!r.ok) return r.response;
|
|
3336
|
+
const current = r.user;
|
|
3337
|
+
let body;
|
|
3338
|
+
try {
|
|
3339
|
+
body = await req.json();
|
|
3340
|
+
} catch {
|
|
3341
|
+
return json({ error: "Invalid JSON" }, { status: 400 });
|
|
3342
|
+
}
|
|
3343
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
3344
|
+
if (!name) return json({ error: "Name is required" }, { status: 400 });
|
|
3345
|
+
const emailRaw = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
|
|
3346
|
+
if (!emailRaw || !PROFILE_EMAIL_RE.test(emailRaw)) {
|
|
3347
|
+
return json({ error: "Valid email is required" }, { status: 400 });
|
|
3348
|
+
}
|
|
3349
|
+
const phone = body.phone === null || body.phone === void 0 ? null : typeof body.phone === "string" ? body.phone.trim() || null : null;
|
|
3350
|
+
const userRepo = dataSource.getRepository(entityMap.users);
|
|
3351
|
+
if (emailRaw !== String(current.email ?? "").toLowerCase()) {
|
|
3352
|
+
const taken = await userRepo.findOne({
|
|
3353
|
+
where: { email: emailRaw, deleted: false },
|
|
3354
|
+
select: ["id"]
|
|
3355
|
+
});
|
|
3356
|
+
if (taken && taken.id !== current.id) {
|
|
3357
|
+
return json({ error: "Email is already in use" }, { status: 409 });
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
await userRepo.update(
|
|
3361
|
+
{ id: current.id },
|
|
3362
|
+
{
|
|
3363
|
+
name,
|
|
3364
|
+
email: emailRaw,
|
|
3365
|
+
phone,
|
|
3366
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
3367
|
+
}
|
|
3368
|
+
);
|
|
3369
|
+
const updated = await userRepo.findOne({
|
|
3370
|
+
where: { id: current.id },
|
|
3371
|
+
select: ["id", "name", "email", "phone"]
|
|
3372
|
+
});
|
|
3373
|
+
if (!updated) return json({ error: "Not found" }, { status: 404 });
|
|
3374
|
+
const row = updated;
|
|
3375
|
+
if (onProfileUpdated) {
|
|
3376
|
+
try {
|
|
3377
|
+
await onProfileUpdated(req, row);
|
|
3378
|
+
} catch {
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
return json({
|
|
3382
|
+
message: "Profile updated successfully",
|
|
3383
|
+
user: { id: row.id, name: row.name, email: row.email, phone: row.phone }
|
|
3384
|
+
});
|
|
3385
|
+
} catch {
|
|
3386
|
+
return json({ error: "Internal server error" }, { status: 500 });
|
|
3387
|
+
}
|
|
2866
3388
|
}
|
|
2867
3389
|
};
|
|
2868
3390
|
}
|
|
@@ -6884,7 +7406,7 @@ function createCmsApiHandler(config) {
|
|
|
6884
7406
|
} : usersApi;
|
|
6885
7407
|
const usersHandlers = usersApiMerged ? createUsersApiHandlers(mergePerm(usersApiMerged) ?? usersApiMerged) : null;
|
|
6886
7408
|
const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
|
|
6887
|
-
const
|
|
7409
|
+
const profileHandlers = userProfile ? createUserProfileHandler(userProfile) : null;
|
|
6888
7410
|
const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
|
|
6889
7411
|
const smsMessageTemplateHandlers = createSmsMessageTemplateHandlers({
|
|
6890
7412
|
dataSource,
|
|
@@ -6983,7 +7505,10 @@ function createCmsApiHandler(config) {
|
|
|
6983
7505
|
}
|
|
6984
7506
|
if (path.length === 2) {
|
|
6985
7507
|
if (path[1] === "avatar" && m === "POST" && avatarPost) return avatarPost(req);
|
|
6986
|
-
if (path[1] === "profile" &&
|
|
7508
|
+
if (path[1] === "profile" && profileHandlers) {
|
|
7509
|
+
if (m === "GET") return profileHandlers.GET(req);
|
|
7510
|
+
if (m === "PUT") return profileHandlers.PUT(req);
|
|
7511
|
+
}
|
|
6987
7512
|
const id = path[1];
|
|
6988
7513
|
if (m === "GET") return usersHandlers.getById(req, id);
|
|
6989
7514
|
if (m === "PUT" || m === "PATCH") return usersHandlers.update(req, id);
|
|
@@ -8046,18 +8571,19 @@ function createStorefrontApiHandler(config) {
|
|
|
8046
8571
|
const contactOrErr = await getContactForAddresses();
|
|
8047
8572
|
if (contactOrErr instanceof Response) return contactOrErr;
|
|
8048
8573
|
const b = await req.json().catch(() => ({}));
|
|
8049
|
-
const
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
);
|
|
8574
|
+
const row = {
|
|
8575
|
+
contactId: contactOrErr.contactId,
|
|
8576
|
+
tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
|
|
8577
|
+
line1: typeof b.line1 === "string" ? b.line1 : "",
|
|
8578
|
+
line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
|
|
8579
|
+
city: typeof b.city === "string" ? b.city : "",
|
|
8580
|
+
state: typeof b.state === "string" ? b.state : "",
|
|
8581
|
+
postalCode: typeof b.postalCode === "string" ? b.postalCode : "",
|
|
8582
|
+
country: typeof b.country === "string" ? b.country : ""
|
|
8583
|
+
};
|
|
8584
|
+
const addrErr = validateAndNormalizeAddressRow(row);
|
|
8585
|
+
if (addrErr) return json({ error: addrErr }, { status: 400 });
|
|
8586
|
+
const created = await addressRepo().save(addressRepo().create(row));
|
|
8061
8587
|
return json(serializeAddress2(created));
|
|
8062
8588
|
}
|
|
8063
8589
|
if (path[0] === "addresses" && path.length === 2 && (method === "PATCH" || method === "PUT")) {
|
|
@@ -8076,7 +8602,16 @@ function createStorefrontApiHandler(config) {
|
|
|
8076
8602
|
if (b.state !== void 0) updates.state = typeof b.state === "string" ? b.state.trim() || null : null;
|
|
8077
8603
|
if (b.postalCode !== void 0) updates.postalCode = typeof b.postalCode === "string" ? b.postalCode.trim() || null : null;
|
|
8078
8604
|
if (b.country !== void 0) updates.country = typeof b.country === "string" ? b.country.trim() || null : null;
|
|
8079
|
-
if (Object.keys(updates).length)
|
|
8605
|
+
if (Object.keys(updates).length) {
|
|
8606
|
+
const merged = { ...existing, ...updates };
|
|
8607
|
+
if (merged.tag === "") merged.tag = null;
|
|
8608
|
+
const addrErr = validateAndNormalizeAddressRow(merged);
|
|
8609
|
+
if (addrErr) return json({ error: addrErr }, { status: 400 });
|
|
8610
|
+
for (const k of Object.keys(updates)) {
|
|
8611
|
+
if (k in merged) updates[k] = merged[k];
|
|
8612
|
+
}
|
|
8613
|
+
await addressRepo().update(id, updates);
|
|
8614
|
+
}
|
|
8080
8615
|
const updated = await addressRepo().findOne({ where: { id } });
|
|
8081
8616
|
return json(serializeAddress2(updated));
|
|
8082
8617
|
}
|