@infuro/cms-core 1.0.15 → 1.0.16
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 +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 +577 -29
- 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 +572 -26
- package/dist/api.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-BiagwMjV.d.ts → index-C85X7cc7.d.ts} +14 -2
- package/dist/{index-BQnqJ7EO.d.cts → index-h42MoUNq.d.cts} +14 -2
- package/dist/index.cjs +5225 -4305
- 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 +5220 -4317
- package/dist/index.js.map +1 -1
- package/dist/migrations/1775200000000-MediaDriveFolders.ts +38 -0
- package/package.json +10 -12
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,
|
|
@@ -757,14 +759,38 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
757
759
|
const repo = dataSource.getRepository(entity);
|
|
758
760
|
const typeFilter = searchParams.get("type");
|
|
759
761
|
const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
|
|
762
|
+
if (resource === "media") {
|
|
763
|
+
const qb = repo.createQueryBuilder("m");
|
|
764
|
+
const parentIdParam = searchParams.get("parentId");
|
|
765
|
+
if (parentIdParam != null && parentIdParam !== "") {
|
|
766
|
+
const n = Number(parentIdParam);
|
|
767
|
+
if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
|
|
768
|
+
qb.where("m.parentId = :pid", { pid: n });
|
|
769
|
+
} else {
|
|
770
|
+
qb.where("m.parentId IS NULL");
|
|
771
|
+
}
|
|
772
|
+
if (search && typeof search === "string" && search.trim()) {
|
|
773
|
+
qb.andWhere("m.filename ILIKE :search", { search: `%${search.trim()}%` });
|
|
774
|
+
}
|
|
775
|
+
if (typeFilter) {
|
|
776
|
+
qb.andWhere(
|
|
777
|
+
new import_typeorm.Brackets((sq) => {
|
|
778
|
+
sq.where("m.kind = :folderKind", { folderKind: "folder" }).orWhere("m.mimeType LIKE :mtp", {
|
|
779
|
+
mtp: `${typeFilter}/%`
|
|
780
|
+
});
|
|
781
|
+
})
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
const allowedSort = ["filename", "createdAt", "id"];
|
|
785
|
+
const sf = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "filename";
|
|
786
|
+
const so = sortOrder === "DESC" ? "DESC" : "ASC";
|
|
787
|
+
qb.orderBy("CASE WHEN m.kind = :fk THEN 0 ELSE 1 END", "ASC").addOrderBy(`m.${sf}`, so).setParameter("fk", "folder").skip(skip).take(limit);
|
|
788
|
+
const [data2, total2] = await qb.getManyAndCount();
|
|
789
|
+
return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
|
|
790
|
+
}
|
|
760
791
|
const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
|
|
761
792
|
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) {
|
|
793
|
+
if (search) {
|
|
768
794
|
where = buildSearchWhereClause(repo, search);
|
|
769
795
|
}
|
|
770
796
|
const intFilterKeys = ["productId", "attributeId", "taxId"];
|
|
@@ -804,6 +830,38 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
804
830
|
if (!body || typeof body !== "object" || Object.keys(body).length === 0) {
|
|
805
831
|
return json({ error: "Invalid request payload" }, { status: 400 });
|
|
806
832
|
}
|
|
833
|
+
if (resource === "media") {
|
|
834
|
+
const b = body;
|
|
835
|
+
const kind = b.kind === "folder" ? "folder" : "file";
|
|
836
|
+
b.kind = kind;
|
|
837
|
+
const fn = String(b.filename ?? "").trim().slice(0, 255);
|
|
838
|
+
if (!fn) return json({ error: "filename required" }, { status: 400 });
|
|
839
|
+
b.filename = fn;
|
|
840
|
+
let pid = null;
|
|
841
|
+
if (b.parentId != null && b.parentId !== "") {
|
|
842
|
+
const n = Number(b.parentId);
|
|
843
|
+
if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
|
|
844
|
+
pid = n;
|
|
845
|
+
}
|
|
846
|
+
b.parentId = pid;
|
|
847
|
+
const mediaRepo = dataSource.getRepository(entityMap.media);
|
|
848
|
+
if (pid != null) {
|
|
849
|
+
const parent = await mediaRepo.findOne({ where: { id: pid } });
|
|
850
|
+
if (!parent || parent.kind !== "folder") {
|
|
851
|
+
return json({ error: "parent must be a folder" }, { status: 400 });
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (kind === "folder") {
|
|
855
|
+
b.url = null;
|
|
856
|
+
b.mimeType = "inode/directory";
|
|
857
|
+
b.size = 0;
|
|
858
|
+
} else {
|
|
859
|
+
if (!b.url || typeof b.url !== "string") return json({ error: "url required for files" }, { status: 400 });
|
|
860
|
+
if (!b.mimeType || typeof b.mimeType !== "string") {
|
|
861
|
+
b.mimeType = "application/octet-stream";
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
807
865
|
const repo = dataSource.getRepository(entity);
|
|
808
866
|
sanitizeBodyForEntity(repo, body);
|
|
809
867
|
const created = await repo.save(repo.create(body));
|
|
@@ -1076,6 +1134,11 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
1076
1134
|
return updated2 ? json(updated2) : json({ message: "Not found" }, { status: 404 });
|
|
1077
1135
|
}
|
|
1078
1136
|
const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
|
|
1137
|
+
if (resource === "media") {
|
|
1138
|
+
const u = updatePayload;
|
|
1139
|
+
delete u.parentId;
|
|
1140
|
+
delete u.kind;
|
|
1141
|
+
}
|
|
1079
1142
|
if (Object.keys(updatePayload).length > 0) {
|
|
1080
1143
|
sanitizeBodyForEntity(repo, updatePayload);
|
|
1081
1144
|
await repo.update(numericId, updatePayload);
|
|
@@ -1250,7 +1313,7 @@ function createUserAuthApiRouter(config) {
|
|
|
1250
1313
|
}
|
|
1251
1314
|
|
|
1252
1315
|
// src/api/cms-handlers.ts
|
|
1253
|
-
var
|
|
1316
|
+
var import_typeorm4 = require("typeorm");
|
|
1254
1317
|
init_email_queue();
|
|
1255
1318
|
init_erp_queue();
|
|
1256
1319
|
|
|
@@ -1270,6 +1333,194 @@ async function assertCaptchaOk(getCms, body, req, json) {
|
|
|
1270
1333
|
return json({ error: result.message }, { status: result.status });
|
|
1271
1334
|
}
|
|
1272
1335
|
|
|
1336
|
+
// src/lib/media-folder-path.ts
|
|
1337
|
+
function sanitizeMediaFolderPath(input) {
|
|
1338
|
+
if (input == null) return "";
|
|
1339
|
+
if (typeof input !== "string") return "";
|
|
1340
|
+
const segments = input.replace(/\\/g, "/").split("/").map((s) => s.trim()).filter(Boolean).filter((s) => s !== ".." && s !== ".");
|
|
1341
|
+
const joined = segments.join("/");
|
|
1342
|
+
return joined.length > 512 ? joined.slice(0, 512) : joined;
|
|
1343
|
+
}
|
|
1344
|
+
function sanitizeStorageSegment(name) {
|
|
1345
|
+
const s = name.replace(/[/\\]/g, "-").trim().slice(0, 255);
|
|
1346
|
+
return s || "item";
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/lib/media-parent-path.ts
|
|
1350
|
+
async function relativePathFromMediaParentId(dataSource, entityMap, parentId) {
|
|
1351
|
+
if (parentId == null) return "";
|
|
1352
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1353
|
+
const segments = [];
|
|
1354
|
+
let id = parentId;
|
|
1355
|
+
for (let d = 0; d < 64 && id != null; d++) {
|
|
1356
|
+
const row = await repo.findOne({ where: { id } });
|
|
1357
|
+
if (!row) break;
|
|
1358
|
+
const m = row;
|
|
1359
|
+
if (m.kind !== "folder") break;
|
|
1360
|
+
segments.unshift(sanitizeStorageSegment(m.filename));
|
|
1361
|
+
id = m.parentId ?? null;
|
|
1362
|
+
}
|
|
1363
|
+
return segments.join("/");
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// src/lib/media-zip-extract.ts
|
|
1367
|
+
var import_typeorm3 = require("typeorm");
|
|
1368
|
+
var ZIP_MIME_TYPES = /* @__PURE__ */ new Set(["application/zip", "application/x-zip-compressed"]);
|
|
1369
|
+
var MAX_ENTRIES = 2e3;
|
|
1370
|
+
var MAX_TOTAL_UNCOMPRESSED = 80 * 1024 * 1024;
|
|
1371
|
+
function isZipMedia(mime, filename) {
|
|
1372
|
+
if (mime && ZIP_MIME_TYPES.has(mime)) return true;
|
|
1373
|
+
return filename.toLowerCase().endsWith(".zip");
|
|
1374
|
+
}
|
|
1375
|
+
async function readBufferFromPublicUrl(url) {
|
|
1376
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
1377
|
+
const r = await fetch(url);
|
|
1378
|
+
if (!r.ok) throw new Error("Failed to download file");
|
|
1379
|
+
return Buffer.from(await r.arrayBuffer());
|
|
1380
|
+
}
|
|
1381
|
+
if (url.startsWith("/")) {
|
|
1382
|
+
const { readFile } = await import("fs/promises");
|
|
1383
|
+
const { join } = await import("path");
|
|
1384
|
+
const rel = url.replace(/^\/+/, "");
|
|
1385
|
+
return readFile(join(process.cwd(), "public", rel));
|
|
1386
|
+
}
|
|
1387
|
+
throw new Error("Unsupported media URL");
|
|
1388
|
+
}
|
|
1389
|
+
function sanitizeZipPath(entryName) {
|
|
1390
|
+
const norm = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
1391
|
+
for (const seg of norm) {
|
|
1392
|
+
if (seg === ".." || seg === ".") return null;
|
|
1393
|
+
}
|
|
1394
|
+
return norm;
|
|
1395
|
+
}
|
|
1396
|
+
function shouldSkipEntry(parts) {
|
|
1397
|
+
if (parts[0] === "__MACOSX") return true;
|
|
1398
|
+
const last = parts[parts.length - 1];
|
|
1399
|
+
if (last === ".DS_Store") return true;
|
|
1400
|
+
return false;
|
|
1401
|
+
}
|
|
1402
|
+
function guessMimeType(fileName) {
|
|
1403
|
+
const lower = fileName.toLowerCase();
|
|
1404
|
+
if (lower.endsWith(".png")) return "image/png";
|
|
1405
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
1406
|
+
if (lower.endsWith(".gif")) return "image/gif";
|
|
1407
|
+
if (lower.endsWith(".webp")) return "image/webp";
|
|
1408
|
+
if (lower.endsWith(".svg")) return "image/svg+xml";
|
|
1409
|
+
if (lower.endsWith(".pdf")) return "application/pdf";
|
|
1410
|
+
if (lower.endsWith(".txt")) return "text/plain";
|
|
1411
|
+
if (lower.endsWith(".json")) return "application/json";
|
|
1412
|
+
if (lower.endsWith(".zip")) return "application/zip";
|
|
1413
|
+
return "application/octet-stream";
|
|
1414
|
+
}
|
|
1415
|
+
async function findOrCreateFolder(dataSource, entityMap, parentId, name) {
|
|
1416
|
+
const safe = sanitizeStorageSegment(name);
|
|
1417
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1418
|
+
const where = parentId == null ? { kind: "folder", filename: safe, parentId: (0, import_typeorm3.IsNull)() } : { kind: "folder", filename: safe, parentId };
|
|
1419
|
+
const existing = await repo.findOne({ where });
|
|
1420
|
+
if (existing) return existing.id;
|
|
1421
|
+
const row = await repo.save(
|
|
1422
|
+
repo.create({
|
|
1423
|
+
kind: "folder",
|
|
1424
|
+
parentId,
|
|
1425
|
+
filename: safe,
|
|
1426
|
+
url: null,
|
|
1427
|
+
mimeType: "inode/directory",
|
|
1428
|
+
size: 0,
|
|
1429
|
+
alt: null,
|
|
1430
|
+
isPublic: false,
|
|
1431
|
+
deleted: false
|
|
1432
|
+
})
|
|
1433
|
+
);
|
|
1434
|
+
return row.id;
|
|
1435
|
+
}
|
|
1436
|
+
async function ensureFolderChain(dataSource, entityMap, rootParentId, pathSegments) {
|
|
1437
|
+
let pid = rootParentId;
|
|
1438
|
+
for (const seg of pathSegments) {
|
|
1439
|
+
if (!seg) continue;
|
|
1440
|
+
pid = await findOrCreateFolder(dataSource, entityMap, pid, seg);
|
|
1441
|
+
}
|
|
1442
|
+
return pid;
|
|
1443
|
+
}
|
|
1444
|
+
async function extractZipMediaIntoParentTree(opts) {
|
|
1445
|
+
const { dataSource, entityMap, zipMediaRow } = opts;
|
|
1446
|
+
const row = zipMediaRow;
|
|
1447
|
+
if (row.kind !== "file" || !row.url) throw new Error("Not a file");
|
|
1448
|
+
if (!isZipMedia(row.mimeType, row.filename)) throw new Error("Not a zip archive");
|
|
1449
|
+
const buffer = await readBufferFromPublicUrl(row.url);
|
|
1450
|
+
const { default: AdmZip } = await import("adm-zip");
|
|
1451
|
+
const zip = new AdmZip(buffer);
|
|
1452
|
+
const entries = zip.getEntries();
|
|
1453
|
+
if (entries.length > MAX_ENTRIES) throw new Error(`Too many zip entries (max ${MAX_ENTRIES})`);
|
|
1454
|
+
const rootParentId = row.parentId;
|
|
1455
|
+
const items = [];
|
|
1456
|
+
let totalUncompressed = 0;
|
|
1457
|
+
for (const e of entries) {
|
|
1458
|
+
const raw = e.entryName;
|
|
1459
|
+
const parts = sanitizeZipPath(raw);
|
|
1460
|
+
if (!parts || shouldSkipEntry(parts)) continue;
|
|
1461
|
+
const isDir = e.isDirectory || /\/$/.test(raw);
|
|
1462
|
+
let data = null;
|
|
1463
|
+
if (!isDir) {
|
|
1464
|
+
data = e.getData();
|
|
1465
|
+
totalUncompressed += data.length;
|
|
1466
|
+
if (totalUncompressed > MAX_TOTAL_UNCOMPRESSED) {
|
|
1467
|
+
throw new Error(`Uncompressed content exceeds limit (${MAX_TOTAL_UNCOMPRESSED} bytes)`);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
items.push({ parts, isDir, data });
|
|
1471
|
+
}
|
|
1472
|
+
items.sort((a, b) => {
|
|
1473
|
+
const da = a.parts.length;
|
|
1474
|
+
const db = b.parts.length;
|
|
1475
|
+
if (da !== db) return da - db;
|
|
1476
|
+
return a.parts.join("/").localeCompare(b.parts.join("/"));
|
|
1477
|
+
});
|
|
1478
|
+
let files = 0;
|
|
1479
|
+
let folderEntries = 0;
|
|
1480
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1481
|
+
for (const it of items) {
|
|
1482
|
+
if (it.isDir) {
|
|
1483
|
+
await ensureFolderChain(dataSource, entityMap, rootParentId, it.parts);
|
|
1484
|
+
folderEntries++;
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
const fileName = it.parts[it.parts.length - 1];
|
|
1488
|
+
const dirParts = it.parts.slice(0, -1);
|
|
1489
|
+
const parentFolderId = await ensureFolderChain(dataSource, entityMap, rootParentId, dirParts);
|
|
1490
|
+
const buf = it.data;
|
|
1491
|
+
const relBase = await relativePathFromMediaParentId(dataSource, entityMap, parentFolderId);
|
|
1492
|
+
const relativeUnderUploads = relBase ? `${relBase}/${fileName}` : fileName;
|
|
1493
|
+
const contentType = guessMimeType(fileName);
|
|
1494
|
+
let publicUrl;
|
|
1495
|
+
if (opts.storage) {
|
|
1496
|
+
publicUrl = await opts.storage.upload(buf, `uploads/${relativeUnderUploads}`, contentType);
|
|
1497
|
+
} else {
|
|
1498
|
+
const fs = await import("fs/promises");
|
|
1499
|
+
const pathMod = await import("path");
|
|
1500
|
+
const dir = pathMod.join(process.cwd(), opts.localUploadDir);
|
|
1501
|
+
const filePath = pathMod.join(dir, relativeUnderUploads);
|
|
1502
|
+
await fs.mkdir(pathMod.dirname(filePath), { recursive: true });
|
|
1503
|
+
await fs.writeFile(filePath, buf);
|
|
1504
|
+
publicUrl = `/${opts.localUploadDir.replace(/^\/+/, "").replace(/\\/g, "/")}/${relativeUnderUploads.replace(/\\/g, "/")}`;
|
|
1505
|
+
}
|
|
1506
|
+
await repo.save(
|
|
1507
|
+
repo.create({
|
|
1508
|
+
kind: "file",
|
|
1509
|
+
parentId: parentFolderId,
|
|
1510
|
+
filename: fileName,
|
|
1511
|
+
url: publicUrl,
|
|
1512
|
+
mimeType: contentType,
|
|
1513
|
+
size: buf.length,
|
|
1514
|
+
alt: null,
|
|
1515
|
+
isPublic: false,
|
|
1516
|
+
deleted: false
|
|
1517
|
+
})
|
|
1518
|
+
);
|
|
1519
|
+
files++;
|
|
1520
|
+
}
|
|
1521
|
+
return { files, folderEntries };
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1273
1524
|
// src/api/cms-handlers.ts
|
|
1274
1525
|
function createDashboardStatsHandler(config) {
|
|
1275
1526
|
const { dataSource, entityMap, json, requireAuth, requirePermission, requireEntityPermission } = config;
|
|
@@ -1287,26 +1538,209 @@ function createDashboardStatsHandler(config) {
|
|
|
1287
1538
|
try {
|
|
1288
1539
|
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
|
|
1289
1540
|
const repo = (name) => entityMap[name] ? dataSource.getRepository(entityMap[name]) : void 0;
|
|
1290
|
-
const [contactsCount, formsCount, formSubmissionsCount, usersCount, blogsCount, recentContacts, recentSubmissions] = await Promise.all([
|
|
1541
|
+
const [contactsCount, formsCount, formSubmissionsCount, usersCount, blogsCount, recentContacts, recentSubmissions, contactTypeRows] = await Promise.all([
|
|
1291
1542
|
repo("contacts")?.count() ?? 0,
|
|
1292
1543
|
repo("forms")?.count({ where: { deleted: false } }) ?? 0,
|
|
1293
1544
|
repo("form_submissions")?.count() ?? 0,
|
|
1294
1545
|
repo("users")?.count({ where: { deleted: false } }) ?? 0,
|
|
1295
1546
|
repo("blogs")?.count({ where: { deleted: false } }) ?? 0,
|
|
1296
|
-
repo("contacts")?.count({ where: { createdAt: (0,
|
|
1297
|
-
repo("form_submissions")?.count({ where: { createdAt: (0,
|
|
1547
|
+
repo("contacts")?.count({ where: { createdAt: (0, import_typeorm4.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0,
|
|
1548
|
+
repo("form_submissions")?.count({ where: { createdAt: (0, import_typeorm4.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0,
|
|
1549
|
+
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
1550
|
]);
|
|
1299
1551
|
return json({
|
|
1300
1552
|
contacts: { total: contactsCount, recent: recentContacts },
|
|
1301
1553
|
forms: { total: formsCount, submissions: formSubmissionsCount, recentSubmissions },
|
|
1302
1554
|
users: usersCount,
|
|
1303
|
-
blogs: blogsCount
|
|
1555
|
+
blogs: blogsCount,
|
|
1556
|
+
contactTypes: (contactTypeRows ?? []).map((row) => ({
|
|
1557
|
+
type: row.type || "unknown",
|
|
1558
|
+
count: Number(row.count || 0)
|
|
1559
|
+
}))
|
|
1304
1560
|
});
|
|
1305
1561
|
} catch (err) {
|
|
1306
1562
|
return json({ error: "Failed to fetch dashboard stats" }, { status: 500 });
|
|
1307
1563
|
}
|
|
1308
1564
|
};
|
|
1309
1565
|
}
|
|
1566
|
+
function toNum(v) {
|
|
1567
|
+
const n = typeof v === "number" ? v : Number(v ?? 0);
|
|
1568
|
+
return Number.isFinite(n) ? n : 0;
|
|
1569
|
+
}
|
|
1570
|
+
function toIsoDate(d) {
|
|
1571
|
+
return d.toISOString().slice(0, 10);
|
|
1572
|
+
}
|
|
1573
|
+
function createEcommerceAnalyticsHandler(config) {
|
|
1574
|
+
const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
|
|
1575
|
+
return async function GET(req) {
|
|
1576
|
+
const authErr = await requireAuth(req);
|
|
1577
|
+
if (authErr) return authErr;
|
|
1578
|
+
if (requireEntityPermission) {
|
|
1579
|
+
const pe = await requireEntityPermission(req, "analytics", "read");
|
|
1580
|
+
if (pe) return pe;
|
|
1581
|
+
}
|
|
1582
|
+
if (!entityMap.orders || !entityMap.order_items || !entityMap.payments || !entityMap.products) {
|
|
1583
|
+
return json({ error: "Store analytics unavailable" }, { status: 404 });
|
|
1584
|
+
}
|
|
1585
|
+
try {
|
|
1586
|
+
const url = new URL(req.url);
|
|
1587
|
+
const rawDays = parseInt(url.searchParams.get("days") || "30", 10);
|
|
1588
|
+
const days = Number.isFinite(rawDays) ? Math.min(365, Math.max(7, rawDays)) : 30;
|
|
1589
|
+
const end = /* @__PURE__ */ new Date();
|
|
1590
|
+
const start = new Date(end.getTime() - days * 24 * 60 * 60 * 1e3);
|
|
1591
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
1592
|
+
const paymentRepo = dataSource.getRepository(entityMap.payments);
|
|
1593
|
+
const itemRepo = dataSource.getRepository(entityMap.order_items);
|
|
1594
|
+
const productRepo = dataSource.getRepository(entityMap.products);
|
|
1595
|
+
const [salesOrders, returnOrders, replacementOrders, payments, products] = await Promise.all([
|
|
1596
|
+
orderRepo.find({
|
|
1597
|
+
where: { deleted: false, createdAt: (0, import_typeorm4.MoreThanOrEqual)(start), orderKind: "sale", status: (0, import_typeorm4.In)(["confirmed", "processing", "completed"]) },
|
|
1598
|
+
select: ["id", "contactId", "createdAt", "subtotal", "discount", "tax", "total", "status"]
|
|
1599
|
+
}),
|
|
1600
|
+
orderRepo.find({
|
|
1601
|
+
where: { deleted: false, createdAt: (0, import_typeorm4.MoreThanOrEqual)(start), orderKind: "return" },
|
|
1602
|
+
select: ["id", "createdAt", "total"]
|
|
1603
|
+
}),
|
|
1604
|
+
orderRepo.find({
|
|
1605
|
+
where: { deleted: false, createdAt: (0, import_typeorm4.MoreThanOrEqual)(start), orderKind: "replacement" },
|
|
1606
|
+
select: ["id", "createdAt", "total"]
|
|
1607
|
+
}),
|
|
1608
|
+
paymentRepo.find({
|
|
1609
|
+
where: { deleted: false, createdAt: (0, import_typeorm4.MoreThanOrEqual)(start) },
|
|
1610
|
+
select: ["id", "status", "method", "amount", "createdAt"]
|
|
1611
|
+
}),
|
|
1612
|
+
productRepo.find({
|
|
1613
|
+
where: { deleted: false },
|
|
1614
|
+
select: ["id", "name", "quantity"]
|
|
1615
|
+
})
|
|
1616
|
+
]);
|
|
1617
|
+
const saleOrderIds = salesOrders.map((o) => o.id);
|
|
1618
|
+
const orderItems = saleOrderIds.length ? await itemRepo.find({
|
|
1619
|
+
where: { orderId: (0, import_typeorm4.In)(saleOrderIds) },
|
|
1620
|
+
select: ["id", "orderId", "productId", "quantity", "total"]
|
|
1621
|
+
}) : [];
|
|
1622
|
+
const grossSales = salesOrders.reduce((sum, o) => sum + toNum(o.subtotal), 0);
|
|
1623
|
+
const discounts = salesOrders.reduce((sum, o) => sum + toNum(o.discount), 0);
|
|
1624
|
+
const taxes = salesOrders.reduce((sum, o) => sum + toNum(o.tax), 0);
|
|
1625
|
+
const returnsValue = returnOrders.reduce((sum, o) => sum + toNum(o.total), 0);
|
|
1626
|
+
const replacementsValue = replacementOrders.reduce((sum, o) => sum + toNum(o.total), 0);
|
|
1627
|
+
const netSales = grossSales - discounts - returnsValue;
|
|
1628
|
+
const ordersCount = salesOrders.length;
|
|
1629
|
+
const aov = ordersCount > 0 ? netSales / ordersCount : 0;
|
|
1630
|
+
const returnRate = ordersCount > 0 ? returnOrders.length / ordersCount * 100 : 0;
|
|
1631
|
+
const salesByDate = /* @__PURE__ */ new Map();
|
|
1632
|
+
const returnsByDate = /* @__PURE__ */ new Map();
|
|
1633
|
+
for (const o of salesOrders) {
|
|
1634
|
+
const key = toIsoDate(new Date(o.createdAt));
|
|
1635
|
+
const row = salesByDate.get(key) ?? { value: 0, orders: 0 };
|
|
1636
|
+
row.value += toNum(o.total);
|
|
1637
|
+
row.orders += 1;
|
|
1638
|
+
salesByDate.set(key, row);
|
|
1639
|
+
}
|
|
1640
|
+
for (const o of returnOrders) {
|
|
1641
|
+
const key = toIsoDate(new Date(o.createdAt));
|
|
1642
|
+
const row = returnsByDate.get(key) ?? { value: 0, count: 0 };
|
|
1643
|
+
row.value += toNum(o.total);
|
|
1644
|
+
row.count += 1;
|
|
1645
|
+
returnsByDate.set(key, row);
|
|
1646
|
+
}
|
|
1647
|
+
const salesOverTime = [];
|
|
1648
|
+
const returnsTrend = [];
|
|
1649
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
1650
|
+
const d = new Date(end.getTime() - i * 24 * 60 * 60 * 1e3);
|
|
1651
|
+
const key = toIsoDate(d);
|
|
1652
|
+
const sales = salesByDate.get(key) ?? { value: 0, orders: 0 };
|
|
1653
|
+
const returns = returnsByDate.get(key) ?? { value: 0, count: 0 };
|
|
1654
|
+
salesOverTime.push({ date: key, value: Number(sales.value.toFixed(2)), orders: sales.orders });
|
|
1655
|
+
returnsTrend.push({ date: key, value: Number(returns.value.toFixed(2)), count: returns.count });
|
|
1656
|
+
}
|
|
1657
|
+
const productNameMap = /* @__PURE__ */ new Map();
|
|
1658
|
+
for (const p of products) productNameMap.set(Number(p.id), (p.name || `Product #${p.id}`).trim());
|
|
1659
|
+
const productAgg = /* @__PURE__ */ new Map();
|
|
1660
|
+
for (const item of orderItems) {
|
|
1661
|
+
const productId = Number(item.productId);
|
|
1662
|
+
const productName = productNameMap.get(productId) || `Product #${productId}`;
|
|
1663
|
+
const row = productAgg.get(productId) ?? { name: productName, units: 0, sales: 0 };
|
|
1664
|
+
row.units += toNum(item.quantity);
|
|
1665
|
+
row.sales += toNum(item.total);
|
|
1666
|
+
productAgg.set(productId, row);
|
|
1667
|
+
}
|
|
1668
|
+
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)) }));
|
|
1669
|
+
const allSaleOrderContactIds = Array.from(new Set(salesOrders.map((o) => Number(o.contactId)).filter((n) => Number.isInteger(n) && n > 0)));
|
|
1670
|
+
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() : [];
|
|
1671
|
+
const countMap = /* @__PURE__ */ new Map();
|
|
1672
|
+
for (const c of allTimeCounts) countMap.set(Number(c.contactId), Number(c.total));
|
|
1673
|
+
const purchasingCustomers = allSaleOrderContactIds.length;
|
|
1674
|
+
const returningCustomers = allSaleOrderContactIds.filter((id) => (countMap.get(id) ?? 0) > 1).length;
|
|
1675
|
+
const newCustomers = Math.max(0, purchasingCustomers - returningCustomers);
|
|
1676
|
+
const returningCustomerRate = purchasingCustomers > 0 ? returningCustomers / purchasingCustomers * 100 : 0;
|
|
1677
|
+
const totalPayments = payments.length;
|
|
1678
|
+
const completedPayments = payments.filter((p) => p.status === "completed").length;
|
|
1679
|
+
const failedPayments = payments.filter((p) => p.status === "failed").length;
|
|
1680
|
+
const paymentSuccessRate = totalPayments > 0 ? completedPayments / totalPayments * 100 : 0;
|
|
1681
|
+
const paymentMethodMap = /* @__PURE__ */ new Map();
|
|
1682
|
+
for (const p of payments) {
|
|
1683
|
+
const method = (p.method || "unknown").toLowerCase();
|
|
1684
|
+
const row = paymentMethodMap.get(method) ?? { method, count: 0, amount: 0 };
|
|
1685
|
+
row.count += 1;
|
|
1686
|
+
row.amount += toNum(p.amount);
|
|
1687
|
+
paymentMethodMap.set(method, row);
|
|
1688
|
+
}
|
|
1689
|
+
const paymentMethods = Array.from(paymentMethodMap.values()).sort((a, b) => b.count - a.count).map((p) => ({ ...p, amount: Number(p.amount.toFixed(2)) }));
|
|
1690
|
+
const totalInventory = products.reduce((sum, p) => sum + toNum(p.quantity), 0);
|
|
1691
|
+
const outOfStockCount = products.filter((p) => toNum(p.quantity) <= 0).length;
|
|
1692
|
+
const lowStockCount = products.filter((p) => toNum(p.quantity) > 0 && toNum(p.quantity) <= 5).length;
|
|
1693
|
+
const inventoryRisk = {
|
|
1694
|
+
outOfStockCount,
|
|
1695
|
+
lowStockCount,
|
|
1696
|
+
totalInventory
|
|
1697
|
+
};
|
|
1698
|
+
return json({
|
|
1699
|
+
rangeDays: days,
|
|
1700
|
+
kpis: {
|
|
1701
|
+
netSales: Number(netSales.toFixed(2)),
|
|
1702
|
+
grossSales: Number(grossSales.toFixed(2)),
|
|
1703
|
+
ordersPlaced: ordersCount,
|
|
1704
|
+
averageOrderValue: Number(aov.toFixed(2)),
|
|
1705
|
+
returningCustomerRate: Number(returningCustomerRate.toFixed(2)),
|
|
1706
|
+
returnRate: Number(returnRate.toFixed(2)),
|
|
1707
|
+
returnValue: Number(returnsValue.toFixed(2)),
|
|
1708
|
+
discounts: Number(discounts.toFixed(2)),
|
|
1709
|
+
taxes: Number(taxes.toFixed(2)),
|
|
1710
|
+
paymentSuccessRate: Number(paymentSuccessRate.toFixed(2))
|
|
1711
|
+
},
|
|
1712
|
+
salesOverTime,
|
|
1713
|
+
topProducts,
|
|
1714
|
+
customerMix: {
|
|
1715
|
+
newCustomers,
|
|
1716
|
+
returningCustomers,
|
|
1717
|
+
repeatPurchaseRate: Number(returningCustomerRate.toFixed(2))
|
|
1718
|
+
},
|
|
1719
|
+
returnsTrend,
|
|
1720
|
+
paymentPerformance: {
|
|
1721
|
+
successCount: completedPayments,
|
|
1722
|
+
failedCount: failedPayments,
|
|
1723
|
+
successRate: Number(paymentSuccessRate.toFixed(2)),
|
|
1724
|
+
methods: paymentMethods
|
|
1725
|
+
},
|
|
1726
|
+
conversionProxy: {
|
|
1727
|
+
sessions: 0,
|
|
1728
|
+
checkoutStarted: 0,
|
|
1729
|
+
ordersPlaced: ordersCount
|
|
1730
|
+
},
|
|
1731
|
+
salesBreakdown: {
|
|
1732
|
+
sales: { count: ordersCount, value: Number(grossSales.toFixed(2)) },
|
|
1733
|
+
returns: { count: returnOrders.length, value: Number(returnsValue.toFixed(2)) },
|
|
1734
|
+
replacements: { count: replacementOrders.length, value: Number(replacementsValue.toFixed(2)) }
|
|
1735
|
+
},
|
|
1736
|
+
geoPerformance: [],
|
|
1737
|
+
inventoryRisk
|
|
1738
|
+
});
|
|
1739
|
+
} catch {
|
|
1740
|
+
return json({ error: "Failed to fetch ecommerce analytics" }, { status: 500 });
|
|
1741
|
+
}
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1310
1744
|
function createAnalyticsHandlers(config) {
|
|
1311
1745
|
const { json, getAnalyticsData, getPropertyId, getPermissions } = config;
|
|
1312
1746
|
return {
|
|
@@ -1338,8 +1772,27 @@ function createAnalyticsHandlers(config) {
|
|
|
1338
1772
|
};
|
|
1339
1773
|
}
|
|
1340
1774
|
function createUploadHandler(config) {
|
|
1341
|
-
const {
|
|
1342
|
-
|
|
1775
|
+
const {
|
|
1776
|
+
json,
|
|
1777
|
+
requireAuth,
|
|
1778
|
+
requireEntityPermission,
|
|
1779
|
+
storage,
|
|
1780
|
+
localUploadDir = "public/uploads",
|
|
1781
|
+
allowedTypes,
|
|
1782
|
+
maxSizeBytes = 10 * 1024 * 1024,
|
|
1783
|
+
dataSource,
|
|
1784
|
+
entityMap
|
|
1785
|
+
} = config;
|
|
1786
|
+
const allowed = allowedTypes ?? [
|
|
1787
|
+
"image/jpeg",
|
|
1788
|
+
"image/png",
|
|
1789
|
+
"image/gif",
|
|
1790
|
+
"image/webp",
|
|
1791
|
+
"application/pdf",
|
|
1792
|
+
"text/plain",
|
|
1793
|
+
"application/zip",
|
|
1794
|
+
"application/x-zip-compressed"
|
|
1795
|
+
];
|
|
1343
1796
|
return async function POST(req) {
|
|
1344
1797
|
const authErr = await requireAuth(req);
|
|
1345
1798
|
if (authErr) return authErr;
|
|
@@ -1352,28 +1805,92 @@ function createUploadHandler(config) {
|
|
|
1352
1805
|
const file = formData.get("file");
|
|
1353
1806
|
if (!file) return json({ error: "No file uploaded" }, { status: 400 });
|
|
1354
1807
|
if (!allowed.includes(file.type)) return json({ error: "File type not allowed" }, { status: 400 });
|
|
1355
|
-
|
|
1808
|
+
const defaultMax = 10 * 1024 * 1024;
|
|
1809
|
+
const maxZipBytes = 80 * 1024 * 1024;
|
|
1810
|
+
const baseMax = maxSizeBytes ?? defaultMax;
|
|
1811
|
+
const effectiveMax = file.type === "application/zip" || file.type === "application/x-zip-compressed" ? Math.max(baseMax, maxZipBytes) : baseMax;
|
|
1812
|
+
if (file.size > effectiveMax) return json({ error: "File size exceeds limit" }, { status: 400 });
|
|
1813
|
+
const parentRaw = formData.get("parentId");
|
|
1814
|
+
let parentId = null;
|
|
1815
|
+
if (parentRaw != null && String(parentRaw).trim() !== "") {
|
|
1816
|
+
const n = Number(parentRaw);
|
|
1817
|
+
if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
|
|
1818
|
+
parentId = n;
|
|
1819
|
+
}
|
|
1820
|
+
let folder = "";
|
|
1821
|
+
if (parentId != null) {
|
|
1822
|
+
if (!dataSource || !entityMap?.media) {
|
|
1823
|
+
return json({ error: "Upload handler needs dataSource and entityMap for folder uploads" }, { status: 400 });
|
|
1824
|
+
}
|
|
1825
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1826
|
+
const p = await repo.findOne({ where: { id: parentId } });
|
|
1827
|
+
if (!p || p.kind !== "folder") {
|
|
1828
|
+
return json({ error: "parent must be a folder" }, { status: 400 });
|
|
1829
|
+
}
|
|
1830
|
+
folder = await relativePathFromMediaParentId(dataSource, entityMap, parentId);
|
|
1831
|
+
} else {
|
|
1832
|
+
const folderRawLegacy = formData.get("folder") ?? formData.get("folderPath");
|
|
1833
|
+
if (folderRawLegacy && typeof folderRawLegacy === "string" && folderRawLegacy.trim()) {
|
|
1834
|
+
folder = sanitizeMediaFolderPath(folderRawLegacy);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1356
1837
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
1357
1838
|
const fileName = `${Date.now()}-${file.name}`;
|
|
1358
1839
|
const contentType = file.type || "application/octet-stream";
|
|
1840
|
+
const relativeUnderUploads = folder ? `${folder}/${fileName}` : fileName;
|
|
1359
1841
|
const raw = typeof storage === "function" ? storage() : storage;
|
|
1360
1842
|
const storageService = raw instanceof Promise ? await raw : raw;
|
|
1361
1843
|
if (storageService) {
|
|
1362
|
-
const fileUrl = await storageService.upload(buffer, `uploads/${
|
|
1363
|
-
return json({ filePath: fileUrl });
|
|
1844
|
+
const fileUrl = await storageService.upload(buffer, `uploads/${relativeUnderUploads}`, contentType);
|
|
1845
|
+
return json({ filePath: fileUrl, parentId });
|
|
1364
1846
|
}
|
|
1365
1847
|
const fs = await import("fs/promises");
|
|
1366
1848
|
const path = await import("path");
|
|
1367
1849
|
const dir = path.join(process.cwd(), localUploadDir);
|
|
1368
|
-
|
|
1369
|
-
|
|
1850
|
+
const filePath = path.join(dir, relativeUnderUploads);
|
|
1851
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
1370
1852
|
await fs.writeFile(filePath, buffer);
|
|
1371
|
-
|
|
1853
|
+
const urlRel = `${localUploadDir.replace(/^\/+/, "").replace(/\\/g, "/")}/${relativeUnderUploads.replace(/\\/g, "/")}`;
|
|
1854
|
+
return json({ filePath: `/${urlRel}`, parentId });
|
|
1372
1855
|
} catch (err) {
|
|
1373
1856
|
return json({ error: "File upload failed" }, { status: 500 });
|
|
1374
1857
|
}
|
|
1375
1858
|
};
|
|
1376
1859
|
}
|
|
1860
|
+
function createMediaZipExtractHandler(config) {
|
|
1861
|
+
const { json, requireAuth, requireEntityPermission, storage, localUploadDir = "public/uploads", dataSource, entityMap } = config;
|
|
1862
|
+
return async function POST(_req, zipMediaId) {
|
|
1863
|
+
const authErr = await requireAuth(_req);
|
|
1864
|
+
if (authErr) return authErr;
|
|
1865
|
+
if (requireEntityPermission) {
|
|
1866
|
+
const pe = await requireEntityPermission(_req, "media", "create");
|
|
1867
|
+
if (pe) return pe;
|
|
1868
|
+
}
|
|
1869
|
+
if (!dataSource || !entityMap?.media) {
|
|
1870
|
+
return json({ error: "Media extract requires dataSource and entityMap" }, { status: 500 });
|
|
1871
|
+
}
|
|
1872
|
+
const id = Number(zipMediaId);
|
|
1873
|
+
if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
|
|
1874
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1875
|
+
const row = await repo.findOne({ where: { id } });
|
|
1876
|
+
if (!row) return json({ error: "Not found" }, { status: 404 });
|
|
1877
|
+
try {
|
|
1878
|
+
const raw = typeof storage === "function" ? storage() : storage;
|
|
1879
|
+
const storageService = raw instanceof Promise ? await raw : raw;
|
|
1880
|
+
const result = await extractZipMediaIntoParentTree({
|
|
1881
|
+
dataSource,
|
|
1882
|
+
entityMap,
|
|
1883
|
+
zipMediaRow: row,
|
|
1884
|
+
storage: storageService,
|
|
1885
|
+
localUploadDir
|
|
1886
|
+
});
|
|
1887
|
+
return json({ ok: true, ...result });
|
|
1888
|
+
} catch (e) {
|
|
1889
|
+
const msg = e instanceof Error ? e.message : "Extract failed";
|
|
1890
|
+
return json({ error: msg }, { status: 400 });
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1377
1894
|
function createBlogBySlugHandler(config) {
|
|
1378
1895
|
const { dataSource, entityMap, json } = config;
|
|
1379
1896
|
return async function GET(_req, slug) {
|
|
@@ -1786,7 +2303,7 @@ function createUsersApiHandlers(config) {
|
|
|
1786
2303
|
const sortField = url.searchParams.get("sortField") || "createdAt";
|
|
1787
2304
|
const sortOrder = url.searchParams.get("sortOrder") === "desc" ? "DESC" : "ASC";
|
|
1788
2305
|
const search = url.searchParams.get("search");
|
|
1789
|
-
const where = search ? [{ name: (0,
|
|
2306
|
+
const where = search ? [{ name: (0, import_typeorm4.ILike)(`%${search}%`) }, { email: (0, import_typeorm4.ILike)(`%${search}%`) }] : {};
|
|
1790
2307
|
const [data, total] = await userRepo().findAndCount({
|
|
1791
2308
|
skip,
|
|
1792
2309
|
take: limit,
|
|
@@ -2146,7 +2663,7 @@ function createChatHandlers(config) {
|
|
|
2146
2663
|
if (contextParts.length === 0) {
|
|
2147
2664
|
const terms = getQueryTerms(message);
|
|
2148
2665
|
if (terms.length > 0) {
|
|
2149
|
-
const conditions = terms.map((t) => ({ content: (0,
|
|
2666
|
+
const conditions = terms.map((t) => ({ content: (0, import_typeorm4.ILike)(`%${t}%`) }));
|
|
2150
2667
|
const chunks = await chunkRepo().find({
|
|
2151
2668
|
where: conditions,
|
|
2152
2669
|
take: KB_CHUNK_LIMIT,
|
|
@@ -2515,6 +3032,7 @@ function createCmsApiHandler(config) {
|
|
|
2515
3032
|
getCms,
|
|
2516
3033
|
userAuth: userAuthConfig,
|
|
2517
3034
|
dashboard,
|
|
3035
|
+
ecommerceAnalytics,
|
|
2518
3036
|
analytics: analyticsConfig,
|
|
2519
3037
|
upload,
|
|
2520
3038
|
blogBySlug,
|
|
@@ -2581,8 +3099,28 @@ function createCmsApiHandler(config) {
|
|
|
2581
3099
|
});
|
|
2582
3100
|
const userAuthRouter = userAuth ? createUserAuthApiRouter(userAuth) : null;
|
|
2583
3101
|
const dashboardGet = dashboard ? createDashboardStatsHandler(mergePerm(dashboard) ?? dashboard) : null;
|
|
3102
|
+
const ecommerceAnalyticsResolved = mergePerm(
|
|
3103
|
+
ecommerceAnalytics ?? {
|
|
3104
|
+
dataSource,
|
|
3105
|
+
entityMap,
|
|
3106
|
+
json: config.json,
|
|
3107
|
+
requireAuth: config.requireAuth
|
|
3108
|
+
}
|
|
3109
|
+
) ?? {
|
|
3110
|
+
dataSource,
|
|
3111
|
+
entityMap,
|
|
3112
|
+
json: config.json,
|
|
3113
|
+
requireAuth: config.requireAuth
|
|
3114
|
+
};
|
|
3115
|
+
const ecommerceAnalyticsGet = createEcommerceAnalyticsHandler(ecommerceAnalyticsResolved);
|
|
2584
3116
|
const analyticsHandlers = analytics ? createAnalyticsHandlers(analytics) : null;
|
|
2585
|
-
const
|
|
3117
|
+
const uploadMerged = upload ? {
|
|
3118
|
+
...mergePerm(upload) ?? upload,
|
|
3119
|
+
dataSource: upload.dataSource ?? dataSource,
|
|
3120
|
+
entityMap: upload.entityMap ?? entityMap
|
|
3121
|
+
} : null;
|
|
3122
|
+
const uploadPost = uploadMerged ? createUploadHandler(uploadMerged) : null;
|
|
3123
|
+
const zipExtractPost = uploadMerged ? createMediaZipExtractHandler(uploadMerged) : null;
|
|
2586
3124
|
const blogBySlugGet = blogBySlug ? createBlogBySlugHandler(blogBySlug) : null;
|
|
2587
3125
|
const formBySlugGet = formBySlug ? createFormBySlugHandler(formBySlug) : null;
|
|
2588
3126
|
const formSaveHandlers = formSaveConfig ? createFormSaveHandlers(mergePerm(formSaveConfig) ?? formSaveConfig) : null;
|
|
@@ -2631,6 +3169,11 @@ function createCmsApiHandler(config) {
|
|
|
2631
3169
|
if (path[0] === "dashboard" && path[1] === "stats" && path.length === 2 && method === "GET" && dashboardGet) {
|
|
2632
3170
|
return dashboardGet(req);
|
|
2633
3171
|
}
|
|
3172
|
+
if (path[0] === "dashboard" && path[1] === "ecommerce" && path.length === 2 && method === "GET" && ecommerceAnalyticsGet) {
|
|
3173
|
+
const g = await analyticsGate();
|
|
3174
|
+
if (g) return g;
|
|
3175
|
+
return ecommerceAnalyticsGet(req);
|
|
3176
|
+
}
|
|
2634
3177
|
if (path[0] === "analytics" && analyticsHandlers) {
|
|
2635
3178
|
if (path.length === 1 && method === "GET") {
|
|
2636
3179
|
const g = await analyticsGate();
|
|
@@ -2649,6 +3192,9 @@ function createCmsApiHandler(config) {
|
|
|
2649
3192
|
}
|
|
2650
3193
|
}
|
|
2651
3194
|
if (path[0] === "upload" && path.length === 1 && method === "POST" && uploadPost) return uploadPost(req);
|
|
3195
|
+
if (path[0] === "media" && path[1] === "extract" && path.length === 3 && method === "POST" && zipExtractPost) {
|
|
3196
|
+
return zipExtractPost(req, path[2]);
|
|
3197
|
+
}
|
|
2652
3198
|
if (path[0] === "blogs" && path[1] === "slug" && path.length === 3 && method === "GET" && blogBySlugGet) {
|
|
2653
3199
|
return blogBySlugGet(req, path[2]);
|
|
2654
3200
|
}
|
|
@@ -2786,7 +3332,7 @@ function createCmsApiHandler(config) {
|
|
|
2786
3332
|
}
|
|
2787
3333
|
|
|
2788
3334
|
// src/api/storefront-handlers.ts
|
|
2789
|
-
var
|
|
3335
|
+
var import_typeorm6 = require("typeorm");
|
|
2790
3336
|
|
|
2791
3337
|
// src/lib/is-valid-signup-email.ts
|
|
2792
3338
|
var MAX_EMAIL = 254;
|
|
@@ -3046,7 +3592,7 @@ async function queueSms(cms, payload) {
|
|
|
3046
3592
|
|
|
3047
3593
|
// src/lib/otp-challenge.ts
|
|
3048
3594
|
var import_crypto = require("crypto");
|
|
3049
|
-
var
|
|
3595
|
+
var import_typeorm5 = require("typeorm");
|
|
3050
3596
|
var OTP_TTL_MS = 10 * 60 * 1e3;
|
|
3051
3597
|
var MAX_SENDS_PER_HOUR = 5;
|
|
3052
3598
|
var MAX_VERIFY_ATTEMPTS = 8;
|
|
@@ -3080,7 +3626,7 @@ function normalizePhoneE164(raw, defaultCountryCode) {
|
|
|
3080
3626
|
async function countRecentOtpSends(dataSource, entityMap, purpose, identifier, since) {
|
|
3081
3627
|
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
3082
3628
|
return repo.count({
|
|
3083
|
-
where: { purpose, identifier, createdAt: (0,
|
|
3629
|
+
where: { purpose, identifier, createdAt: (0, import_typeorm5.MoreThan)(since) }
|
|
3084
3630
|
});
|
|
3085
3631
|
}
|
|
3086
3632
|
async function createOtpChallenge(dataSource, entityMap, input) {
|
|
@@ -3094,7 +3640,7 @@ async function createOtpChallenge(dataSource, entityMap, input) {
|
|
|
3094
3640
|
await repo.delete({
|
|
3095
3641
|
purpose,
|
|
3096
3642
|
identifier,
|
|
3097
|
-
consumedAt: (0,
|
|
3643
|
+
consumedAt: (0, import_typeorm5.IsNull)()
|
|
3098
3644
|
});
|
|
3099
3645
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
3100
3646
|
const codeHash = hashOtpCode(code, purpose, identifier, pepper);
|
|
@@ -3115,7 +3661,7 @@ async function verifyAndConsumeOtpChallenge(dataSource, entityMap, input) {
|
|
|
3115
3661
|
const { purpose, identifier, code, pepper } = input;
|
|
3116
3662
|
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
3117
3663
|
const row = await repo.findOne({
|
|
3118
|
-
where: { purpose, identifier, consumedAt: (0,
|
|
3664
|
+
where: { purpose, identifier, consumedAt: (0, import_typeorm5.IsNull)() },
|
|
3119
3665
|
order: { id: "DESC" }
|
|
3120
3666
|
});
|
|
3121
3667
|
if (!row) {
|
|
@@ -3349,7 +3895,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3349
3895
|
const u = await userRepo().findOne({ where: { id: userId } });
|
|
3350
3896
|
if (!u) return null;
|
|
3351
3897
|
const unclaimed = await contactRepo().findOne({
|
|
3352
|
-
where: { email: u.email, userId: (0,
|
|
3898
|
+
where: { email: u.email, userId: (0, import_typeorm6.IsNull)(), deleted: false }
|
|
3353
3899
|
});
|
|
3354
3900
|
if (unclaimed) {
|
|
3355
3901
|
await contactRepo().update(unclaimed.id, { userId });
|
|
@@ -4390,7 +4936,7 @@ function createStorefrontApiHandler(config) {
|
|
|
4390
4936
|
const previewByOrder = {};
|
|
4391
4937
|
if (orderIds.length) {
|
|
4392
4938
|
const oItems = await orderItemRepo().find({
|
|
4393
|
-
where: { orderId: (0,
|
|
4939
|
+
where: { orderId: (0, import_typeorm6.In)(orderIds) },
|
|
4394
4940
|
relations: ["product"],
|
|
4395
4941
|
order: { id: "ASC" }
|
|
4396
4942
|
});
|
|
@@ -4523,9 +5069,11 @@ function createStorefrontApiHandler(config) {
|
|
|
4523
5069
|
createCrudByIdHandler,
|
|
4524
5070
|
createCrudHandler,
|
|
4525
5071
|
createDashboardStatsHandler,
|
|
5072
|
+
createEcommerceAnalyticsHandler,
|
|
4526
5073
|
createForgotPasswordHandler,
|
|
4527
5074
|
createFormBySlugHandler,
|
|
4528
5075
|
createInviteAcceptHandler,
|
|
5076
|
+
createMediaZipExtractHandler,
|
|
4529
5077
|
createSetPasswordHandler,
|
|
4530
5078
|
createSettingsApiHandlers,
|
|
4531
5079
|
createStorefrontApiHandler,
|