@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.js
CHANGED
|
@@ -397,7 +397,7 @@ var init_paid_order_erp = __esm({
|
|
|
397
397
|
});
|
|
398
398
|
|
|
399
399
|
// src/api/crud.ts
|
|
400
|
-
import {
|
|
400
|
+
import { Brackets, ILike, MoreThan } from "typeorm";
|
|
401
401
|
|
|
402
402
|
// src/plugins/erp/erp-contact-sync.ts
|
|
403
403
|
init_erp_queue();
|
|
@@ -710,14 +710,38 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
710
710
|
const repo = dataSource.getRepository(entity);
|
|
711
711
|
const typeFilter = searchParams.get("type");
|
|
712
712
|
const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
|
|
713
|
+
if (resource === "media") {
|
|
714
|
+
const qb = repo.createQueryBuilder("m");
|
|
715
|
+
const parentIdParam = searchParams.get("parentId");
|
|
716
|
+
if (parentIdParam != null && parentIdParam !== "") {
|
|
717
|
+
const n = Number(parentIdParam);
|
|
718
|
+
if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
|
|
719
|
+
qb.where("m.parentId = :pid", { pid: n });
|
|
720
|
+
} else {
|
|
721
|
+
qb.where("m.parentId IS NULL");
|
|
722
|
+
}
|
|
723
|
+
if (search && typeof search === "string" && search.trim()) {
|
|
724
|
+
qb.andWhere("m.filename ILIKE :search", { search: `%${search.trim()}%` });
|
|
725
|
+
}
|
|
726
|
+
if (typeFilter) {
|
|
727
|
+
qb.andWhere(
|
|
728
|
+
new Brackets((sq) => {
|
|
729
|
+
sq.where("m.kind = :folderKind", { folderKind: "folder" }).orWhere("m.mimeType LIKE :mtp", {
|
|
730
|
+
mtp: `${typeFilter}/%`
|
|
731
|
+
});
|
|
732
|
+
})
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
const allowedSort = ["filename", "createdAt", "id"];
|
|
736
|
+
const sf = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "filename";
|
|
737
|
+
const so = sortOrder === "DESC" ? "DESC" : "ASC";
|
|
738
|
+
qb.orderBy("CASE WHEN m.kind = :fk THEN 0 ELSE 1 END", "ASC").addOrderBy(`m.${sf}`, so).setParameter("fk", "folder").skip(skip).take(limit);
|
|
739
|
+
const [data2, total2] = await qb.getManyAndCount();
|
|
740
|
+
return json({ total: total2, page, limit, totalPages: Math.ceil(total2 / limit), data: data2 });
|
|
741
|
+
}
|
|
713
742
|
const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
|
|
714
743
|
let where = {};
|
|
715
|
-
if (
|
|
716
|
-
const mediaWhere = {};
|
|
717
|
-
if (search) mediaWhere.filename = ILike(`%${search}%`);
|
|
718
|
-
if (typeFilter) mediaWhere.mimeType = Like(`${typeFilter}/%`);
|
|
719
|
-
where = Object.keys(mediaWhere).length > 0 ? mediaWhere : {};
|
|
720
|
-
} else if (search) {
|
|
744
|
+
if (search) {
|
|
721
745
|
where = buildSearchWhereClause(repo, search);
|
|
722
746
|
}
|
|
723
747
|
const intFilterKeys = ["productId", "attributeId", "taxId"];
|
|
@@ -757,6 +781,38 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
757
781
|
if (!body || typeof body !== "object" || Object.keys(body).length === 0) {
|
|
758
782
|
return json({ error: "Invalid request payload" }, { status: 400 });
|
|
759
783
|
}
|
|
784
|
+
if (resource === "media") {
|
|
785
|
+
const b = body;
|
|
786
|
+
const kind = b.kind === "folder" ? "folder" : "file";
|
|
787
|
+
b.kind = kind;
|
|
788
|
+
const fn = String(b.filename ?? "").trim().slice(0, 255);
|
|
789
|
+
if (!fn) return json({ error: "filename required" }, { status: 400 });
|
|
790
|
+
b.filename = fn;
|
|
791
|
+
let pid = null;
|
|
792
|
+
if (b.parentId != null && b.parentId !== "") {
|
|
793
|
+
const n = Number(b.parentId);
|
|
794
|
+
if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
|
|
795
|
+
pid = n;
|
|
796
|
+
}
|
|
797
|
+
b.parentId = pid;
|
|
798
|
+
const mediaRepo = dataSource.getRepository(entityMap.media);
|
|
799
|
+
if (pid != null) {
|
|
800
|
+
const parent = await mediaRepo.findOne({ where: { id: pid } });
|
|
801
|
+
if (!parent || parent.kind !== "folder") {
|
|
802
|
+
return json({ error: "parent must be a folder" }, { status: 400 });
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (kind === "folder") {
|
|
806
|
+
b.url = null;
|
|
807
|
+
b.mimeType = "inode/directory";
|
|
808
|
+
b.size = 0;
|
|
809
|
+
} else {
|
|
810
|
+
if (!b.url || typeof b.url !== "string") return json({ error: "url required for files" }, { status: 400 });
|
|
811
|
+
if (!b.mimeType || typeof b.mimeType !== "string") {
|
|
812
|
+
b.mimeType = "application/octet-stream";
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
760
816
|
const repo = dataSource.getRepository(entity);
|
|
761
817
|
sanitizeBodyForEntity(repo, body);
|
|
762
818
|
const created = await repo.save(repo.create(body));
|
|
@@ -1029,6 +1085,11 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
1029
1085
|
return updated2 ? json(updated2) : json({ message: "Not found" }, { status: 404 });
|
|
1030
1086
|
}
|
|
1031
1087
|
const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
|
|
1088
|
+
if (resource === "media") {
|
|
1089
|
+
const u = updatePayload;
|
|
1090
|
+
delete u.parentId;
|
|
1091
|
+
delete u.kind;
|
|
1092
|
+
}
|
|
1032
1093
|
if (Object.keys(updatePayload).length > 0) {
|
|
1033
1094
|
sanitizeBodyForEntity(repo, updatePayload);
|
|
1034
1095
|
await repo.update(numericId, updatePayload);
|
|
@@ -1205,7 +1266,7 @@ function createUserAuthApiRouter(config) {
|
|
|
1205
1266
|
// src/api/cms-handlers.ts
|
|
1206
1267
|
init_email_queue();
|
|
1207
1268
|
init_erp_queue();
|
|
1208
|
-
import { MoreThanOrEqual, ILike as ILike2 } from "typeorm";
|
|
1269
|
+
import { MoreThanOrEqual, ILike as ILike2, In } from "typeorm";
|
|
1209
1270
|
|
|
1210
1271
|
// src/plugins/captcha/assert.ts
|
|
1211
1272
|
async function assertCaptchaOk(getCms, body, req, json) {
|
|
@@ -1223,6 +1284,194 @@ async function assertCaptchaOk(getCms, body, req, json) {
|
|
|
1223
1284
|
return json({ error: result.message }, { status: result.status });
|
|
1224
1285
|
}
|
|
1225
1286
|
|
|
1287
|
+
// src/lib/media-folder-path.ts
|
|
1288
|
+
function sanitizeMediaFolderPath(input) {
|
|
1289
|
+
if (input == null) return "";
|
|
1290
|
+
if (typeof input !== "string") return "";
|
|
1291
|
+
const segments = input.replace(/\\/g, "/").split("/").map((s) => s.trim()).filter(Boolean).filter((s) => s !== ".." && s !== ".");
|
|
1292
|
+
const joined = segments.join("/");
|
|
1293
|
+
return joined.length > 512 ? joined.slice(0, 512) : joined;
|
|
1294
|
+
}
|
|
1295
|
+
function sanitizeStorageSegment(name) {
|
|
1296
|
+
const s = name.replace(/[/\\]/g, "-").trim().slice(0, 255);
|
|
1297
|
+
return s || "item";
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// src/lib/media-parent-path.ts
|
|
1301
|
+
async function relativePathFromMediaParentId(dataSource, entityMap, parentId) {
|
|
1302
|
+
if (parentId == null) return "";
|
|
1303
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1304
|
+
const segments = [];
|
|
1305
|
+
let id = parentId;
|
|
1306
|
+
for (let d = 0; d < 64 && id != null; d++) {
|
|
1307
|
+
const row = await repo.findOne({ where: { id } });
|
|
1308
|
+
if (!row) break;
|
|
1309
|
+
const m = row;
|
|
1310
|
+
if (m.kind !== "folder") break;
|
|
1311
|
+
segments.unshift(sanitizeStorageSegment(m.filename));
|
|
1312
|
+
id = m.parentId ?? null;
|
|
1313
|
+
}
|
|
1314
|
+
return segments.join("/");
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// src/lib/media-zip-extract.ts
|
|
1318
|
+
import { IsNull as IsNull2 } from "typeorm";
|
|
1319
|
+
var ZIP_MIME_TYPES = /* @__PURE__ */ new Set(["application/zip", "application/x-zip-compressed"]);
|
|
1320
|
+
var MAX_ENTRIES = 2e3;
|
|
1321
|
+
var MAX_TOTAL_UNCOMPRESSED = 80 * 1024 * 1024;
|
|
1322
|
+
function isZipMedia(mime, filename) {
|
|
1323
|
+
if (mime && ZIP_MIME_TYPES.has(mime)) return true;
|
|
1324
|
+
return filename.toLowerCase().endsWith(".zip");
|
|
1325
|
+
}
|
|
1326
|
+
async function readBufferFromPublicUrl(url) {
|
|
1327
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
1328
|
+
const r = await fetch(url);
|
|
1329
|
+
if (!r.ok) throw new Error("Failed to download file");
|
|
1330
|
+
return Buffer.from(await r.arrayBuffer());
|
|
1331
|
+
}
|
|
1332
|
+
if (url.startsWith("/")) {
|
|
1333
|
+
const { readFile } = await import("fs/promises");
|
|
1334
|
+
const { join } = await import("path");
|
|
1335
|
+
const rel = url.replace(/^\/+/, "");
|
|
1336
|
+
return readFile(join(process.cwd(), "public", rel));
|
|
1337
|
+
}
|
|
1338
|
+
throw new Error("Unsupported media URL");
|
|
1339
|
+
}
|
|
1340
|
+
function sanitizeZipPath(entryName) {
|
|
1341
|
+
const norm = entryName.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
1342
|
+
for (const seg of norm) {
|
|
1343
|
+
if (seg === ".." || seg === ".") return null;
|
|
1344
|
+
}
|
|
1345
|
+
return norm;
|
|
1346
|
+
}
|
|
1347
|
+
function shouldSkipEntry(parts) {
|
|
1348
|
+
if (parts[0] === "__MACOSX") return true;
|
|
1349
|
+
const last = parts[parts.length - 1];
|
|
1350
|
+
if (last === ".DS_Store") return true;
|
|
1351
|
+
return false;
|
|
1352
|
+
}
|
|
1353
|
+
function guessMimeType(fileName) {
|
|
1354
|
+
const lower = fileName.toLowerCase();
|
|
1355
|
+
if (lower.endsWith(".png")) return "image/png";
|
|
1356
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
1357
|
+
if (lower.endsWith(".gif")) return "image/gif";
|
|
1358
|
+
if (lower.endsWith(".webp")) return "image/webp";
|
|
1359
|
+
if (lower.endsWith(".svg")) return "image/svg+xml";
|
|
1360
|
+
if (lower.endsWith(".pdf")) return "application/pdf";
|
|
1361
|
+
if (lower.endsWith(".txt")) return "text/plain";
|
|
1362
|
+
if (lower.endsWith(".json")) return "application/json";
|
|
1363
|
+
if (lower.endsWith(".zip")) return "application/zip";
|
|
1364
|
+
return "application/octet-stream";
|
|
1365
|
+
}
|
|
1366
|
+
async function findOrCreateFolder(dataSource, entityMap, parentId, name) {
|
|
1367
|
+
const safe = sanitizeStorageSegment(name);
|
|
1368
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1369
|
+
const where = parentId == null ? { kind: "folder", filename: safe, parentId: IsNull2() } : { kind: "folder", filename: safe, parentId };
|
|
1370
|
+
const existing = await repo.findOne({ where });
|
|
1371
|
+
if (existing) return existing.id;
|
|
1372
|
+
const row = await repo.save(
|
|
1373
|
+
repo.create({
|
|
1374
|
+
kind: "folder",
|
|
1375
|
+
parentId,
|
|
1376
|
+
filename: safe,
|
|
1377
|
+
url: null,
|
|
1378
|
+
mimeType: "inode/directory",
|
|
1379
|
+
size: 0,
|
|
1380
|
+
alt: null,
|
|
1381
|
+
isPublic: false,
|
|
1382
|
+
deleted: false
|
|
1383
|
+
})
|
|
1384
|
+
);
|
|
1385
|
+
return row.id;
|
|
1386
|
+
}
|
|
1387
|
+
async function ensureFolderChain(dataSource, entityMap, rootParentId, pathSegments) {
|
|
1388
|
+
let pid = rootParentId;
|
|
1389
|
+
for (const seg of pathSegments) {
|
|
1390
|
+
if (!seg) continue;
|
|
1391
|
+
pid = await findOrCreateFolder(dataSource, entityMap, pid, seg);
|
|
1392
|
+
}
|
|
1393
|
+
return pid;
|
|
1394
|
+
}
|
|
1395
|
+
async function extractZipMediaIntoParentTree(opts) {
|
|
1396
|
+
const { dataSource, entityMap, zipMediaRow } = opts;
|
|
1397
|
+
const row = zipMediaRow;
|
|
1398
|
+
if (row.kind !== "file" || !row.url) throw new Error("Not a file");
|
|
1399
|
+
if (!isZipMedia(row.mimeType, row.filename)) throw new Error("Not a zip archive");
|
|
1400
|
+
const buffer = await readBufferFromPublicUrl(row.url);
|
|
1401
|
+
const { default: AdmZip } = await import("adm-zip");
|
|
1402
|
+
const zip = new AdmZip(buffer);
|
|
1403
|
+
const entries = zip.getEntries();
|
|
1404
|
+
if (entries.length > MAX_ENTRIES) throw new Error(`Too many zip entries (max ${MAX_ENTRIES})`);
|
|
1405
|
+
const rootParentId = row.parentId;
|
|
1406
|
+
const items = [];
|
|
1407
|
+
let totalUncompressed = 0;
|
|
1408
|
+
for (const e of entries) {
|
|
1409
|
+
const raw = e.entryName;
|
|
1410
|
+
const parts = sanitizeZipPath(raw);
|
|
1411
|
+
if (!parts || shouldSkipEntry(parts)) continue;
|
|
1412
|
+
const isDir = e.isDirectory || /\/$/.test(raw);
|
|
1413
|
+
let data = null;
|
|
1414
|
+
if (!isDir) {
|
|
1415
|
+
data = e.getData();
|
|
1416
|
+
totalUncompressed += data.length;
|
|
1417
|
+
if (totalUncompressed > MAX_TOTAL_UNCOMPRESSED) {
|
|
1418
|
+
throw new Error(`Uncompressed content exceeds limit (${MAX_TOTAL_UNCOMPRESSED} bytes)`);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
items.push({ parts, isDir, data });
|
|
1422
|
+
}
|
|
1423
|
+
items.sort((a, b) => {
|
|
1424
|
+
const da = a.parts.length;
|
|
1425
|
+
const db = b.parts.length;
|
|
1426
|
+
if (da !== db) return da - db;
|
|
1427
|
+
return a.parts.join("/").localeCompare(b.parts.join("/"));
|
|
1428
|
+
});
|
|
1429
|
+
let files = 0;
|
|
1430
|
+
let folderEntries = 0;
|
|
1431
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1432
|
+
for (const it of items) {
|
|
1433
|
+
if (it.isDir) {
|
|
1434
|
+
await ensureFolderChain(dataSource, entityMap, rootParentId, it.parts);
|
|
1435
|
+
folderEntries++;
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
const fileName = it.parts[it.parts.length - 1];
|
|
1439
|
+
const dirParts = it.parts.slice(0, -1);
|
|
1440
|
+
const parentFolderId = await ensureFolderChain(dataSource, entityMap, rootParentId, dirParts);
|
|
1441
|
+
const buf = it.data;
|
|
1442
|
+
const relBase = await relativePathFromMediaParentId(dataSource, entityMap, parentFolderId);
|
|
1443
|
+
const relativeUnderUploads = relBase ? `${relBase}/${fileName}` : fileName;
|
|
1444
|
+
const contentType = guessMimeType(fileName);
|
|
1445
|
+
let publicUrl;
|
|
1446
|
+
if (opts.storage) {
|
|
1447
|
+
publicUrl = await opts.storage.upload(buf, `uploads/${relativeUnderUploads}`, contentType);
|
|
1448
|
+
} else {
|
|
1449
|
+
const fs = await import("fs/promises");
|
|
1450
|
+
const pathMod = await import("path");
|
|
1451
|
+
const dir = pathMod.join(process.cwd(), opts.localUploadDir);
|
|
1452
|
+
const filePath = pathMod.join(dir, relativeUnderUploads);
|
|
1453
|
+
await fs.mkdir(pathMod.dirname(filePath), { recursive: true });
|
|
1454
|
+
await fs.writeFile(filePath, buf);
|
|
1455
|
+
publicUrl = `/${opts.localUploadDir.replace(/^\/+/, "").replace(/\\/g, "/")}/${relativeUnderUploads.replace(/\\/g, "/")}`;
|
|
1456
|
+
}
|
|
1457
|
+
await repo.save(
|
|
1458
|
+
repo.create({
|
|
1459
|
+
kind: "file",
|
|
1460
|
+
parentId: parentFolderId,
|
|
1461
|
+
filename: fileName,
|
|
1462
|
+
url: publicUrl,
|
|
1463
|
+
mimeType: contentType,
|
|
1464
|
+
size: buf.length,
|
|
1465
|
+
alt: null,
|
|
1466
|
+
isPublic: false,
|
|
1467
|
+
deleted: false
|
|
1468
|
+
})
|
|
1469
|
+
);
|
|
1470
|
+
files++;
|
|
1471
|
+
}
|
|
1472
|
+
return { files, folderEntries };
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1226
1475
|
// src/api/cms-handlers.ts
|
|
1227
1476
|
function createDashboardStatsHandler(config) {
|
|
1228
1477
|
const { dataSource, entityMap, json, requireAuth, requirePermission, requireEntityPermission } = config;
|
|
@@ -1240,26 +1489,209 @@ function createDashboardStatsHandler(config) {
|
|
|
1240
1489
|
try {
|
|
1241
1490
|
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
|
|
1242
1491
|
const repo = (name) => entityMap[name] ? dataSource.getRepository(entityMap[name]) : void 0;
|
|
1243
|
-
const [contactsCount, formsCount, formSubmissionsCount, usersCount, blogsCount, recentContacts, recentSubmissions] = await Promise.all([
|
|
1492
|
+
const [contactsCount, formsCount, formSubmissionsCount, usersCount, blogsCount, recentContacts, recentSubmissions, contactTypeRows] = await Promise.all([
|
|
1244
1493
|
repo("contacts")?.count() ?? 0,
|
|
1245
1494
|
repo("forms")?.count({ where: { deleted: false } }) ?? 0,
|
|
1246
1495
|
repo("form_submissions")?.count() ?? 0,
|
|
1247
1496
|
repo("users")?.count({ where: { deleted: false } }) ?? 0,
|
|
1248
1497
|
repo("blogs")?.count({ where: { deleted: false } }) ?? 0,
|
|
1249
1498
|
repo("contacts")?.count({ where: { createdAt: MoreThanOrEqual(sevenDaysAgo) } }) ?? 0,
|
|
1250
|
-
repo("form_submissions")?.count({ where: { createdAt: MoreThanOrEqual(sevenDaysAgo) } }) ?? 0
|
|
1499
|
+
repo("form_submissions")?.count({ where: { createdAt: MoreThanOrEqual(sevenDaysAgo) } }) ?? 0,
|
|
1500
|
+
repo("contacts")?.createQueryBuilder("c").select("COALESCE(NULLIF(TRIM(c.type), ''), 'unknown')", "type").addSelect("COUNT(*)", "count").where("c.deleted = :deleted", { deleted: false }).groupBy("COALESCE(NULLIF(TRIM(c.type), ''), 'unknown')").getRawMany() ?? []
|
|
1251
1501
|
]);
|
|
1252
1502
|
return json({
|
|
1253
1503
|
contacts: { total: contactsCount, recent: recentContacts },
|
|
1254
1504
|
forms: { total: formsCount, submissions: formSubmissionsCount, recentSubmissions },
|
|
1255
1505
|
users: usersCount,
|
|
1256
|
-
blogs: blogsCount
|
|
1506
|
+
blogs: blogsCount,
|
|
1507
|
+
contactTypes: (contactTypeRows ?? []).map((row) => ({
|
|
1508
|
+
type: row.type || "unknown",
|
|
1509
|
+
count: Number(row.count || 0)
|
|
1510
|
+
}))
|
|
1257
1511
|
});
|
|
1258
1512
|
} catch (err) {
|
|
1259
1513
|
return json({ error: "Failed to fetch dashboard stats" }, { status: 500 });
|
|
1260
1514
|
}
|
|
1261
1515
|
};
|
|
1262
1516
|
}
|
|
1517
|
+
function toNum(v) {
|
|
1518
|
+
const n = typeof v === "number" ? v : Number(v ?? 0);
|
|
1519
|
+
return Number.isFinite(n) ? n : 0;
|
|
1520
|
+
}
|
|
1521
|
+
function toIsoDate(d) {
|
|
1522
|
+
return d.toISOString().slice(0, 10);
|
|
1523
|
+
}
|
|
1524
|
+
function createEcommerceAnalyticsHandler(config) {
|
|
1525
|
+
const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
|
|
1526
|
+
return async function GET(req) {
|
|
1527
|
+
const authErr = await requireAuth(req);
|
|
1528
|
+
if (authErr) return authErr;
|
|
1529
|
+
if (requireEntityPermission) {
|
|
1530
|
+
const pe = await requireEntityPermission(req, "analytics", "read");
|
|
1531
|
+
if (pe) return pe;
|
|
1532
|
+
}
|
|
1533
|
+
if (!entityMap.orders || !entityMap.order_items || !entityMap.payments || !entityMap.products) {
|
|
1534
|
+
return json({ error: "Store analytics unavailable" }, { status: 404 });
|
|
1535
|
+
}
|
|
1536
|
+
try {
|
|
1537
|
+
const url = new URL(req.url);
|
|
1538
|
+
const rawDays = parseInt(url.searchParams.get("days") || "30", 10);
|
|
1539
|
+
const days = Number.isFinite(rawDays) ? Math.min(365, Math.max(7, rawDays)) : 30;
|
|
1540
|
+
const end = /* @__PURE__ */ new Date();
|
|
1541
|
+
const start = new Date(end.getTime() - days * 24 * 60 * 60 * 1e3);
|
|
1542
|
+
const orderRepo = dataSource.getRepository(entityMap.orders);
|
|
1543
|
+
const paymentRepo = dataSource.getRepository(entityMap.payments);
|
|
1544
|
+
const itemRepo = dataSource.getRepository(entityMap.order_items);
|
|
1545
|
+
const productRepo = dataSource.getRepository(entityMap.products);
|
|
1546
|
+
const [salesOrders, returnOrders, replacementOrders, payments, products] = await Promise.all([
|
|
1547
|
+
orderRepo.find({
|
|
1548
|
+
where: { deleted: false, createdAt: MoreThanOrEqual(start), orderKind: "sale", status: In(["confirmed", "processing", "completed"]) },
|
|
1549
|
+
select: ["id", "contactId", "createdAt", "subtotal", "discount", "tax", "total", "status"]
|
|
1550
|
+
}),
|
|
1551
|
+
orderRepo.find({
|
|
1552
|
+
where: { deleted: false, createdAt: MoreThanOrEqual(start), orderKind: "return" },
|
|
1553
|
+
select: ["id", "createdAt", "total"]
|
|
1554
|
+
}),
|
|
1555
|
+
orderRepo.find({
|
|
1556
|
+
where: { deleted: false, createdAt: MoreThanOrEqual(start), orderKind: "replacement" },
|
|
1557
|
+
select: ["id", "createdAt", "total"]
|
|
1558
|
+
}),
|
|
1559
|
+
paymentRepo.find({
|
|
1560
|
+
where: { deleted: false, createdAt: MoreThanOrEqual(start) },
|
|
1561
|
+
select: ["id", "status", "method", "amount", "createdAt"]
|
|
1562
|
+
}),
|
|
1563
|
+
productRepo.find({
|
|
1564
|
+
where: { deleted: false },
|
|
1565
|
+
select: ["id", "name", "quantity"]
|
|
1566
|
+
})
|
|
1567
|
+
]);
|
|
1568
|
+
const saleOrderIds = salesOrders.map((o) => o.id);
|
|
1569
|
+
const orderItems = saleOrderIds.length ? await itemRepo.find({
|
|
1570
|
+
where: { orderId: In(saleOrderIds) },
|
|
1571
|
+
select: ["id", "orderId", "productId", "quantity", "total"]
|
|
1572
|
+
}) : [];
|
|
1573
|
+
const grossSales = salesOrders.reduce((sum, o) => sum + toNum(o.subtotal), 0);
|
|
1574
|
+
const discounts = salesOrders.reduce((sum, o) => sum + toNum(o.discount), 0);
|
|
1575
|
+
const taxes = salesOrders.reduce((sum, o) => sum + toNum(o.tax), 0);
|
|
1576
|
+
const returnsValue = returnOrders.reduce((sum, o) => sum + toNum(o.total), 0);
|
|
1577
|
+
const replacementsValue = replacementOrders.reduce((sum, o) => sum + toNum(o.total), 0);
|
|
1578
|
+
const netSales = grossSales - discounts - returnsValue;
|
|
1579
|
+
const ordersCount = salesOrders.length;
|
|
1580
|
+
const aov = ordersCount > 0 ? netSales / ordersCount : 0;
|
|
1581
|
+
const returnRate = ordersCount > 0 ? returnOrders.length / ordersCount * 100 : 0;
|
|
1582
|
+
const salesByDate = /* @__PURE__ */ new Map();
|
|
1583
|
+
const returnsByDate = /* @__PURE__ */ new Map();
|
|
1584
|
+
for (const o of salesOrders) {
|
|
1585
|
+
const key = toIsoDate(new Date(o.createdAt));
|
|
1586
|
+
const row = salesByDate.get(key) ?? { value: 0, orders: 0 };
|
|
1587
|
+
row.value += toNum(o.total);
|
|
1588
|
+
row.orders += 1;
|
|
1589
|
+
salesByDate.set(key, row);
|
|
1590
|
+
}
|
|
1591
|
+
for (const o of returnOrders) {
|
|
1592
|
+
const key = toIsoDate(new Date(o.createdAt));
|
|
1593
|
+
const row = returnsByDate.get(key) ?? { value: 0, count: 0 };
|
|
1594
|
+
row.value += toNum(o.total);
|
|
1595
|
+
row.count += 1;
|
|
1596
|
+
returnsByDate.set(key, row);
|
|
1597
|
+
}
|
|
1598
|
+
const salesOverTime = [];
|
|
1599
|
+
const returnsTrend = [];
|
|
1600
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
1601
|
+
const d = new Date(end.getTime() - i * 24 * 60 * 60 * 1e3);
|
|
1602
|
+
const key = toIsoDate(d);
|
|
1603
|
+
const sales = salesByDate.get(key) ?? { value: 0, orders: 0 };
|
|
1604
|
+
const returns = returnsByDate.get(key) ?? { value: 0, count: 0 };
|
|
1605
|
+
salesOverTime.push({ date: key, value: Number(sales.value.toFixed(2)), orders: sales.orders });
|
|
1606
|
+
returnsTrend.push({ date: key, value: Number(returns.value.toFixed(2)), count: returns.count });
|
|
1607
|
+
}
|
|
1608
|
+
const productNameMap = /* @__PURE__ */ new Map();
|
|
1609
|
+
for (const p of products) productNameMap.set(Number(p.id), (p.name || `Product #${p.id}`).trim());
|
|
1610
|
+
const productAgg = /* @__PURE__ */ new Map();
|
|
1611
|
+
for (const item of orderItems) {
|
|
1612
|
+
const productId = Number(item.productId);
|
|
1613
|
+
const productName = productNameMap.get(productId) || `Product #${productId}`;
|
|
1614
|
+
const row = productAgg.get(productId) ?? { name: productName, units: 0, sales: 0 };
|
|
1615
|
+
row.units += toNum(item.quantity);
|
|
1616
|
+
row.sales += toNum(item.total);
|
|
1617
|
+
productAgg.set(productId, row);
|
|
1618
|
+
}
|
|
1619
|
+
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)) }));
|
|
1620
|
+
const allSaleOrderContactIds = Array.from(new Set(salesOrders.map((o) => Number(o.contactId)).filter((n) => Number.isInteger(n) && n > 0)));
|
|
1621
|
+
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() : [];
|
|
1622
|
+
const countMap = /* @__PURE__ */ new Map();
|
|
1623
|
+
for (const c of allTimeCounts) countMap.set(Number(c.contactId), Number(c.total));
|
|
1624
|
+
const purchasingCustomers = allSaleOrderContactIds.length;
|
|
1625
|
+
const returningCustomers = allSaleOrderContactIds.filter((id) => (countMap.get(id) ?? 0) > 1).length;
|
|
1626
|
+
const newCustomers = Math.max(0, purchasingCustomers - returningCustomers);
|
|
1627
|
+
const returningCustomerRate = purchasingCustomers > 0 ? returningCustomers / purchasingCustomers * 100 : 0;
|
|
1628
|
+
const totalPayments = payments.length;
|
|
1629
|
+
const completedPayments = payments.filter((p) => p.status === "completed").length;
|
|
1630
|
+
const failedPayments = payments.filter((p) => p.status === "failed").length;
|
|
1631
|
+
const paymentSuccessRate = totalPayments > 0 ? completedPayments / totalPayments * 100 : 0;
|
|
1632
|
+
const paymentMethodMap = /* @__PURE__ */ new Map();
|
|
1633
|
+
for (const p of payments) {
|
|
1634
|
+
const method = (p.method || "unknown").toLowerCase();
|
|
1635
|
+
const row = paymentMethodMap.get(method) ?? { method, count: 0, amount: 0 };
|
|
1636
|
+
row.count += 1;
|
|
1637
|
+
row.amount += toNum(p.amount);
|
|
1638
|
+
paymentMethodMap.set(method, row);
|
|
1639
|
+
}
|
|
1640
|
+
const paymentMethods = Array.from(paymentMethodMap.values()).sort((a, b) => b.count - a.count).map((p) => ({ ...p, amount: Number(p.amount.toFixed(2)) }));
|
|
1641
|
+
const totalInventory = products.reduce((sum, p) => sum + toNum(p.quantity), 0);
|
|
1642
|
+
const outOfStockCount = products.filter((p) => toNum(p.quantity) <= 0).length;
|
|
1643
|
+
const lowStockCount = products.filter((p) => toNum(p.quantity) > 0 && toNum(p.quantity) <= 5).length;
|
|
1644
|
+
const inventoryRisk = {
|
|
1645
|
+
outOfStockCount,
|
|
1646
|
+
lowStockCount,
|
|
1647
|
+
totalInventory
|
|
1648
|
+
};
|
|
1649
|
+
return json({
|
|
1650
|
+
rangeDays: days,
|
|
1651
|
+
kpis: {
|
|
1652
|
+
netSales: Number(netSales.toFixed(2)),
|
|
1653
|
+
grossSales: Number(grossSales.toFixed(2)),
|
|
1654
|
+
ordersPlaced: ordersCount,
|
|
1655
|
+
averageOrderValue: Number(aov.toFixed(2)),
|
|
1656
|
+
returningCustomerRate: Number(returningCustomerRate.toFixed(2)),
|
|
1657
|
+
returnRate: Number(returnRate.toFixed(2)),
|
|
1658
|
+
returnValue: Number(returnsValue.toFixed(2)),
|
|
1659
|
+
discounts: Number(discounts.toFixed(2)),
|
|
1660
|
+
taxes: Number(taxes.toFixed(2)),
|
|
1661
|
+
paymentSuccessRate: Number(paymentSuccessRate.toFixed(2))
|
|
1662
|
+
},
|
|
1663
|
+
salesOverTime,
|
|
1664
|
+
topProducts,
|
|
1665
|
+
customerMix: {
|
|
1666
|
+
newCustomers,
|
|
1667
|
+
returningCustomers,
|
|
1668
|
+
repeatPurchaseRate: Number(returningCustomerRate.toFixed(2))
|
|
1669
|
+
},
|
|
1670
|
+
returnsTrend,
|
|
1671
|
+
paymentPerformance: {
|
|
1672
|
+
successCount: completedPayments,
|
|
1673
|
+
failedCount: failedPayments,
|
|
1674
|
+
successRate: Number(paymentSuccessRate.toFixed(2)),
|
|
1675
|
+
methods: paymentMethods
|
|
1676
|
+
},
|
|
1677
|
+
conversionProxy: {
|
|
1678
|
+
sessions: 0,
|
|
1679
|
+
checkoutStarted: 0,
|
|
1680
|
+
ordersPlaced: ordersCount
|
|
1681
|
+
},
|
|
1682
|
+
salesBreakdown: {
|
|
1683
|
+
sales: { count: ordersCount, value: Number(grossSales.toFixed(2)) },
|
|
1684
|
+
returns: { count: returnOrders.length, value: Number(returnsValue.toFixed(2)) },
|
|
1685
|
+
replacements: { count: replacementOrders.length, value: Number(replacementsValue.toFixed(2)) }
|
|
1686
|
+
},
|
|
1687
|
+
geoPerformance: [],
|
|
1688
|
+
inventoryRisk
|
|
1689
|
+
});
|
|
1690
|
+
} catch {
|
|
1691
|
+
return json({ error: "Failed to fetch ecommerce analytics" }, { status: 500 });
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1263
1695
|
function createAnalyticsHandlers(config) {
|
|
1264
1696
|
const { json, getAnalyticsData, getPropertyId, getPermissions } = config;
|
|
1265
1697
|
return {
|
|
@@ -1291,8 +1723,27 @@ function createAnalyticsHandlers(config) {
|
|
|
1291
1723
|
};
|
|
1292
1724
|
}
|
|
1293
1725
|
function createUploadHandler(config) {
|
|
1294
|
-
const {
|
|
1295
|
-
|
|
1726
|
+
const {
|
|
1727
|
+
json,
|
|
1728
|
+
requireAuth,
|
|
1729
|
+
requireEntityPermission,
|
|
1730
|
+
storage,
|
|
1731
|
+
localUploadDir = "public/uploads",
|
|
1732
|
+
allowedTypes,
|
|
1733
|
+
maxSizeBytes = 10 * 1024 * 1024,
|
|
1734
|
+
dataSource,
|
|
1735
|
+
entityMap
|
|
1736
|
+
} = config;
|
|
1737
|
+
const allowed = allowedTypes ?? [
|
|
1738
|
+
"image/jpeg",
|
|
1739
|
+
"image/png",
|
|
1740
|
+
"image/gif",
|
|
1741
|
+
"image/webp",
|
|
1742
|
+
"application/pdf",
|
|
1743
|
+
"text/plain",
|
|
1744
|
+
"application/zip",
|
|
1745
|
+
"application/x-zip-compressed"
|
|
1746
|
+
];
|
|
1296
1747
|
return async function POST(req) {
|
|
1297
1748
|
const authErr = await requireAuth(req);
|
|
1298
1749
|
if (authErr) return authErr;
|
|
@@ -1305,28 +1756,92 @@ function createUploadHandler(config) {
|
|
|
1305
1756
|
const file = formData.get("file");
|
|
1306
1757
|
if (!file) return json({ error: "No file uploaded" }, { status: 400 });
|
|
1307
1758
|
if (!allowed.includes(file.type)) return json({ error: "File type not allowed" }, { status: 400 });
|
|
1308
|
-
|
|
1759
|
+
const defaultMax = 10 * 1024 * 1024;
|
|
1760
|
+
const maxZipBytes = 80 * 1024 * 1024;
|
|
1761
|
+
const baseMax = maxSizeBytes ?? defaultMax;
|
|
1762
|
+
const effectiveMax = file.type === "application/zip" || file.type === "application/x-zip-compressed" ? Math.max(baseMax, maxZipBytes) : baseMax;
|
|
1763
|
+
if (file.size > effectiveMax) return json({ error: "File size exceeds limit" }, { status: 400 });
|
|
1764
|
+
const parentRaw = formData.get("parentId");
|
|
1765
|
+
let parentId = null;
|
|
1766
|
+
if (parentRaw != null && String(parentRaw).trim() !== "") {
|
|
1767
|
+
const n = Number(parentRaw);
|
|
1768
|
+
if (!Number.isFinite(n)) return json({ error: "Invalid parentId" }, { status: 400 });
|
|
1769
|
+
parentId = n;
|
|
1770
|
+
}
|
|
1771
|
+
let folder = "";
|
|
1772
|
+
if (parentId != null) {
|
|
1773
|
+
if (!dataSource || !entityMap?.media) {
|
|
1774
|
+
return json({ error: "Upload handler needs dataSource and entityMap for folder uploads" }, { status: 400 });
|
|
1775
|
+
}
|
|
1776
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1777
|
+
const p = await repo.findOne({ where: { id: parentId } });
|
|
1778
|
+
if (!p || p.kind !== "folder") {
|
|
1779
|
+
return json({ error: "parent must be a folder" }, { status: 400 });
|
|
1780
|
+
}
|
|
1781
|
+
folder = await relativePathFromMediaParentId(dataSource, entityMap, parentId);
|
|
1782
|
+
} else {
|
|
1783
|
+
const folderRawLegacy = formData.get("folder") ?? formData.get("folderPath");
|
|
1784
|
+
if (folderRawLegacy && typeof folderRawLegacy === "string" && folderRawLegacy.trim()) {
|
|
1785
|
+
folder = sanitizeMediaFolderPath(folderRawLegacy);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1309
1788
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
1310
1789
|
const fileName = `${Date.now()}-${file.name}`;
|
|
1311
1790
|
const contentType = file.type || "application/octet-stream";
|
|
1791
|
+
const relativeUnderUploads = folder ? `${folder}/${fileName}` : fileName;
|
|
1312
1792
|
const raw = typeof storage === "function" ? storage() : storage;
|
|
1313
1793
|
const storageService = raw instanceof Promise ? await raw : raw;
|
|
1314
1794
|
if (storageService) {
|
|
1315
|
-
const fileUrl = await storageService.upload(buffer, `uploads/${
|
|
1316
|
-
return json({ filePath: fileUrl });
|
|
1795
|
+
const fileUrl = await storageService.upload(buffer, `uploads/${relativeUnderUploads}`, contentType);
|
|
1796
|
+
return json({ filePath: fileUrl, parentId });
|
|
1317
1797
|
}
|
|
1318
1798
|
const fs = await import("fs/promises");
|
|
1319
1799
|
const path = await import("path");
|
|
1320
1800
|
const dir = path.join(process.cwd(), localUploadDir);
|
|
1321
|
-
|
|
1322
|
-
|
|
1801
|
+
const filePath = path.join(dir, relativeUnderUploads);
|
|
1802
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
1323
1803
|
await fs.writeFile(filePath, buffer);
|
|
1324
|
-
|
|
1804
|
+
const urlRel = `${localUploadDir.replace(/^\/+/, "").replace(/\\/g, "/")}/${relativeUnderUploads.replace(/\\/g, "/")}`;
|
|
1805
|
+
return json({ filePath: `/${urlRel}`, parentId });
|
|
1325
1806
|
} catch (err) {
|
|
1326
1807
|
return json({ error: "File upload failed" }, { status: 500 });
|
|
1327
1808
|
}
|
|
1328
1809
|
};
|
|
1329
1810
|
}
|
|
1811
|
+
function createMediaZipExtractHandler(config) {
|
|
1812
|
+
const { json, requireAuth, requireEntityPermission, storage, localUploadDir = "public/uploads", dataSource, entityMap } = config;
|
|
1813
|
+
return async function POST(_req, zipMediaId) {
|
|
1814
|
+
const authErr = await requireAuth(_req);
|
|
1815
|
+
if (authErr) return authErr;
|
|
1816
|
+
if (requireEntityPermission) {
|
|
1817
|
+
const pe = await requireEntityPermission(_req, "media", "create");
|
|
1818
|
+
if (pe) return pe;
|
|
1819
|
+
}
|
|
1820
|
+
if (!dataSource || !entityMap?.media) {
|
|
1821
|
+
return json({ error: "Media extract requires dataSource and entityMap" }, { status: 500 });
|
|
1822
|
+
}
|
|
1823
|
+
const id = Number(zipMediaId);
|
|
1824
|
+
if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
|
|
1825
|
+
const repo = dataSource.getRepository(entityMap.media);
|
|
1826
|
+
const row = await repo.findOne({ where: { id } });
|
|
1827
|
+
if (!row) return json({ error: "Not found" }, { status: 404 });
|
|
1828
|
+
try {
|
|
1829
|
+
const raw = typeof storage === "function" ? storage() : storage;
|
|
1830
|
+
const storageService = raw instanceof Promise ? await raw : raw;
|
|
1831
|
+
const result = await extractZipMediaIntoParentTree({
|
|
1832
|
+
dataSource,
|
|
1833
|
+
entityMap,
|
|
1834
|
+
zipMediaRow: row,
|
|
1835
|
+
storage: storageService,
|
|
1836
|
+
localUploadDir
|
|
1837
|
+
});
|
|
1838
|
+
return json({ ok: true, ...result });
|
|
1839
|
+
} catch (e) {
|
|
1840
|
+
const msg = e instanceof Error ? e.message : "Extract failed";
|
|
1841
|
+
return json({ error: msg }, { status: 400 });
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1330
1845
|
function createBlogBySlugHandler(config) {
|
|
1331
1846
|
const { dataSource, entityMap, json } = config;
|
|
1332
1847
|
return async function GET(_req, slug) {
|
|
@@ -2468,6 +2983,7 @@ function createCmsApiHandler(config) {
|
|
|
2468
2983
|
getCms,
|
|
2469
2984
|
userAuth: userAuthConfig,
|
|
2470
2985
|
dashboard,
|
|
2986
|
+
ecommerceAnalytics,
|
|
2471
2987
|
analytics: analyticsConfig,
|
|
2472
2988
|
upload,
|
|
2473
2989
|
blogBySlug,
|
|
@@ -2534,8 +3050,28 @@ function createCmsApiHandler(config) {
|
|
|
2534
3050
|
});
|
|
2535
3051
|
const userAuthRouter = userAuth ? createUserAuthApiRouter(userAuth) : null;
|
|
2536
3052
|
const dashboardGet = dashboard ? createDashboardStatsHandler(mergePerm(dashboard) ?? dashboard) : null;
|
|
3053
|
+
const ecommerceAnalyticsResolved = mergePerm(
|
|
3054
|
+
ecommerceAnalytics ?? {
|
|
3055
|
+
dataSource,
|
|
3056
|
+
entityMap,
|
|
3057
|
+
json: config.json,
|
|
3058
|
+
requireAuth: config.requireAuth
|
|
3059
|
+
}
|
|
3060
|
+
) ?? {
|
|
3061
|
+
dataSource,
|
|
3062
|
+
entityMap,
|
|
3063
|
+
json: config.json,
|
|
3064
|
+
requireAuth: config.requireAuth
|
|
3065
|
+
};
|
|
3066
|
+
const ecommerceAnalyticsGet = createEcommerceAnalyticsHandler(ecommerceAnalyticsResolved);
|
|
2537
3067
|
const analyticsHandlers = analytics ? createAnalyticsHandlers(analytics) : null;
|
|
2538
|
-
const
|
|
3068
|
+
const uploadMerged = upload ? {
|
|
3069
|
+
...mergePerm(upload) ?? upload,
|
|
3070
|
+
dataSource: upload.dataSource ?? dataSource,
|
|
3071
|
+
entityMap: upload.entityMap ?? entityMap
|
|
3072
|
+
} : null;
|
|
3073
|
+
const uploadPost = uploadMerged ? createUploadHandler(uploadMerged) : null;
|
|
3074
|
+
const zipExtractPost = uploadMerged ? createMediaZipExtractHandler(uploadMerged) : null;
|
|
2539
3075
|
const blogBySlugGet = blogBySlug ? createBlogBySlugHandler(blogBySlug) : null;
|
|
2540
3076
|
const formBySlugGet = formBySlug ? createFormBySlugHandler(formBySlug) : null;
|
|
2541
3077
|
const formSaveHandlers = formSaveConfig ? createFormSaveHandlers(mergePerm(formSaveConfig) ?? formSaveConfig) : null;
|
|
@@ -2584,6 +3120,11 @@ function createCmsApiHandler(config) {
|
|
|
2584
3120
|
if (path[0] === "dashboard" && path[1] === "stats" && path.length === 2 && method === "GET" && dashboardGet) {
|
|
2585
3121
|
return dashboardGet(req);
|
|
2586
3122
|
}
|
|
3123
|
+
if (path[0] === "dashboard" && path[1] === "ecommerce" && path.length === 2 && method === "GET" && ecommerceAnalyticsGet) {
|
|
3124
|
+
const g = await analyticsGate();
|
|
3125
|
+
if (g) return g;
|
|
3126
|
+
return ecommerceAnalyticsGet(req);
|
|
3127
|
+
}
|
|
2587
3128
|
if (path[0] === "analytics" && analyticsHandlers) {
|
|
2588
3129
|
if (path.length === 1 && method === "GET") {
|
|
2589
3130
|
const g = await analyticsGate();
|
|
@@ -2602,6 +3143,9 @@ function createCmsApiHandler(config) {
|
|
|
2602
3143
|
}
|
|
2603
3144
|
}
|
|
2604
3145
|
if (path[0] === "upload" && path.length === 1 && method === "POST" && uploadPost) return uploadPost(req);
|
|
3146
|
+
if (path[0] === "media" && path[1] === "extract" && path.length === 3 && method === "POST" && zipExtractPost) {
|
|
3147
|
+
return zipExtractPost(req, path[2]);
|
|
3148
|
+
}
|
|
2605
3149
|
if (path[0] === "blogs" && path[1] === "slug" && path.length === 3 && method === "GET" && blogBySlugGet) {
|
|
2606
3150
|
return blogBySlugGet(req, path[2]);
|
|
2607
3151
|
}
|
|
@@ -2739,7 +3283,7 @@ function createCmsApiHandler(config) {
|
|
|
2739
3283
|
}
|
|
2740
3284
|
|
|
2741
3285
|
// src/api/storefront-handlers.ts
|
|
2742
|
-
import { In, IsNull as
|
|
3286
|
+
import { In as In2, IsNull as IsNull4 } from "typeorm";
|
|
2743
3287
|
|
|
2744
3288
|
// src/lib/is-valid-signup-email.ts
|
|
2745
3289
|
var MAX_EMAIL = 254;
|
|
@@ -2999,7 +3543,7 @@ async function queueSms(cms, payload) {
|
|
|
2999
3543
|
|
|
3000
3544
|
// src/lib/otp-challenge.ts
|
|
3001
3545
|
import { createHmac, randomInt, timingSafeEqual } from "crypto";
|
|
3002
|
-
import { IsNull as
|
|
3546
|
+
import { IsNull as IsNull3, MoreThan as MoreThan2 } from "typeorm";
|
|
3003
3547
|
var OTP_TTL_MS = 10 * 60 * 1e3;
|
|
3004
3548
|
var MAX_SENDS_PER_HOUR = 5;
|
|
3005
3549
|
var MAX_VERIFY_ATTEMPTS = 8;
|
|
@@ -3047,7 +3591,7 @@ async function createOtpChallenge(dataSource, entityMap, input) {
|
|
|
3047
3591
|
await repo.delete({
|
|
3048
3592
|
purpose,
|
|
3049
3593
|
identifier,
|
|
3050
|
-
consumedAt:
|
|
3594
|
+
consumedAt: IsNull3()
|
|
3051
3595
|
});
|
|
3052
3596
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
3053
3597
|
const codeHash = hashOtpCode(code, purpose, identifier, pepper);
|
|
@@ -3068,7 +3612,7 @@ async function verifyAndConsumeOtpChallenge(dataSource, entityMap, input) {
|
|
|
3068
3612
|
const { purpose, identifier, code, pepper } = input;
|
|
3069
3613
|
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
3070
3614
|
const row = await repo.findOne({
|
|
3071
|
-
where: { purpose, identifier, consumedAt:
|
|
3615
|
+
where: { purpose, identifier, consumedAt: IsNull3() },
|
|
3072
3616
|
order: { id: "DESC" }
|
|
3073
3617
|
});
|
|
3074
3618
|
if (!row) {
|
|
@@ -3302,7 +3846,7 @@ function createStorefrontApiHandler(config) {
|
|
|
3302
3846
|
const u = await userRepo().findOne({ where: { id: userId } });
|
|
3303
3847
|
if (!u) return null;
|
|
3304
3848
|
const unclaimed = await contactRepo().findOne({
|
|
3305
|
-
where: { email: u.email, userId:
|
|
3849
|
+
where: { email: u.email, userId: IsNull4(), deleted: false }
|
|
3306
3850
|
});
|
|
3307
3851
|
if (unclaimed) {
|
|
3308
3852
|
await contactRepo().update(unclaimed.id, { userId });
|
|
@@ -4343,7 +4887,7 @@ function createStorefrontApiHandler(config) {
|
|
|
4343
4887
|
const previewByOrder = {};
|
|
4344
4888
|
if (orderIds.length) {
|
|
4345
4889
|
const oItems = await orderItemRepo().find({
|
|
4346
|
-
where: { orderId:
|
|
4890
|
+
where: { orderId: In2(orderIds) },
|
|
4347
4891
|
relations: ["product"],
|
|
4348
4892
|
order: { id: "ASC" }
|
|
4349
4893
|
});
|
|
@@ -4475,9 +5019,11 @@ export {
|
|
|
4475
5019
|
createCrudByIdHandler,
|
|
4476
5020
|
createCrudHandler,
|
|
4477
5021
|
createDashboardStatsHandler,
|
|
5022
|
+
createEcommerceAnalyticsHandler,
|
|
4478
5023
|
createForgotPasswordHandler,
|
|
4479
5024
|
createFormBySlugHandler,
|
|
4480
5025
|
createInviteAcceptHandler,
|
|
5026
|
+
createMediaZipExtractHandler,
|
|
4481
5027
|
createSetPasswordHandler,
|
|
4482
5028
|
createSettingsApiHandlers,
|
|
4483
5029
|
createStorefrontApiHandler,
|