@infuro/cms-core 1.0.8 → 1.0.10
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 +2562 -1176
- package/dist/admin.cjs.map +1 -1
- package/dist/admin.d.cts +41 -2
- package/dist/admin.d.ts +41 -2
- package/dist/admin.js +2596 -1214
- package/dist/admin.js.map +1 -1
- package/dist/api.cjs +1695 -151
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +2 -1
- package/dist/api.d.ts +2 -1
- package/dist/api.js +1689 -146
- package/dist/api.js.map +1 -1
- package/dist/auth.cjs +153 -9
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +17 -27
- package/dist/auth.d.ts +17 -27
- package/dist/auth.js +143 -8
- package/dist/auth.js.map +1 -1
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/helpers-dlrF_49e.d.cts +60 -0
- package/dist/helpers-dlrF_49e.d.ts +60 -0
- package/dist/{index-P5ajDo8-.d.ts → index-C_CZLmHD.d.cts} +88 -1
- package/dist/{index-P5ajDo8-.d.cts → index-DeO4AnAj.d.ts} +88 -1
- package/dist/index.cjs +3340 -715
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +154 -5
- package/dist/index.d.ts +154 -5
- package/dist/index.js +2821 -223
- package/dist/index.js.map +1 -1
- package/dist/migrations/1772178563555-ChatAndKnowledgeBase.ts +55 -0
- package/dist/migrations/{1731900000000-KnowledgeBaseVector.ts → 1772178563556-KnowledgeBaseVector.ts} +3 -4
- package/dist/migrations/1774300000000-RbacSeedGroupsAndPermissionUnique.ts +24 -0
- package/dist/migrations/1774300000001-SeedAdministratorUsersPermission.ts +35 -0
- package/dist/migrations/1774400000000-CustomerAdminAccessContactUser.ts +37 -0
- package/dist/migrations/1774400000001-StorefrontCartWishlist.ts +100 -0
- package/dist/migrations/1774400000002-WishlistGuestId.ts +29 -0
- package/dist/migrations/1774500000000-ProductCollectionHsn.ts +15 -0
- package/package.json +13 -7
- package/dist/migrations/1731800000000-ChatAndKnowledgeBase.ts +0 -39
- /package/{dist → src/admin}/admin.css +0 -0
package/dist/index.js
CHANGED
|
@@ -17,105 +17,91 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
17
17
|
return result;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
// src/plugins/email/email-
|
|
21
|
-
var
|
|
22
|
-
__export(
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
// src/plugins/email/email-queue.ts
|
|
21
|
+
var email_queue_exports = {};
|
|
22
|
+
__export(email_queue_exports, {
|
|
23
|
+
queueEmail: () => queueEmail,
|
|
24
|
+
queueOrderPlacedEmails: () => queueOrderPlacedEmails,
|
|
25
|
+
registerEmailQueueProcessor: () => registerEmailQueueProcessor
|
|
25
26
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
27
|
+
function registerEmailQueueProcessor(cms) {
|
|
28
|
+
const queue = cms.getPlugin("queue");
|
|
29
|
+
const email = cms.getPlugin("email");
|
|
30
|
+
if (!queue || !email) return;
|
|
31
|
+
queue.registerProcessor(EMAIL_QUEUE_NAME, async (data) => {
|
|
32
|
+
const payload = data;
|
|
33
|
+
const { to, templateName, ctx, subject, html, text } = payload;
|
|
34
|
+
if (!to) return;
|
|
35
|
+
if (templateName && ctx) {
|
|
36
|
+
const rendered = email.renderTemplate(templateName, ctx);
|
|
37
|
+
await email.send({ to, subject: rendered.subject, html: rendered.html, text: rendered.text });
|
|
38
|
+
} else if (subject != null && html != null) {
|
|
39
|
+
await email.send({ to, subject, html, text });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async function queueEmail(cms, payload) {
|
|
44
|
+
const queue = cms.getPlugin("queue");
|
|
45
|
+
if (queue) {
|
|
46
|
+
await queue.add(EMAIL_QUEUE_NAME, payload);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const email = cms.getPlugin("email");
|
|
50
|
+
if (email && payload.templateName && payload.ctx) {
|
|
51
|
+
const rendered = email.renderTemplate(payload.templateName, payload.ctx);
|
|
52
|
+
await email.send({ to: payload.to, subject: rendered.subject, html: rendered.html, text: rendered.text });
|
|
53
|
+
} else if (email && payload.subject != null && payload.html != null) {
|
|
54
|
+
await email.send({ to: payload.to, subject: payload.subject, html: payload.html, text: payload.text });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function queueOrderPlacedEmails(cms, payload) {
|
|
58
|
+
const { orderNumber: orderNumber2, total, currency, customerName, customerEmail, salesTeamEmails, companyDetails, lineItems } = payload;
|
|
59
|
+
const base = {
|
|
60
|
+
orderNumber: orderNumber2,
|
|
61
|
+
total: total != null ? String(total) : void 0,
|
|
62
|
+
currency,
|
|
63
|
+
customerName,
|
|
64
|
+
companyDetails: companyDetails ?? {},
|
|
65
|
+
lineItems: lineItems ?? []
|
|
66
|
+
};
|
|
67
|
+
const customerLower = customerEmail?.trim().toLowerCase() ?? "";
|
|
68
|
+
const jobs = [];
|
|
69
|
+
if (customerEmail?.trim()) {
|
|
70
|
+
jobs.push(
|
|
71
|
+
queueEmail(cms, {
|
|
72
|
+
to: customerEmail.trim(),
|
|
73
|
+
templateName: "orderPlaced",
|
|
74
|
+
ctx: { ...base, audience: "customer" }
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const seen = /* @__PURE__ */ new Set();
|
|
79
|
+
for (const raw of salesTeamEmails) {
|
|
80
|
+
const to = raw.trim();
|
|
81
|
+
if (!to) continue;
|
|
82
|
+
const key = to.toLowerCase();
|
|
83
|
+
if (seen.has(key)) continue;
|
|
84
|
+
seen.add(key);
|
|
85
|
+
if (customerLower && key === customerLower) continue;
|
|
86
|
+
jobs.push(
|
|
87
|
+
queueEmail(cms, {
|
|
88
|
+
to,
|
|
89
|
+
templateName: "orderPlaced",
|
|
90
|
+
ctx: {
|
|
91
|
+
...base,
|
|
92
|
+
audience: "sales",
|
|
93
|
+
internalCustomerEmail: customerEmail?.trim() || void 0
|
|
90
94
|
}
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
emailTemplates = {
|
|
94
|
-
formSubmission: (data) => ({
|
|
95
|
-
subject: `New Form Submission: ${data.formName}`,
|
|
96
|
-
html: `<h2>New Form Submission</h2><p><strong>Form:</strong> ${data.formName}</p><p><strong>Contact:</strong> ${data.contactName} (${data.contactEmail})</p><pre>${JSON.stringify(data.formData, null, 2)}</pre>`,
|
|
97
|
-
text: `New Form Submission
|
|
98
|
-
Form: ${data.formName}
|
|
99
|
-
Contact: ${data.contactName} (${data.contactEmail})
|
|
100
|
-
${JSON.stringify(data.formData, null, 2)}`
|
|
101
|
-
}),
|
|
102
|
-
contactSubmission: (data) => ({
|
|
103
|
-
subject: `New Contact Form Submission from ${data.name}`,
|
|
104
|
-
html: `<h2>New Contact Form Submission</h2><p><strong>Name:</strong> ${data.name}</p><p><strong>Email:</strong> ${data.email}</p>${data.phone ? `<p><strong>Phone:</strong> ${data.phone}</p>` : ""}${data.message ? `<p><strong>Message:</strong></p><p>${data.message}</p>` : ""}`,
|
|
105
|
-
text: `New Contact Form Submission
|
|
106
|
-
Name: ${data.name}
|
|
107
|
-
Email: ${data.email}
|
|
108
|
-
${data.phone ? `Phone: ${data.phone}
|
|
109
|
-
` : ""}${data.message ? `Message: ${data.message}` : ""}`
|
|
110
|
-
}),
|
|
111
|
-
passwordReset: (data) => ({
|
|
112
|
-
subject: "Reset your password",
|
|
113
|
-
html: `<h2>Reset your password</h2><p>Click the link below to set a new password. This link expires in 1 hour.</p><p><a href="${data.resetLink}">${data.resetLink}</a></p>`,
|
|
114
|
-
text: `Reset your password: ${data.resetLink}
|
|
115
|
-
|
|
116
|
-
This link expires in 1 hour.`
|
|
117
95
|
})
|
|
118
|
-
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
await Promise.all(jobs);
|
|
99
|
+
}
|
|
100
|
+
var EMAIL_QUEUE_NAME;
|
|
101
|
+
var init_email_queue = __esm({
|
|
102
|
+
"src/plugins/email/email-queue.ts"() {
|
|
103
|
+
"use strict";
|
|
104
|
+
EMAIL_QUEUE_NAME = "email";
|
|
119
105
|
}
|
|
120
106
|
});
|
|
121
107
|
|
|
@@ -179,7 +165,7 @@ __export(razorpay_exports, {
|
|
|
179
165
|
RazorpayPaymentService: () => RazorpayPaymentService
|
|
180
166
|
});
|
|
181
167
|
import Razorpay from "razorpay";
|
|
182
|
-
import
|
|
168
|
+
import crypto2 from "crypto";
|
|
183
169
|
var RazorpayPaymentService;
|
|
184
170
|
var init_razorpay = __esm({
|
|
185
171
|
"src/plugins/payment/razorpay.ts"() {
|
|
@@ -220,8 +206,8 @@ var init_razorpay = __esm({
|
|
|
220
206
|
}
|
|
221
207
|
verifyWebhookSignature(payload, signature) {
|
|
222
208
|
const secret = this.webhookSecret || this.keySecret;
|
|
223
|
-
const expectedSignature =
|
|
224
|
-
return
|
|
209
|
+
const expectedSignature = crypto2.createHmac("sha256", secret).update(typeof payload === "string" ? payload : payload.toString("utf8")).digest("hex");
|
|
210
|
+
return crypto2.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
|
|
225
211
|
}
|
|
226
212
|
};
|
|
227
213
|
}
|
|
@@ -475,9 +461,569 @@ function erpPlugin(config) {
|
|
|
475
461
|
};
|
|
476
462
|
}
|
|
477
463
|
|
|
464
|
+
// src/plugins/email/email-service.ts
|
|
465
|
+
import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";
|
|
466
|
+
import nodemailer from "nodemailer";
|
|
467
|
+
|
|
468
|
+
// src/plugins/email/templates/layout.ts
|
|
469
|
+
function escapeHtml(s) {
|
|
470
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
471
|
+
}
|
|
472
|
+
function socialLinkInnerHtml(s) {
|
|
473
|
+
if (s.iconUrl?.trim()) {
|
|
474
|
+
return `<img src="${escapeHtml(s.iconUrl.trim())}" alt="" width="20" height="20" style="display:inline-block;vertical-align:middle;border:0;" />`;
|
|
475
|
+
}
|
|
476
|
+
const label = s.icon && s.icon.trim() || "\u{1F517}";
|
|
477
|
+
return escapeHtml(label);
|
|
478
|
+
}
|
|
479
|
+
function supportLinesHtml(supportEmail, supportPhone, opts) {
|
|
480
|
+
const parts = [];
|
|
481
|
+
const top = opts?.tightTop ? "margin-top:0;" : "margin-top:4px;";
|
|
482
|
+
if (supportEmail) {
|
|
483
|
+
parts.push(`<div style="font-size:13px;color:#444;${top}">📧 ${escapeHtml(supportEmail)}</div>`);
|
|
484
|
+
}
|
|
485
|
+
if (supportPhone) {
|
|
486
|
+
parts.push(`<div style="font-size:13px;color:#444;margin-top:2px;">📞 ${escapeHtml(supportPhone)}</div>`);
|
|
487
|
+
}
|
|
488
|
+
return parts.join("");
|
|
489
|
+
}
|
|
490
|
+
function renderLayout(options) {
|
|
491
|
+
const { bodyHtml, companyDetails } = options;
|
|
492
|
+
const {
|
|
493
|
+
logoUrl,
|
|
494
|
+
companyName,
|
|
495
|
+
supportEmail,
|
|
496
|
+
supportPhone,
|
|
497
|
+
socialLinks,
|
|
498
|
+
footerDisclaimer,
|
|
499
|
+
followUsTitle = "Follow Us"
|
|
500
|
+
} = companyDetails;
|
|
501
|
+
const supportFooterHtml = supportLinesHtml(supportEmail, supportPhone, { tightTop: true });
|
|
502
|
+
const hasSocial = Boolean(socialLinks?.length);
|
|
503
|
+
const brandFooterInner = [];
|
|
504
|
+
if (logoUrl) {
|
|
505
|
+
brandFooterInner.push(
|
|
506
|
+
`<img src="${escapeHtml(logoUrl)}" alt="" width="36" style="max-height:28px;display:inline-block;vertical-align:middle;margin-right:10px;border:0;" />`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
if (companyName) {
|
|
510
|
+
brandFooterInner.push(
|
|
511
|
+
`<span style="font-size:14px;font-weight:600;color:#111;vertical-align:middle;">${escapeHtml(companyName)}</span>`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
const brandFooterHtml = brandFooterInner.length ? `<div style="line-height:1.4;">${brandFooterInner.join("")}</div>` : "";
|
|
515
|
+
const followTitleHtml = hasSocial && followUsTitle ? `<div class="footer-follow-title" style="font-size:13px;font-weight:600;color:#555;">${escapeHtml(followUsTitle)}</div>` : "";
|
|
516
|
+
const socialRowHtml = hasSocial && socialLinks ? `<div class="footer-social-icons" style="margin-top:8px;text-align:right;font-size:0;line-height:0;">${socialLinks.map(
|
|
517
|
+
(s) => `<a href="${escapeHtml(s.url)}" style="display:inline-block;margin-left:12px;text-decoration:none;color:#333;font-size:18px;line-height:1;vertical-align:middle;">${socialLinkInnerHtml(s)}</a>`
|
|
518
|
+
).join("")}</div>` : "";
|
|
519
|
+
const rowSupport = supportFooterHtml ? `<tr><td class="footer-support-cell" colspan="2" valign="top" style="padding:0 0 10px 0;">${supportFooterHtml}</td></tr>` : "";
|
|
520
|
+
let footerBlock = "";
|
|
521
|
+
if (brandFooterHtml || followTitleHtml || socialRowHtml || footerDisclaimer || rowSupport) {
|
|
522
|
+
let rowBrandFollow = "";
|
|
523
|
+
if (brandFooterHtml && followTitleHtml) {
|
|
524
|
+
rowBrandFollow = `<tr>
|
|
525
|
+
<td class="footer-brand-cell" valign="top" style="padding:0 0 10px 0;vertical-align:top;">${brandFooterHtml}</td>
|
|
526
|
+
<td class="footer-follow-cell" valign="top" style="padding:0 0 10px 0;vertical-align:top;text-align:right;">${followTitleHtml}</td>
|
|
527
|
+
</tr>`;
|
|
528
|
+
} else if (brandFooterHtml) {
|
|
529
|
+
rowBrandFollow = `<tr><td class="footer-brand-cell" colspan="2" valign="top" style="padding:0 0 10px 0;">${brandFooterHtml}</td></tr>`;
|
|
530
|
+
} else if (followTitleHtml) {
|
|
531
|
+
rowBrandFollow = `<tr><td class="footer-follow-cell" colspan="2" valign="top" style="padding:0 0 10px 0;text-align:right;">${followTitleHtml}</td></tr>`;
|
|
532
|
+
}
|
|
533
|
+
const rowSocial = socialRowHtml ? `<tr><td class="footer-social-cell" colspan="2" style="padding:0;text-align:right;">${socialRowHtml}</td></tr>` : "";
|
|
534
|
+
footerBlock = `<table role="presentation" class="footer-main" width="100%" cellpadding="0" cellspacing="0" style="margin-top:28px;padding-top:16px;">${rowBrandFollow}${rowSupport}${rowSocial}</table>`;
|
|
535
|
+
}
|
|
536
|
+
const disclaimerBlock = footerDisclaimer ? `<div style="margin-top:20px;padding-top:12px;font-size:11px;line-height:1.5;color:#888;">${escapeHtml(footerDisclaimer).replace(/\n/g, "<br/>")}</div>` : "";
|
|
537
|
+
const responsiveCss = `
|
|
538
|
+
@media only screen and (max-width: 600px) {
|
|
539
|
+
.email-wrap { padding-left: 12px !important; padding-right: 12px !important; }
|
|
540
|
+
.footer-brand-cell, .footer-follow-cell { display: block !important; width: 100% !important; box-sizing: border-box; }
|
|
541
|
+
.footer-follow-cell { text-align: left !important; padding-top: 12px !important; padding-bottom: 0 !important; }
|
|
542
|
+
.footer-social-cell { text-align: left !important; }
|
|
543
|
+
.footer-social-icons { text-align: left !important; }
|
|
544
|
+
.footer-social-icons a:first-child { margin-left: 0 !important; }
|
|
545
|
+
}
|
|
546
|
+
`;
|
|
547
|
+
return `<!DOCTYPE html>
|
|
548
|
+
<html>
|
|
549
|
+
<head>
|
|
550
|
+
<meta charset="utf-8">
|
|
551
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
552
|
+
<style type="text/css">${responsiveCss}</style>
|
|
553
|
+
</head>
|
|
554
|
+
<body style="margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;font-size:14px;line-height:1.5;color:#333;background:#fff;">
|
|
555
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#fff;">
|
|
556
|
+
<tr><td class="email-wrap" style="padding:16px;">
|
|
557
|
+
<div>
|
|
558
|
+
${bodyHtml}
|
|
559
|
+
</div>
|
|
560
|
+
${footerBlock}
|
|
561
|
+
${disclaimerBlock}
|
|
562
|
+
</td></tr>
|
|
563
|
+
</table>
|
|
564
|
+
</body>
|
|
565
|
+
</html>`;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/plugins/email/templates/types.ts
|
|
569
|
+
function normalizeSocialLinkItem(o) {
|
|
570
|
+
const url = String(o.url ?? "").trim();
|
|
571
|
+
if (!url) return null;
|
|
572
|
+
let iconUrl = String(o.iconUrl ?? o.icon_image ?? "").trim();
|
|
573
|
+
let icon = String(o.icon ?? "").trim();
|
|
574
|
+
if (!iconUrl && /^https?:\/\//i.test(icon)) {
|
|
575
|
+
iconUrl = icon;
|
|
576
|
+
icon = "";
|
|
577
|
+
}
|
|
578
|
+
const item = { url };
|
|
579
|
+
if (iconUrl) item.iconUrl = iconUrl;
|
|
580
|
+
if (icon) item.icon = icon;
|
|
581
|
+
return item;
|
|
582
|
+
}
|
|
583
|
+
function parseSocialLinksJson(raw) {
|
|
584
|
+
if (raw == null || raw.trim() === "") return void 0;
|
|
585
|
+
try {
|
|
586
|
+
const parsed = JSON.parse(raw);
|
|
587
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
588
|
+
const out = [];
|
|
589
|
+
for (const item of parsed) {
|
|
590
|
+
if (item && typeof item === "object" && "url" in item) {
|
|
591
|
+
const n = normalizeSocialLinkItem(item);
|
|
592
|
+
if (n) out.push(n);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return out.length ? out : void 0;
|
|
596
|
+
} catch {
|
|
597
|
+
return void 0;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function mergeEmailLayoutCompanyDetails(branding, emailSettings) {
|
|
601
|
+
const fromBranding = getCompanyDetailsFromSettings(branding);
|
|
602
|
+
const pick = (emailVal, fallback) => {
|
|
603
|
+
const t = emailVal?.trim();
|
|
604
|
+
return t || fallback?.trim() || void 0;
|
|
605
|
+
};
|
|
606
|
+
const logoUrl = pick(emailSettings.logoUrl ?? emailSettings.emailLogoUrl, fromBranding.logoUrl);
|
|
607
|
+
const companyName = pick(emailSettings.companyName ?? emailSettings.emailCompanyName, fromBranding.companyName);
|
|
608
|
+
const supportEmail = pick(emailSettings.supportEmail ?? emailSettings.emailSupportEmail, fromBranding.supportEmail);
|
|
609
|
+
const supportPhone = pick(emailSettings.supportPhone, void 0);
|
|
610
|
+
const footerDisclaimer = pick(emailSettings.footerDisclaimer, void 0);
|
|
611
|
+
const followUsTitle = pick(emailSettings.followUsTitle, "Follow Us") || "Follow Us";
|
|
612
|
+
const socialFromEmail = parseSocialLinksJson(emailSettings.socialLinks);
|
|
613
|
+
const socialLinks = socialFromEmail?.length ? socialFromEmail : fromBranding.socialLinks;
|
|
614
|
+
return {
|
|
615
|
+
logoUrl,
|
|
616
|
+
companyName,
|
|
617
|
+
supportEmail,
|
|
618
|
+
supportPhone,
|
|
619
|
+
socialLinks,
|
|
620
|
+
footerDisclaimer,
|
|
621
|
+
followUsTitle
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function getCompanyDetailsFromSettings(settingsGroup) {
|
|
625
|
+
const logoUrl = settingsGroup.logo ?? settingsGroup.logoUrl ?? "";
|
|
626
|
+
const companyName = settingsGroup.companyName ?? settingsGroup.company_name ?? "";
|
|
627
|
+
const supportEmail = settingsGroup.supportEmail ?? settingsGroup.support_email ?? "";
|
|
628
|
+
let socialLinks = [];
|
|
629
|
+
const raw = settingsGroup.socialLinks ?? settingsGroup.social_links;
|
|
630
|
+
if (typeof raw === "string") {
|
|
631
|
+
try {
|
|
632
|
+
const arr = JSON.parse(raw);
|
|
633
|
+
if (Array.isArray(arr)) {
|
|
634
|
+
for (const item of arr) {
|
|
635
|
+
if (item && typeof item === "object") {
|
|
636
|
+
const n = normalizeSocialLinkItem(item);
|
|
637
|
+
if (n) socialLinks.push(n);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
} catch {
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return { logoUrl: logoUrl || void 0, companyName: companyName || void 0, supportEmail: supportEmail || void 0, socialLinks: socialLinks.length ? socialLinks : void 0 };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/plugins/email/templates/inline-cta.ts
|
|
648
|
+
function escapeHtml2(s) {
|
|
649
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
650
|
+
}
|
|
651
|
+
function escapeAttr(s) {
|
|
652
|
+
return escapeHtml2(s);
|
|
653
|
+
}
|
|
654
|
+
function primaryCtaButton(href, label) {
|
|
655
|
+
return `<table role="presentation" cellpadding="0" cellspacing="0" style="margin:20px 0;">
|
|
656
|
+
<tr><td style="border-radius:6px;background:#1a1a1a;">
|
|
657
|
+
<a href="${escapeAttr(href)}" style="display:inline-block;padding:12px 22px;font-size:14px;font-weight:600;color:#ffffff;text-decoration:none;">${escapeHtml2(label)}</a>
|
|
658
|
+
</td></tr>
|
|
659
|
+
</table>`;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/plugins/email/templates/signup.ts
|
|
663
|
+
function render(ctx) {
|
|
664
|
+
const { name, loginUrl, verifyEmailUrl, companyDetails } = ctx;
|
|
665
|
+
if (verifyEmailUrl) {
|
|
666
|
+
const subject2 = "Confirm your email";
|
|
667
|
+
const greeting = name && name.trim() ? `Hi ${escapeHtml2(name.trim())},` : "Hi,";
|
|
668
|
+
const bodyHtml2 = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">${greeting}</p>
|
|
669
|
+
<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">Thanks for signing up. Please confirm your email address to activate your account. Until you confirm, you won\u2019t be able to sign in.</p>
|
|
670
|
+
${primaryCtaButton(verifyEmailUrl, "Confirm email address")}
|
|
671
|
+
<p style="margin:16px 0 0 0;font-size:12px;line-height:1.5;color:#888;">This link expires in a few days. If you didn\u2019t create an account, you can ignore this message.<br/><span style="word-break:break-all;">${escapeHtml2(verifyEmailUrl)}</span></p>`;
|
|
672
|
+
const text2 = [
|
|
673
|
+
greeting,
|
|
674
|
+
"",
|
|
675
|
+
"Confirm your email to activate your account:",
|
|
676
|
+
verifyEmailUrl,
|
|
677
|
+
"",
|
|
678
|
+
"You cannot sign in until your email is confirmed."
|
|
679
|
+
].join("\n");
|
|
680
|
+
const html2 = renderLayout({ bodyHtml: bodyHtml2, companyDetails });
|
|
681
|
+
return { subject: subject2, html: html2, text: text2 };
|
|
682
|
+
}
|
|
683
|
+
const subject = "Welcome";
|
|
684
|
+
const bodyHtml = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">Welcome${name ? `, ${escapeHtml2(name)}` : ""}.</p><p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">Your account has been created.</p>${loginUrl ? `${primaryCtaButton(loginUrl, "Sign in")}` : ""}`;
|
|
685
|
+
const text = `Welcome${name ? `, ${name}` : ""}. Your account has been created.${loginUrl ? ` Sign in: ${loginUrl}` : ""}`;
|
|
686
|
+
const html = renderLayout({ bodyHtml, companyDetails });
|
|
687
|
+
return { subject, html, text };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/plugins/email/templates/passwordReset.ts
|
|
691
|
+
function render2(ctx) {
|
|
692
|
+
const { resetLink, companyDetails } = ctx;
|
|
693
|
+
const subject = "Reset your password";
|
|
694
|
+
const bodyHtml = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">Hello,</p>
|
|
695
|
+
<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">We received a request to reset the password for your account. If you made this request, use the button below to choose a new password.</p>
|
|
696
|
+
<p style="margin:0 0 8px 0;font-size:14px;line-height:1.5;color:#555;">For your security, this link will stop working after a short time. If it expires, request a new reset from the sign-in page.</p>
|
|
697
|
+
${primaryCtaButton(resetLink, "Reset password")}
|
|
698
|
+
<p style="margin:16px 0 0 0;font-size:12px;line-height:1.5;color:#888;">If you did not request a password reset, you can safely ignore this email\u2014your password will stay the same.</p>
|
|
699
|
+
<p style="margin:12px 0 0 0;font-size:12px;line-height:1.5;color:#888;">If the button does not work, copy and paste this URL into your browser:<br/><span style="word-break:break-all;">${escapeHtml2(resetLink)}</span></p>`;
|
|
700
|
+
const text = [
|
|
701
|
+
"Hello,",
|
|
702
|
+
"",
|
|
703
|
+
"We received a request to reset your password. Open the link below to set a new password:",
|
|
704
|
+
resetLink,
|
|
705
|
+
"",
|
|
706
|
+
"This link expires after a limited time.",
|
|
707
|
+
"",
|
|
708
|
+
"If you did not request this, you can ignore this email."
|
|
709
|
+
].join("\n");
|
|
710
|
+
const html = renderLayout({ bodyHtml, companyDetails });
|
|
711
|
+
return { subject, html, text };
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// src/plugins/email/templates/passwordChange.ts
|
|
715
|
+
function render3(ctx) {
|
|
716
|
+
const { name, companyDetails } = ctx;
|
|
717
|
+
const subject = "Password changed";
|
|
718
|
+
const bodyHtml = `<h2>Password changed</h2><p>Your password has been updated successfully${name ? `, ${escapeHtml3(name)}` : ""}.</p>`;
|
|
719
|
+
const text = `Your password has been updated successfully${name ? `, ${name}` : ""}.`;
|
|
720
|
+
const html = renderLayout({ bodyHtml, companyDetails });
|
|
721
|
+
return { subject, html, text };
|
|
722
|
+
}
|
|
723
|
+
function escapeHtml3(s) {
|
|
724
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/plugins/email/templates/orderPlaced.ts
|
|
728
|
+
function formatMoney(amount, currency) {
|
|
729
|
+
const v = typeof amount === "string" ? parseFloat(amount) : amount;
|
|
730
|
+
const num = Number.isFinite(v) ? Number(v).toFixed(2) : String(amount);
|
|
731
|
+
return currency ? `${num} ${currency}` : num;
|
|
732
|
+
}
|
|
733
|
+
function renderLineItemsHtml(items, currency) {
|
|
734
|
+
if (!items.length) {
|
|
735
|
+
return '<p style="margin:12px 0 0 0;font-size:14px;color:#666;">No line items.</p>';
|
|
736
|
+
}
|
|
737
|
+
const rows = items.map((it) => {
|
|
738
|
+
const name = escapeHtml2(it.productName);
|
|
739
|
+
const sku = it.sku && String(it.sku).trim() ? `<span style="font-size:12px;color:#888;"> (${escapeHtml2(String(it.sku).trim())})</span>` : "";
|
|
740
|
+
return `<tr>
|
|
741
|
+
<td style="padding:10px 8px 10px 0;border-bottom:1px solid #eee;vertical-align:top;font-size:14px;color:#333;">${name}${sku}</td>
|
|
742
|
+
<td align="right" style="padding:10px 8px;border-bottom:1px solid #eee;vertical-align:top;font-size:14px;color:#333;white-space:nowrap;">${escapeHtml2(String(it.quantity))}</td>
|
|
743
|
+
<td align="right" style="padding:10px 8px;border-bottom:1px solid #eee;vertical-align:top;font-size:14px;color:#333;white-space:nowrap;">${escapeHtml2(formatMoney(it.unitPrice, currency))}</td>
|
|
744
|
+
<td align="right" style="padding:10px 0 10px 8px;border-bottom:1px solid #eee;vertical-align:top;font-size:14px;color:#333;white-space:nowrap;">${escapeHtml2(formatMoney(it.lineTotal, currency))}</td>
|
|
745
|
+
</tr>`;
|
|
746
|
+
}).join("");
|
|
747
|
+
return `<p style="margin:16px 0 8px 0;font-size:14px;font-weight:600;color:#111;">Order items</p>
|
|
748
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;">
|
|
749
|
+
<tr>
|
|
750
|
+
<td style="padding:0 8px 8px 0;font-size:12px;font-weight:600;color:#555;text-transform:uppercase;letter-spacing:0.02em;">Item</td>
|
|
751
|
+
<td align="right" style="padding:0 8px 8px 8px;font-size:12px;font-weight:600;color:#555;text-transform:uppercase;">Qty</td>
|
|
752
|
+
<td align="right" style="padding:0 8px 8px 8px;font-size:12px;font-weight:600;color:#555;text-transform:uppercase;">Price</td>
|
|
753
|
+
<td align="right" style="padding:0 0 8px 8px;font-size:12px;font-weight:600;color:#555;text-transform:uppercase;">Total</td>
|
|
754
|
+
</tr>
|
|
755
|
+
${rows}
|
|
756
|
+
</table>`;
|
|
757
|
+
}
|
|
758
|
+
function renderLineItemsText(items, currency) {
|
|
759
|
+
if (!items.length) return "";
|
|
760
|
+
const lines = items.map(
|
|
761
|
+
(it) => `- ${it.productName} \xD7 ${it.quantity} @ ${formatMoney(it.unitPrice, currency)} = ${formatMoney(it.lineTotal, currency)}${it.sku ? ` [${it.sku}]` : ""}`
|
|
762
|
+
);
|
|
763
|
+
return ["Items:", ...lines].join("\n");
|
|
764
|
+
}
|
|
765
|
+
function render4(ctx) {
|
|
766
|
+
const {
|
|
767
|
+
orderNumber: orderNumber2,
|
|
768
|
+
total,
|
|
769
|
+
currency,
|
|
770
|
+
customerName,
|
|
771
|
+
companyDetails,
|
|
772
|
+
audience = "customer",
|
|
773
|
+
internalCustomerEmail,
|
|
774
|
+
lineItems = []
|
|
775
|
+
} = ctx;
|
|
776
|
+
const itemsHtml = renderLineItemsHtml(lineItems, currency);
|
|
777
|
+
const itemsText = renderLineItemsText(lineItems, currency);
|
|
778
|
+
const totalLine = total != null && String(total).trim() !== "" ? `<p style="margin:12px 0 0 0;font-size:15px;line-height:1.5;color:#333;"><strong>Order total:</strong> ${escapeHtml2(String(total))}${currency ? ` ${escapeHtml2(currency)}` : ""}</p>` : "";
|
|
779
|
+
let subject;
|
|
780
|
+
let bodyHtml;
|
|
781
|
+
let text;
|
|
782
|
+
if (audience === "sales") {
|
|
783
|
+
subject = `New order #${orderNumber2}`;
|
|
784
|
+
const who = customerName || internalCustomerEmail ? `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;"><strong>Customer:</strong> ${escapeHtml2(customerName || "\u2014")}${internalCustomerEmail ? ` <span style="color:#555;">(${escapeHtml2(internalCustomerEmail)})</span>` : ""}</p>` : "";
|
|
785
|
+
bodyHtml = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">A new order has been placed and payment completed.</p>
|
|
786
|
+
<p style="margin:0 0 8px 0;font-size:15px;line-height:1.5;color:#333;"><strong>Order number:</strong> ${escapeHtml2(orderNumber2)}</p>
|
|
787
|
+
${who}
|
|
788
|
+
${itemsHtml}
|
|
789
|
+
${totalLine}`;
|
|
790
|
+
text = [
|
|
791
|
+
`New order #${orderNumber2}`,
|
|
792
|
+
customerName ? `Customer: ${customerName}` : "",
|
|
793
|
+
internalCustomerEmail ? `Email: ${internalCustomerEmail}` : "",
|
|
794
|
+
itemsText,
|
|
795
|
+
total != null ? `Order total: ${total}${currency ? ` ${currency}` : ""}` : ""
|
|
796
|
+
].filter(Boolean).join("\n\n");
|
|
797
|
+
} else {
|
|
798
|
+
subject = `Order confirmed #${orderNumber2}`;
|
|
799
|
+
const thanksPlain = customerName && customerName.trim() ? `Thank you, ${customerName.trim()}.` : "Thank you for your order.";
|
|
800
|
+
const thanksHtml = `${escapeHtml2(thanksPlain)} We\u2019ve received your order and will process it shortly.`;
|
|
801
|
+
bodyHtml = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">${thanksHtml}</p>
|
|
802
|
+
<p style="margin:0 0 8px 0;font-size:15px;line-height:1.5;color:#333;"><strong>Order number:</strong> ${escapeHtml2(orderNumber2)}</p>
|
|
803
|
+
${itemsHtml}
|
|
804
|
+
${totalLine}
|
|
805
|
+
<p style="margin:16px 0 0 0;font-size:13px;line-height:1.5;color:#666;">If you have questions, reply to this email or contact us using the details below.</p>`;
|
|
806
|
+
text = [
|
|
807
|
+
`Order confirmed #${orderNumber2}`,
|
|
808
|
+
`${thanksPlain} We\u2019ve received your order and will process it shortly.`,
|
|
809
|
+
"",
|
|
810
|
+
itemsText,
|
|
811
|
+
total != null ? `Order total: ${total}${currency ? ` ${currency}` : ""}` : "",
|
|
812
|
+
"",
|
|
813
|
+
"We will process your order shortly."
|
|
814
|
+
].filter((line, i, arr) => !(line === "" && arr[i - 1] === "")).join("\n");
|
|
815
|
+
}
|
|
816
|
+
const html = renderLayout({ bodyHtml, companyDetails });
|
|
817
|
+
return { subject, html, text };
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/plugins/email/templates/returnInitiated.ts
|
|
821
|
+
function render5(ctx) {
|
|
822
|
+
const { returnId, orderNumber: orderNumber2, companyDetails } = ctx;
|
|
823
|
+
const subject = "Return initiated";
|
|
824
|
+
const bodyHtml = `<h2>Return initiated</h2><p>Your return request has been received.</p>${orderNumber2 ? `<p><strong>Order:</strong> ${escapeHtml4(orderNumber2)}</p>` : ""}${returnId ? `<p><strong>Return ID:</strong> ${escapeHtml4(returnId)}</p>` : ""}`;
|
|
825
|
+
const text = `Return initiated.${orderNumber2 ? ` Order: ${orderNumber2}` : ""}${returnId ? ` Return ID: ${returnId}` : ""}`;
|
|
826
|
+
const html = renderLayout({ bodyHtml, companyDetails });
|
|
827
|
+
return { subject, html, text };
|
|
828
|
+
}
|
|
829
|
+
function escapeHtml4(s) {
|
|
830
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/plugins/email/templates/shippingUpdate.ts
|
|
834
|
+
function render6(ctx) {
|
|
835
|
+
const { orderNumber: orderNumber2, status, trackingUrl, companyDetails } = ctx;
|
|
836
|
+
const subject = status ? `Shipping update: ${status}` : "Shipping update";
|
|
837
|
+
const bodyHtml = `<h2>Shipping update</h2>${status ? `<p><strong>Status:</strong> ${escapeHtml5(status)}</p>` : ""}${orderNumber2 ? `<p><strong>Order:</strong> ${escapeHtml5(orderNumber2)}</p>` : ""}${trackingUrl ? `<p><a href="${escapeHtml5(trackingUrl)}">Track your order</a></p>` : ""}`;
|
|
838
|
+
const text = `Shipping update.${status ? ` Status: ${status}` : ""}${orderNumber2 ? ` Order: ${orderNumber2}` : ""}${trackingUrl ? ` Track: ${trackingUrl}` : ""}`;
|
|
839
|
+
const html = renderLayout({ bodyHtml, companyDetails });
|
|
840
|
+
return { subject, html, text };
|
|
841
|
+
}
|
|
842
|
+
function escapeHtml5(s) {
|
|
843
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/plugins/email/templates/invite.ts
|
|
847
|
+
function render7(ctx) {
|
|
848
|
+
const { inviteLink, email, inviteeName, companyDetails } = ctx;
|
|
849
|
+
const subject = "You're invited";
|
|
850
|
+
const greeting = inviteeName && inviteeName.trim() ? `Hello ${escapeHtml2(inviteeName.trim())},` : "Hello,";
|
|
851
|
+
const bodyHtml = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">${greeting}</p>
|
|
852
|
+
<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">You have been invited to create your account for <strong>${escapeHtml2(email)}</strong>. Use the secure link below to choose your password and activate access.</p>
|
|
853
|
+
<p style="margin:0 0 8px 0;font-size:14px;line-height:1.5;color:#555;">This link is personal to you. If you did not expect this invitation, you can ignore this message.</p>
|
|
854
|
+
${primaryCtaButton(inviteLink, "Accept invitation & set password")}
|
|
855
|
+
<p style="margin:16px 0 0 0;font-size:12px;line-height:1.5;color:#888;">If the button does not work, copy and paste this URL into your browser:<br/><span style="word-break:break-all;">${escapeHtml2(inviteLink)}</span></p>`;
|
|
856
|
+
const text = [
|
|
857
|
+
inviteeName?.trim() ? `Hello ${inviteeName.trim()},` : "Hello,",
|
|
858
|
+
"",
|
|
859
|
+
`You have been invited to create your account (${email}).`,
|
|
860
|
+
"Open this link to set your password:",
|
|
861
|
+
inviteLink,
|
|
862
|
+
"",
|
|
863
|
+
"If you did not expect this invitation, you can ignore this email."
|
|
864
|
+
].join("\n");
|
|
865
|
+
const html = renderLayout({ bodyHtml, companyDetails });
|
|
866
|
+
return { subject, html, text };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// src/plugins/email/templates/formSubmission.ts
|
|
870
|
+
function render8(ctx) {
|
|
871
|
+
const { formName, contactName, contactEmail, formData, formFieldRows, companyDetails } = ctx;
|
|
872
|
+
const subject = `New Form Submission: ${formName}`;
|
|
873
|
+
const fieldsBlock = formFieldRows && formFieldRows.length > 0 ? formFieldRows.map(
|
|
874
|
+
(r) => `<p style="margin:10px 0 0 0;line-height:1.5;"><strong>${escapeHtml6(r.label)}</strong><br/>${escapeHtml6(r.value)}</p>`
|
|
875
|
+
).join("") : `<p style="margin:10px 0 0 0;font-size:13px;white-space:pre-wrap;">${escapeHtml6(JSON.stringify(formData, null, 2))}</p>`;
|
|
876
|
+
const bodyHtml = `<p style="margin:0 0 6px 0;font-size:18px;font-weight:600;color:#111;">New Form Submission</p>
|
|
877
|
+
<p style="margin:0 0 4px 0;"><strong>Form:</strong> ${escapeHtml6(formName)}</p>
|
|
878
|
+
<p style="margin:0 0 8px 0;"><strong>Contact:</strong> ${escapeHtml6(contactName)}${contactEmail ? ` (${escapeHtml6(contactEmail)})` : ""}</p>
|
|
879
|
+
${fieldsBlock}`;
|
|
880
|
+
const textLines = [
|
|
881
|
+
"New Form Submission",
|
|
882
|
+
`Form: ${formName}`,
|
|
883
|
+
`Contact: ${contactName}${contactEmail ? ` (${contactEmail})` : ""}`,
|
|
884
|
+
"",
|
|
885
|
+
...formFieldRows?.length ? formFieldRows.map((r) => `${r.label}: ${r.value}`) : [JSON.stringify(formData, null, 2)]
|
|
886
|
+
];
|
|
887
|
+
const text = textLines.join("\n");
|
|
888
|
+
const html = renderLayout({ bodyHtml, companyDetails });
|
|
889
|
+
return { subject, html, text };
|
|
890
|
+
}
|
|
891
|
+
function escapeHtml6(s) {
|
|
892
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/plugins/email/templates/index.ts
|
|
896
|
+
var templateRenderMap = {
|
|
897
|
+
signup: render,
|
|
898
|
+
passwordReset: render2,
|
|
899
|
+
passwordChange: render3,
|
|
900
|
+
orderPlaced: render4,
|
|
901
|
+
returnInitiated: render5,
|
|
902
|
+
shippingUpdate: render6,
|
|
903
|
+
invite: render7,
|
|
904
|
+
formSubmission: render8
|
|
905
|
+
};
|
|
906
|
+
function getTemplateRenderer(name) {
|
|
907
|
+
return templateRenderMap[name];
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// src/plugins/email/renderer.ts
|
|
911
|
+
function renderEmail(templateName, ctx, options) {
|
|
912
|
+
const layoutFn = options?.renderLayout ?? renderLayout;
|
|
913
|
+
const getBody = options?.getBody;
|
|
914
|
+
if (getBody) {
|
|
915
|
+
const custom = getBody(templateName, ctx);
|
|
916
|
+
if (custom != null) {
|
|
917
|
+
const html = layoutFn({ bodyHtml: custom.bodyHtml, companyDetails: ctx.companyDetails });
|
|
918
|
+
return { subject: custom.subject, html, text: custom.text };
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
const render9 = getTemplateRenderer(templateName);
|
|
922
|
+
if (!render9) {
|
|
923
|
+
throw new Error(`Unknown email template: ${templateName}`);
|
|
924
|
+
}
|
|
925
|
+
return render9(ctx);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// src/plugins/email/email-service.ts
|
|
929
|
+
var EmailService = class {
|
|
930
|
+
config;
|
|
931
|
+
templateOptions;
|
|
932
|
+
sesClient;
|
|
933
|
+
transporter;
|
|
934
|
+
constructor(config) {
|
|
935
|
+
this.config = config;
|
|
936
|
+
this.templateOptions = config.templateOptions;
|
|
937
|
+
if (config.type === "AWS") {
|
|
938
|
+
if (!config.region || !config.accessKeyId || !config.secretAccessKey) {
|
|
939
|
+
throw new Error("AWS SES configuration incomplete");
|
|
940
|
+
}
|
|
941
|
+
this.sesClient = new SESClient({
|
|
942
|
+
region: config.region,
|
|
943
|
+
credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey }
|
|
944
|
+
});
|
|
945
|
+
} else if (config.type === "SMTP" || config.type === "GMAIL") {
|
|
946
|
+
if (!config.user || !config.password) throw new Error("SMTP configuration incomplete");
|
|
947
|
+
const host = config.type === "GMAIL" ? "smtp.gmail.com" : config.host || void 0;
|
|
948
|
+
const port = config.port ?? 587;
|
|
949
|
+
const secure = config.secure ?? false;
|
|
950
|
+
this.transporter = nodemailer.createTransport({
|
|
951
|
+
...host ? { host } : {},
|
|
952
|
+
port,
|
|
953
|
+
secure,
|
|
954
|
+
auth: { user: config.user, pass: config.password }
|
|
955
|
+
});
|
|
956
|
+
} else {
|
|
957
|
+
throw new Error(`Unsupported email type: ${config.type}`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
async send(emailData) {
|
|
961
|
+
try {
|
|
962
|
+
if (this.config.type === "AWS" && this.sesClient) {
|
|
963
|
+
await this.sesClient.send(
|
|
964
|
+
new SendEmailCommand({
|
|
965
|
+
Source: emailData.from || this.config.from,
|
|
966
|
+
Destination: { ToAddresses: [emailData.to || this.config.to] },
|
|
967
|
+
Message: {
|
|
968
|
+
Subject: { Data: emailData.subject, Charset: "UTF-8" },
|
|
969
|
+
Body: {
|
|
970
|
+
Html: { Data: emailData.html, Charset: "UTF-8" },
|
|
971
|
+
...emailData.text && { Text: { Data: emailData.text, Charset: "UTF-8" } }
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
})
|
|
975
|
+
);
|
|
976
|
+
return true;
|
|
977
|
+
}
|
|
978
|
+
if ((this.config.type === "SMTP" || this.config.type === "GMAIL") && this.transporter) {
|
|
979
|
+
await this.transporter.sendMail({
|
|
980
|
+
from: emailData.from || this.config.from,
|
|
981
|
+
to: emailData.to || this.config.to,
|
|
982
|
+
subject: emailData.subject,
|
|
983
|
+
html: emailData.html,
|
|
984
|
+
text: emailData.text
|
|
985
|
+
});
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
return false;
|
|
989
|
+
} catch (error) {
|
|
990
|
+
console.error("Email sending failed:", error);
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
renderTemplate(templateName, ctx) {
|
|
995
|
+
return renderEmail(templateName, ctx, this.templateOptions);
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
var emailTemplates = {
|
|
999
|
+
formSubmission: (data) => ({
|
|
1000
|
+
subject: `New Form Submission: ${data.formName}`,
|
|
1001
|
+
html: `<h2>New Form Submission</h2><p><strong>Form:</strong> ${data.formName}</p><p><strong>Contact:</strong> ${data.contactName} (${data.contactEmail})</p><pre>${JSON.stringify(data.formData, null, 2)}</pre>`,
|
|
1002
|
+
text: `New Form Submission
|
|
1003
|
+
Form: ${data.formName}
|
|
1004
|
+
Contact: ${data.contactName} (${data.contactEmail})
|
|
1005
|
+
${JSON.stringify(data.formData, null, 2)}`
|
|
1006
|
+
}),
|
|
1007
|
+
contactSubmission: (data) => ({
|
|
1008
|
+
subject: `New Contact Form Submission from ${data.name}`,
|
|
1009
|
+
html: `<h2>New Contact Form Submission</h2><p><strong>Name:</strong> ${data.name}</p><p><strong>Email:</strong> ${data.email}</p>${data.phone ? `<p><strong>Phone:</strong> ${data.phone}</p>` : ""}${data.message ? `<p><strong>Message:</strong></p><p>${data.message}</p>` : ""}`,
|
|
1010
|
+
text: `New Contact Form Submission
|
|
1011
|
+
Name: ${data.name}
|
|
1012
|
+
Email: ${data.email}
|
|
1013
|
+
${data.phone ? `Phone: ${data.phone}
|
|
1014
|
+
` : ""}${data.message ? `Message: ${data.message}` : ""}`
|
|
1015
|
+
}),
|
|
1016
|
+
passwordReset: (data) => ({
|
|
1017
|
+
subject: "Reset your password",
|
|
1018
|
+
html: `<h2>Reset your password</h2><p>Click the link below to set a new password. This link expires in 1 hour.</p><p><a href="${data.resetLink}">${data.resetLink}</a></p>`,
|
|
1019
|
+
text: `Reset your password: ${data.resetLink}
|
|
1020
|
+
|
|
1021
|
+
This link expires in 1 hour.`
|
|
1022
|
+
})
|
|
1023
|
+
};
|
|
1024
|
+
|
|
478
1025
|
// src/plugins/email/index.ts
|
|
479
|
-
|
|
480
|
-
init_email_service();
|
|
1026
|
+
init_email_queue();
|
|
481
1027
|
function emailPlugin(config) {
|
|
482
1028
|
return {
|
|
483
1029
|
name: "email",
|
|
@@ -486,6 +1032,8 @@ function emailPlugin(config) {
|
|
|
486
1032
|
const from = config.from || context.config.SMTP_FROM || "no-reply@example.com";
|
|
487
1033
|
const to = config.to || context.config.SMTP_TO || "info@example.com";
|
|
488
1034
|
const type = config.type || context.config.SMTP_TYPE || "SMTP";
|
|
1035
|
+
const portEnv = context.config.SMTP_PORT;
|
|
1036
|
+
const portParsed = portEnv ? parseInt(portEnv, 10) : void 0;
|
|
489
1037
|
const merged = {
|
|
490
1038
|
...config,
|
|
491
1039
|
from,
|
|
@@ -493,6 +1041,9 @@ function emailPlugin(config) {
|
|
|
493
1041
|
type,
|
|
494
1042
|
user: config.user ?? context.config.SMTP_USER,
|
|
495
1043
|
password: config.password ?? context.config.SMTP_PASSWORD,
|
|
1044
|
+
host: config.host ?? context.config.SMTP_HOST,
|
|
1045
|
+
port: config.port ?? (Number.isFinite(portParsed) ? portParsed : void 0),
|
|
1046
|
+
secure: config.secure ?? context.config.SMTP_SECURE === "true",
|
|
496
1047
|
region: config.region ?? context.config.AWS_REGION,
|
|
497
1048
|
accessKeyId: config.accessKeyId ?? context.config.AWS_ACCESS_KEY_ID,
|
|
498
1049
|
secretAccessKey: config.secretAccessKey ?? context.config.AWS_SECRET_ACCESS_KEY
|
|
@@ -926,6 +1477,161 @@ function llmPlugin(config = {}) {
|
|
|
926
1477
|
};
|
|
927
1478
|
}
|
|
928
1479
|
|
|
1480
|
+
// src/plugins/cache/index.ts
|
|
1481
|
+
import Redis from "ioredis";
|
|
1482
|
+
|
|
1483
|
+
// src/plugins/cache/memory-cache.ts
|
|
1484
|
+
import { LRUCache } from "lru-cache";
|
|
1485
|
+
var DEFAULT_MAX = 500;
|
|
1486
|
+
var DEFAULT_TTL_MS = 60 * 1e3;
|
|
1487
|
+
var MemoryCache = class {
|
|
1488
|
+
cache;
|
|
1489
|
+
constructor(options) {
|
|
1490
|
+
const max = options?.max ?? DEFAULT_MAX;
|
|
1491
|
+
const ttlMs = options?.ttlMs ?? DEFAULT_TTL_MS;
|
|
1492
|
+
this.cache = new LRUCache({ max, ttl: ttlMs });
|
|
1493
|
+
}
|
|
1494
|
+
async get(key) {
|
|
1495
|
+
const value = this.cache.get(key);
|
|
1496
|
+
return value ?? null;
|
|
1497
|
+
}
|
|
1498
|
+
async set(key, value, ttlSeconds) {
|
|
1499
|
+
const ttlMs = ttlSeconds != null && ttlSeconds > 0 ? ttlSeconds * 1e3 : void 0;
|
|
1500
|
+
this.cache.set(key, value, { ttl: ttlMs });
|
|
1501
|
+
}
|
|
1502
|
+
async del(key) {
|
|
1503
|
+
this.cache.delete(key);
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
|
|
1507
|
+
// src/plugins/cache/redis-cache.ts
|
|
1508
|
+
var RedisCache = class {
|
|
1509
|
+
constructor(client) {
|
|
1510
|
+
this.client = client;
|
|
1511
|
+
}
|
|
1512
|
+
async get(key) {
|
|
1513
|
+
return this.client.get(key);
|
|
1514
|
+
}
|
|
1515
|
+
async set(key, value, ttlSeconds) {
|
|
1516
|
+
if (ttlSeconds != null && ttlSeconds > 0) {
|
|
1517
|
+
await this.client.set(key, value, "EX", ttlSeconds);
|
|
1518
|
+
} else {
|
|
1519
|
+
await this.client.set(key, value);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
async del(key) {
|
|
1523
|
+
await this.client.del(key);
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
// src/plugins/cache/index.ts
|
|
1528
|
+
function cachePlugin(config) {
|
|
1529
|
+
return {
|
|
1530
|
+
name: "cache",
|
|
1531
|
+
version: "1.0.0",
|
|
1532
|
+
async init(context) {
|
|
1533
|
+
const redisUrl = config?.redisUrl ?? context.config.REDIS_URL ?? "";
|
|
1534
|
+
const url = typeof redisUrl === "string" ? redisUrl.trim() : "";
|
|
1535
|
+
if (url) {
|
|
1536
|
+
const client = new Redis(url);
|
|
1537
|
+
return new RedisCache(client);
|
|
1538
|
+
}
|
|
1539
|
+
return new MemoryCache();
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// src/plugins/queue/bullmq-queue.ts
|
|
1545
|
+
import { Queue, Worker } from "bullmq";
|
|
1546
|
+
function connectionFromUrl(redisUrl) {
|
|
1547
|
+
const u = new URL(redisUrl);
|
|
1548
|
+
return {
|
|
1549
|
+
host: u.hostname,
|
|
1550
|
+
port: u.port ? parseInt(u.port, 10) : 6379,
|
|
1551
|
+
...u.username && { username: u.username },
|
|
1552
|
+
...u.password && { password: u.password },
|
|
1553
|
+
maxRetriesPerRequest: null
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
var BullMQQueue = class {
|
|
1557
|
+
connectionOptions;
|
|
1558
|
+
queues = /* @__PURE__ */ new Map();
|
|
1559
|
+
workers = /* @__PURE__ */ new Map();
|
|
1560
|
+
constructor(redisUrl) {
|
|
1561
|
+
this.connectionOptions = connectionFromUrl(redisUrl);
|
|
1562
|
+
}
|
|
1563
|
+
async add(queueName, data) {
|
|
1564
|
+
const queue = this.getOrCreateQueue(queueName);
|
|
1565
|
+
await queue.add(queueName, data);
|
|
1566
|
+
}
|
|
1567
|
+
registerProcessor(queueName, processor) {
|
|
1568
|
+
if (this.workers.has(queueName)) return;
|
|
1569
|
+
const worker = new Worker(
|
|
1570
|
+
queueName,
|
|
1571
|
+
async (job) => {
|
|
1572
|
+
await processor(job.data);
|
|
1573
|
+
},
|
|
1574
|
+
{ connection: this.connectionOptions }
|
|
1575
|
+
);
|
|
1576
|
+
this.workers.set(queueName, worker);
|
|
1577
|
+
}
|
|
1578
|
+
getOrCreateQueue(queueName) {
|
|
1579
|
+
let queue = this.queues.get(queueName);
|
|
1580
|
+
if (!queue) {
|
|
1581
|
+
queue = new Queue(queueName, { connection: this.connectionOptions });
|
|
1582
|
+
this.queues.set(queueName, queue);
|
|
1583
|
+
}
|
|
1584
|
+
return queue;
|
|
1585
|
+
}
|
|
1586
|
+
async destroy() {
|
|
1587
|
+
for (const w of this.workers.values()) await w.close();
|
|
1588
|
+
this.workers.clear();
|
|
1589
|
+
for (const q of this.queues.values()) await q.close();
|
|
1590
|
+
this.queues.clear();
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1594
|
+
// src/plugins/queue/memory-queue.ts
|
|
1595
|
+
import fastq from "fastq";
|
|
1596
|
+
var MemoryQueue = class {
|
|
1597
|
+
queues = /* @__PURE__ */ new Map();
|
|
1598
|
+
async add(queueName, data) {
|
|
1599
|
+
const q = this.queues.get(queueName);
|
|
1600
|
+
if (!q) throw new Error(`No processor registered for queue: ${queueName}`);
|
|
1601
|
+
void Promise.resolve(q.push(data)).catch(() => void 0);
|
|
1602
|
+
}
|
|
1603
|
+
registerProcessor(queueName, processor) {
|
|
1604
|
+
if (this.queues.has(queueName)) return;
|
|
1605
|
+
const worker = async (task) => processor(task);
|
|
1606
|
+
const q = fastq.promise(worker, 1);
|
|
1607
|
+
this.queues.set(queueName, q);
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1611
|
+
// src/plugins/queue/index.ts
|
|
1612
|
+
function queuePlugin(config) {
|
|
1613
|
+
let instance;
|
|
1614
|
+
return {
|
|
1615
|
+
name: "queue",
|
|
1616
|
+
version: "1.0.0",
|
|
1617
|
+
async init(context) {
|
|
1618
|
+
const redisUrl = config?.redisUrl ?? context.config.REDIS_URL ?? "";
|
|
1619
|
+
const url = typeof redisUrl === "string" ? redisUrl.trim() : "";
|
|
1620
|
+
if (url) {
|
|
1621
|
+
instance = new BullMQQueue(url);
|
|
1622
|
+
return instance;
|
|
1623
|
+
}
|
|
1624
|
+
instance = new MemoryQueue();
|
|
1625
|
+
return instance;
|
|
1626
|
+
},
|
|
1627
|
+
async destroy() {
|
|
1628
|
+
if (instance && "destroy" in instance && typeof instance.destroy === "function") {
|
|
1629
|
+
await instance.destroy();
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
|
|
929
1635
|
// src/lib/utils.ts
|
|
930
1636
|
import { clsx } from "clsx";
|
|
931
1637
|
import { twMerge } from "tailwind-merge";
|
|
@@ -962,6 +1668,39 @@ function truncateText(text, maxLength) {
|
|
|
962
1668
|
return text.substring(0, maxLength).trim() + "...";
|
|
963
1669
|
}
|
|
964
1670
|
|
|
1671
|
+
// src/lib/link-contact-to-user.ts
|
|
1672
|
+
import { IsNull } from "typeorm";
|
|
1673
|
+
async function linkUnclaimedContactToUser(dataSource, contactsEntity, userId, email) {
|
|
1674
|
+
const repo = dataSource.getRepository(contactsEntity);
|
|
1675
|
+
const found = await repo.findOne({
|
|
1676
|
+
where: { email, userId: IsNull(), deleted: false }
|
|
1677
|
+
});
|
|
1678
|
+
if (found) await repo.update(found.id, { userId });
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// src/lib/email-recipients.ts
|
|
1682
|
+
function parseEmailRecipientsFromConfig(raw) {
|
|
1683
|
+
if (raw == null || raw === "") return [];
|
|
1684
|
+
const trimmed = raw.trim();
|
|
1685
|
+
if (trimmed.startsWith("[")) {
|
|
1686
|
+
try {
|
|
1687
|
+
const parsed = JSON.parse(trimmed);
|
|
1688
|
+
if (Array.isArray(parsed)) {
|
|
1689
|
+
return parsed.map((e) => String(e).trim()).filter(Boolean);
|
|
1690
|
+
}
|
|
1691
|
+
} catch {
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
return trimmed.split(/[,;]+/).map((s) => s.trim()).filter(Boolean);
|
|
1695
|
+
}
|
|
1696
|
+
function serializeEmailRecipients(emails) {
|
|
1697
|
+
return JSON.stringify(emails);
|
|
1698
|
+
}
|
|
1699
|
+
function joinRecipientsForSend(emails) {
|
|
1700
|
+
if (!emails.length) return null;
|
|
1701
|
+
return emails.join(", ");
|
|
1702
|
+
}
|
|
1703
|
+
|
|
965
1704
|
// src/entities/user.entity.ts
|
|
966
1705
|
import { Entity as Entity3, PrimaryGeneratedColumn as PrimaryGeneratedColumn3, Column as Column3, ManyToOne as ManyToOne2, JoinColumn as JoinColumn2 } from "typeorm";
|
|
967
1706
|
|
|
@@ -1095,6 +1834,7 @@ var User = class {
|
|
|
1095
1834
|
email;
|
|
1096
1835
|
password;
|
|
1097
1836
|
blocked;
|
|
1837
|
+
adminAccess;
|
|
1098
1838
|
groupId;
|
|
1099
1839
|
createdAt;
|
|
1100
1840
|
updatedAt;
|
|
@@ -1120,6 +1860,9 @@ __decorateClass([
|
|
|
1120
1860
|
__decorateClass([
|
|
1121
1861
|
Column3("boolean", { default: false })
|
|
1122
1862
|
], User.prototype, "blocked", 2);
|
|
1863
|
+
__decorateClass([
|
|
1864
|
+
Column3("boolean", { default: false })
|
|
1865
|
+
], User.prototype, "adminAccess", 2);
|
|
1123
1866
|
__decorateClass([
|
|
1124
1867
|
Column3("int", { nullable: true })
|
|
1125
1868
|
], User.prototype, "groupId", 2);
|
|
@@ -1522,7 +2265,7 @@ Blog = __decorateClass([
|
|
|
1522
2265
|
], Blog);
|
|
1523
2266
|
|
|
1524
2267
|
// src/entities/contact.entity.ts
|
|
1525
|
-
import { Entity as Entity18, PrimaryGeneratedColumn as PrimaryGeneratedColumn18, Column as Column18, OneToMany as OneToMany8 } from "typeorm";
|
|
2268
|
+
import { Entity as Entity18, PrimaryGeneratedColumn as PrimaryGeneratedColumn18, Column as Column18, OneToMany as OneToMany8, ManyToOne as ManyToOne12, JoinColumn as JoinColumn12 } from "typeorm";
|
|
1526
2269
|
|
|
1527
2270
|
// src/entities/form-submission.entity.ts
|
|
1528
2271
|
import { Entity as Entity12, PrimaryGeneratedColumn as PrimaryGeneratedColumn12, Column as Column12, ManyToOne as ManyToOne6, JoinColumn as JoinColumn6 } from "typeorm";
|
|
@@ -2066,6 +2809,8 @@ var Contact = class {
|
|
|
2066
2809
|
createdBy;
|
|
2067
2810
|
updatedBy;
|
|
2068
2811
|
deletedBy;
|
|
2812
|
+
userId;
|
|
2813
|
+
user;
|
|
2069
2814
|
form_submissions;
|
|
2070
2815
|
addresses;
|
|
2071
2816
|
orders;
|
|
@@ -2117,6 +2862,13 @@ __decorateClass([
|
|
|
2117
2862
|
__decorateClass([
|
|
2118
2863
|
Column18("int", { nullable: true })
|
|
2119
2864
|
], Contact.prototype, "deletedBy", 2);
|
|
2865
|
+
__decorateClass([
|
|
2866
|
+
Column18("int", { nullable: true })
|
|
2867
|
+
], Contact.prototype, "userId", 2);
|
|
2868
|
+
__decorateClass([
|
|
2869
|
+
ManyToOne12(() => User, { onDelete: "SET NULL" }),
|
|
2870
|
+
JoinColumn12({ name: "userId" })
|
|
2871
|
+
], Contact.prototype, "user", 2);
|
|
2120
2872
|
__decorateClass([
|
|
2121
2873
|
OneToMany8(() => FormSubmission, (fs) => fs.contact)
|
|
2122
2874
|
], Contact.prototype, "form_submissions", 2);
|
|
@@ -2250,7 +3002,7 @@ Media = __decorateClass([
|
|
|
2250
3002
|
], Media);
|
|
2251
3003
|
|
|
2252
3004
|
// src/entities/page.entity.ts
|
|
2253
|
-
import { Entity as Entity21, PrimaryGeneratedColumn as PrimaryGeneratedColumn21, Column as Column21, ManyToOne as
|
|
3005
|
+
import { Entity as Entity21, PrimaryGeneratedColumn as PrimaryGeneratedColumn21, Column as Column21, ManyToOne as ManyToOne13, JoinColumn as JoinColumn13 } from "typeorm";
|
|
2254
3006
|
var Page = class {
|
|
2255
3007
|
id;
|
|
2256
3008
|
title;
|
|
@@ -2292,15 +3044,15 @@ __decorateClass([
|
|
|
2292
3044
|
Column21("int", { nullable: true })
|
|
2293
3045
|
], Page.prototype, "parentId", 2);
|
|
2294
3046
|
__decorateClass([
|
|
2295
|
-
|
|
2296
|
-
|
|
3047
|
+
ManyToOne13(() => Page, { onDelete: "SET NULL" }),
|
|
3048
|
+
JoinColumn13({ name: "parentId" })
|
|
2297
3049
|
], Page.prototype, "parent", 2);
|
|
2298
3050
|
__decorateClass([
|
|
2299
3051
|
Column21("int", { nullable: true })
|
|
2300
3052
|
], Page.prototype, "seoId", 2);
|
|
2301
3053
|
__decorateClass([
|
|
2302
|
-
|
|
2303
|
-
|
|
3054
|
+
ManyToOne13(() => Seo, { onDelete: "SET NULL" }),
|
|
3055
|
+
JoinColumn13({ name: "seoId" })
|
|
2304
3056
|
], Page.prototype, "seo", 2);
|
|
2305
3057
|
__decorateClass([
|
|
2306
3058
|
Column21({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
|
@@ -2328,7 +3080,7 @@ Page = __decorateClass([
|
|
|
2328
3080
|
], Page);
|
|
2329
3081
|
|
|
2330
3082
|
// src/entities/product-category.entity.ts
|
|
2331
|
-
import { Entity as Entity22, PrimaryGeneratedColumn as PrimaryGeneratedColumn22, Column as Column22, ManyToOne as
|
|
3083
|
+
import { Entity as Entity22, PrimaryGeneratedColumn as PrimaryGeneratedColumn22, Column as Column22, ManyToOne as ManyToOne14, OneToMany as OneToMany9, JoinColumn as JoinColumn14 } from "typeorm";
|
|
2332
3084
|
var ProductCategory = class {
|
|
2333
3085
|
id;
|
|
2334
3086
|
name;
|
|
@@ -2400,8 +3152,8 @@ __decorateClass([
|
|
|
2400
3152
|
Column22("int", { nullable: true })
|
|
2401
3153
|
], ProductCategory.prototype, "deletedBy", 2);
|
|
2402
3154
|
__decorateClass([
|
|
2403
|
-
|
|
2404
|
-
|
|
3155
|
+
ManyToOne14(() => ProductCategory, (c) => c.children, { onDelete: "SET NULL" }),
|
|
3156
|
+
JoinColumn14({ name: "parentId" })
|
|
2405
3157
|
], ProductCategory.prototype, "parent", 2);
|
|
2406
3158
|
__decorateClass([
|
|
2407
3159
|
OneToMany9(() => ProductCategory, (c) => c.parent)
|
|
@@ -2417,10 +3169,10 @@ ProductCategory = __decorateClass([
|
|
|
2417
3169
|
], ProductCategory);
|
|
2418
3170
|
|
|
2419
3171
|
// src/entities/collection.entity.ts
|
|
2420
|
-
import { Entity as Entity24, PrimaryGeneratedColumn as PrimaryGeneratedColumn24, Column as Column24, ManyToOne as
|
|
3172
|
+
import { Entity as Entity24, PrimaryGeneratedColumn as PrimaryGeneratedColumn24, Column as Column24, ManyToOne as ManyToOne16, OneToMany as OneToMany11, JoinColumn as JoinColumn16 } from "typeorm";
|
|
2421
3173
|
|
|
2422
3174
|
// src/entities/brand.entity.ts
|
|
2423
|
-
import { Entity as Entity23, PrimaryGeneratedColumn as PrimaryGeneratedColumn23, Column as Column23, OneToMany as OneToMany10, ManyToOne as
|
|
3175
|
+
import { Entity as Entity23, PrimaryGeneratedColumn as PrimaryGeneratedColumn23, Column as Column23, OneToMany as OneToMany10, ManyToOne as ManyToOne15, JoinColumn as JoinColumn15 } from "typeorm";
|
|
2424
3176
|
var Brand = class {
|
|
2425
3177
|
id;
|
|
2426
3178
|
name;
|
|
@@ -2491,8 +3243,8 @@ __decorateClass([
|
|
|
2491
3243
|
Column23("int", { nullable: true })
|
|
2492
3244
|
], Brand.prototype, "seoId", 2);
|
|
2493
3245
|
__decorateClass([
|
|
2494
|
-
|
|
2495
|
-
|
|
3246
|
+
ManyToOne15(() => Seo, { onDelete: "SET NULL" }),
|
|
3247
|
+
JoinColumn15({ name: "seoId" })
|
|
2496
3248
|
], Brand.prototype, "seo", 2);
|
|
2497
3249
|
__decorateClass([
|
|
2498
3250
|
OneToMany10("Product", "brand")
|
|
@@ -2511,6 +3263,7 @@ var Collection = class {
|
|
|
2511
3263
|
brandId;
|
|
2512
3264
|
name;
|
|
2513
3265
|
slug;
|
|
3266
|
+
hsn;
|
|
2514
3267
|
description;
|
|
2515
3268
|
image;
|
|
2516
3269
|
metadata;
|
|
@@ -2544,6 +3297,9 @@ __decorateClass([
|
|
|
2544
3297
|
__decorateClass([
|
|
2545
3298
|
Column24("varchar", { unique: true })
|
|
2546
3299
|
], Collection.prototype, "slug", 2);
|
|
3300
|
+
__decorateClass([
|
|
3301
|
+
Column24("varchar", { nullable: true })
|
|
3302
|
+
], Collection.prototype, "hsn", 2);
|
|
2547
3303
|
__decorateClass([
|
|
2548
3304
|
Column24("text", { nullable: true })
|
|
2549
3305
|
], Collection.prototype, "description", 2);
|
|
@@ -2584,16 +3340,16 @@ __decorateClass([
|
|
|
2584
3340
|
Column24("int", { nullable: true })
|
|
2585
3341
|
], Collection.prototype, "seoId", 2);
|
|
2586
3342
|
__decorateClass([
|
|
2587
|
-
|
|
2588
|
-
|
|
3343
|
+
ManyToOne16(() => Seo, { onDelete: "SET NULL" }),
|
|
3344
|
+
JoinColumn16({ name: "seoId" })
|
|
2589
3345
|
], Collection.prototype, "seo", 2);
|
|
2590
3346
|
__decorateClass([
|
|
2591
|
-
|
|
2592
|
-
|
|
3347
|
+
ManyToOne16(() => ProductCategory, (c) => c.collections, { onDelete: "SET NULL" }),
|
|
3348
|
+
JoinColumn16({ name: "categoryId" })
|
|
2593
3349
|
], Collection.prototype, "category", 2);
|
|
2594
3350
|
__decorateClass([
|
|
2595
|
-
|
|
2596
|
-
|
|
3351
|
+
ManyToOne16(() => Brand, (b) => b.collections, { onDelete: "SET NULL" }),
|
|
3352
|
+
JoinColumn16({ name: "brandId" })
|
|
2597
3353
|
], Collection.prototype, "brand", 2);
|
|
2598
3354
|
__decorateClass([
|
|
2599
3355
|
OneToMany11("Product", "collection")
|
|
@@ -2603,13 +3359,14 @@ Collection = __decorateClass([
|
|
|
2603
3359
|
], Collection);
|
|
2604
3360
|
|
|
2605
3361
|
// src/entities/product.entity.ts
|
|
2606
|
-
import { Entity as Entity25, PrimaryGeneratedColumn as PrimaryGeneratedColumn25, Column as Column25, ManyToOne as
|
|
3362
|
+
import { Entity as Entity25, PrimaryGeneratedColumn as PrimaryGeneratedColumn25, Column as Column25, ManyToOne as ManyToOne17, OneToMany as OneToMany12, JoinColumn as JoinColumn17 } from "typeorm";
|
|
2607
3363
|
var Product = class {
|
|
2608
3364
|
id;
|
|
2609
3365
|
collectionId;
|
|
2610
3366
|
brandId;
|
|
2611
3367
|
categoryId;
|
|
2612
3368
|
sku;
|
|
3369
|
+
hsn;
|
|
2613
3370
|
slug;
|
|
2614
3371
|
name;
|
|
2615
3372
|
price;
|
|
@@ -2648,6 +3405,9 @@ __decorateClass([
|
|
|
2648
3405
|
__decorateClass([
|
|
2649
3406
|
Column25("varchar", { nullable: true })
|
|
2650
3407
|
], Product.prototype, "sku", 2);
|
|
3408
|
+
__decorateClass([
|
|
3409
|
+
Column25("varchar", { nullable: true })
|
|
3410
|
+
], Product.prototype, "hsn", 2);
|
|
2651
3411
|
__decorateClass([
|
|
2652
3412
|
Column25("varchar", { unique: true, nullable: true })
|
|
2653
3413
|
], Product.prototype, "slug", 2);
|
|
@@ -2697,20 +3457,20 @@ __decorateClass([
|
|
|
2697
3457
|
Column25("int", { nullable: true })
|
|
2698
3458
|
], Product.prototype, "seoId", 2);
|
|
2699
3459
|
__decorateClass([
|
|
2700
|
-
|
|
2701
|
-
|
|
3460
|
+
ManyToOne17(() => Seo, { onDelete: "SET NULL" }),
|
|
3461
|
+
JoinColumn17({ name: "seoId" })
|
|
2702
3462
|
], Product.prototype, "seo", 2);
|
|
2703
3463
|
__decorateClass([
|
|
2704
|
-
|
|
2705
|
-
|
|
3464
|
+
ManyToOne17(() => Collection, (c) => c.products, { onDelete: "SET NULL" }),
|
|
3465
|
+
JoinColumn17({ name: "collectionId" })
|
|
2706
3466
|
], Product.prototype, "collection", 2);
|
|
2707
3467
|
__decorateClass([
|
|
2708
|
-
|
|
2709
|
-
|
|
3468
|
+
ManyToOne17(() => Brand, (b) => b.products, { onDelete: "SET NULL" }),
|
|
3469
|
+
JoinColumn17({ name: "brandId" })
|
|
2710
3470
|
], Product.prototype, "brand", 2);
|
|
2711
3471
|
__decorateClass([
|
|
2712
|
-
|
|
2713
|
-
|
|
3472
|
+
ManyToOne17(() => ProductCategory, (c) => c.products, { onDelete: "SET NULL" }),
|
|
3473
|
+
JoinColumn17({ name: "categoryId" })
|
|
2714
3474
|
], Product.prototype, "category", 2);
|
|
2715
3475
|
__decorateClass([
|
|
2716
3476
|
OneToMany12("ProductAttribute", "product")
|
|
@@ -2791,7 +3551,7 @@ Attribute = __decorateClass([
|
|
|
2791
3551
|
], Attribute);
|
|
2792
3552
|
|
|
2793
3553
|
// src/entities/product-attribute.entity.ts
|
|
2794
|
-
import { Entity as Entity27, PrimaryGeneratedColumn as PrimaryGeneratedColumn27, Column as Column27, ManyToOne as
|
|
3554
|
+
import { Entity as Entity27, PrimaryGeneratedColumn as PrimaryGeneratedColumn27, Column as Column27, ManyToOne as ManyToOne18, JoinColumn as JoinColumn18 } from "typeorm";
|
|
2795
3555
|
var ProductAttribute = class {
|
|
2796
3556
|
id;
|
|
2797
3557
|
productId;
|
|
@@ -2825,12 +3585,12 @@ __decorateClass([
|
|
|
2825
3585
|
Column27({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
|
2826
3586
|
], ProductAttribute.prototype, "updatedAt", 2);
|
|
2827
3587
|
__decorateClass([
|
|
2828
|
-
|
|
2829
|
-
|
|
3588
|
+
ManyToOne18(() => Product, (p) => p.attributes, { onDelete: "CASCADE" }),
|
|
3589
|
+
JoinColumn18({ name: "productId" })
|
|
2830
3590
|
], ProductAttribute.prototype, "product", 2);
|
|
2831
3591
|
__decorateClass([
|
|
2832
|
-
|
|
2833
|
-
|
|
3592
|
+
ManyToOne18(() => Attribute, { onDelete: "CASCADE" }),
|
|
3593
|
+
JoinColumn18({ name: "attributeId" })
|
|
2834
3594
|
], ProductAttribute.prototype, "attribute", 2);
|
|
2835
3595
|
ProductAttribute = __decorateClass([
|
|
2836
3596
|
Entity27("product_attributes")
|
|
@@ -2905,7 +3665,7 @@ Tax = __decorateClass([
|
|
|
2905
3665
|
], Tax);
|
|
2906
3666
|
|
|
2907
3667
|
// src/entities/product-tax.entity.ts
|
|
2908
|
-
import { Entity as Entity29, PrimaryGeneratedColumn as PrimaryGeneratedColumn29, Column as Column29, ManyToOne as
|
|
3668
|
+
import { Entity as Entity29, PrimaryGeneratedColumn as PrimaryGeneratedColumn29, Column as Column29, ManyToOne as ManyToOne19, JoinColumn as JoinColumn19 } from "typeorm";
|
|
2909
3669
|
var ProductTax = class {
|
|
2910
3670
|
id;
|
|
2911
3671
|
productId;
|
|
@@ -2935,19 +3695,19 @@ __decorateClass([
|
|
|
2935
3695
|
Column29({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
|
2936
3696
|
], ProductTax.prototype, "updatedAt", 2);
|
|
2937
3697
|
__decorateClass([
|
|
2938
|
-
|
|
2939
|
-
|
|
3698
|
+
ManyToOne19(() => Product, (p) => p.taxes, { onDelete: "CASCADE" }),
|
|
3699
|
+
JoinColumn19({ name: "productId" })
|
|
2940
3700
|
], ProductTax.prototype, "product", 2);
|
|
2941
3701
|
__decorateClass([
|
|
2942
|
-
|
|
2943
|
-
|
|
3702
|
+
ManyToOne19(() => Tax, { onDelete: "CASCADE" }),
|
|
3703
|
+
JoinColumn19({ name: "taxId" })
|
|
2944
3704
|
], ProductTax.prototype, "tax", 2);
|
|
2945
3705
|
ProductTax = __decorateClass([
|
|
2946
3706
|
Entity29("product_taxes")
|
|
2947
3707
|
], ProductTax);
|
|
2948
3708
|
|
|
2949
3709
|
// src/entities/order-item.entity.ts
|
|
2950
|
-
import { Entity as Entity30, PrimaryGeneratedColumn as PrimaryGeneratedColumn30, Column as Column30, ManyToOne as
|
|
3710
|
+
import { Entity as Entity30, PrimaryGeneratedColumn as PrimaryGeneratedColumn30, Column as Column30, ManyToOne as ManyToOne20, JoinColumn as JoinColumn20 } from "typeorm";
|
|
2951
3711
|
var OrderItem = class {
|
|
2952
3712
|
id;
|
|
2953
3713
|
orderId;
|
|
@@ -2993,12 +3753,12 @@ __decorateClass([
|
|
|
2993
3753
|
Column30({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
|
2994
3754
|
], OrderItem.prototype, "updatedAt", 2);
|
|
2995
3755
|
__decorateClass([
|
|
2996
|
-
|
|
2997
|
-
|
|
3756
|
+
ManyToOne20(() => Order, (o) => o.items, { onDelete: "CASCADE" }),
|
|
3757
|
+
JoinColumn20({ name: "orderId" })
|
|
2998
3758
|
], OrderItem.prototype, "order", 2);
|
|
2999
3759
|
__decorateClass([
|
|
3000
|
-
|
|
3001
|
-
|
|
3760
|
+
ManyToOne20(() => Product, { onDelete: "CASCADE" }),
|
|
3761
|
+
JoinColumn20({ name: "productId" })
|
|
3002
3762
|
], OrderItem.prototype, "product", 2);
|
|
3003
3763
|
OrderItem = __decorateClass([
|
|
3004
3764
|
Entity30("order_items")
|
|
@@ -3008,7 +3768,7 @@ OrderItem = __decorateClass([
|
|
|
3008
3768
|
import { Entity as Entity32, PrimaryGeneratedColumn as PrimaryGeneratedColumn32, Column as Column32, OneToMany as OneToMany13 } from "typeorm";
|
|
3009
3769
|
|
|
3010
3770
|
// src/entities/knowledge-base-chunk.entity.ts
|
|
3011
|
-
import { Entity as Entity31, PrimaryGeneratedColumn as PrimaryGeneratedColumn31, Column as Column31, ManyToOne as
|
|
3771
|
+
import { Entity as Entity31, PrimaryGeneratedColumn as PrimaryGeneratedColumn31, Column as Column31, ManyToOne as ManyToOne21, JoinColumn as JoinColumn21 } from "typeorm";
|
|
3012
3772
|
var KnowledgeBaseChunk = class {
|
|
3013
3773
|
id;
|
|
3014
3774
|
documentId;
|
|
@@ -3033,8 +3793,8 @@ __decorateClass([
|
|
|
3033
3793
|
Column31({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
|
3034
3794
|
], KnowledgeBaseChunk.prototype, "createdAt", 2);
|
|
3035
3795
|
__decorateClass([
|
|
3036
|
-
|
|
3037
|
-
|
|
3796
|
+
ManyToOne21(() => KnowledgeBaseDocument, (d) => d.chunks, { onDelete: "CASCADE" }),
|
|
3797
|
+
JoinColumn21({ name: "documentId" })
|
|
3038
3798
|
], KnowledgeBaseChunk.prototype, "document", 2);
|
|
3039
3799
|
KnowledgeBaseChunk = __decorateClass([
|
|
3040
3800
|
Entity31("knowledge_base_chunks")
|
|
@@ -3075,24 +3835,198 @@ KnowledgeBaseDocument = __decorateClass([
|
|
|
3075
3835
|
Entity32("knowledge_base_documents")
|
|
3076
3836
|
], KnowledgeBaseDocument);
|
|
3077
3837
|
|
|
3078
|
-
// src/entities/
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3838
|
+
// src/entities/cart.entity.ts
|
|
3839
|
+
import { Entity as Entity33, PrimaryGeneratedColumn as PrimaryGeneratedColumn33, Column as Column33, ManyToOne as ManyToOne22, OneToMany as OneToMany14, JoinColumn as JoinColumn22 } from "typeorm";
|
|
3840
|
+
var Cart = class {
|
|
3841
|
+
id;
|
|
3842
|
+
guestToken;
|
|
3843
|
+
contactId;
|
|
3844
|
+
currency;
|
|
3845
|
+
expiresAt;
|
|
3846
|
+
createdAt;
|
|
3847
|
+
updatedAt;
|
|
3848
|
+
contact;
|
|
3849
|
+
items;
|
|
3850
|
+
};
|
|
3851
|
+
__decorateClass([
|
|
3852
|
+
PrimaryGeneratedColumn33()
|
|
3853
|
+
], Cart.prototype, "id", 2);
|
|
3854
|
+
__decorateClass([
|
|
3855
|
+
Column33("varchar", { nullable: true })
|
|
3856
|
+
], Cart.prototype, "guestToken", 2);
|
|
3857
|
+
__decorateClass([
|
|
3858
|
+
Column33("int", { nullable: true })
|
|
3859
|
+
], Cart.prototype, "contactId", 2);
|
|
3860
|
+
__decorateClass([
|
|
3861
|
+
Column33("varchar", { default: "INR" })
|
|
3862
|
+
], Cart.prototype, "currency", 2);
|
|
3863
|
+
__decorateClass([
|
|
3864
|
+
Column33({ type: "timestamp", nullable: true })
|
|
3865
|
+
], Cart.prototype, "expiresAt", 2);
|
|
3866
|
+
__decorateClass([
|
|
3867
|
+
Column33({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
|
3868
|
+
], Cart.prototype, "createdAt", 2);
|
|
3869
|
+
__decorateClass([
|
|
3870
|
+
Column33({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
|
3871
|
+
], Cart.prototype, "updatedAt", 2);
|
|
3872
|
+
__decorateClass([
|
|
3873
|
+
ManyToOne22(() => Contact, { onDelete: "CASCADE" }),
|
|
3874
|
+
JoinColumn22({ name: "contactId" })
|
|
3875
|
+
], Cart.prototype, "contact", 2);
|
|
3876
|
+
__decorateClass([
|
|
3877
|
+
OneToMany14("CartItem", "cart")
|
|
3878
|
+
], Cart.prototype, "items", 2);
|
|
3879
|
+
Cart = __decorateClass([
|
|
3880
|
+
Entity33("carts")
|
|
3881
|
+
], Cart);
|
|
3882
|
+
|
|
3883
|
+
// src/entities/cart-item.entity.ts
|
|
3884
|
+
import { Entity as Entity34, PrimaryGeneratedColumn as PrimaryGeneratedColumn34, Column as Column34, ManyToOne as ManyToOne23, JoinColumn as JoinColumn23 } from "typeorm";
|
|
3885
|
+
var CartItem = class {
|
|
3886
|
+
id;
|
|
3887
|
+
cartId;
|
|
3888
|
+
productId;
|
|
3889
|
+
quantity;
|
|
3890
|
+
metadata;
|
|
3891
|
+
createdAt;
|
|
3892
|
+
updatedAt;
|
|
3893
|
+
cart;
|
|
3894
|
+
product;
|
|
3895
|
+
};
|
|
3896
|
+
__decorateClass([
|
|
3897
|
+
PrimaryGeneratedColumn34()
|
|
3898
|
+
], CartItem.prototype, "id", 2);
|
|
3899
|
+
__decorateClass([
|
|
3900
|
+
Column34("int")
|
|
3901
|
+
], CartItem.prototype, "cartId", 2);
|
|
3902
|
+
__decorateClass([
|
|
3903
|
+
Column34("int")
|
|
3904
|
+
], CartItem.prototype, "productId", 2);
|
|
3905
|
+
__decorateClass([
|
|
3906
|
+
Column34("int", { default: 1 })
|
|
3907
|
+
], CartItem.prototype, "quantity", 2);
|
|
3908
|
+
__decorateClass([
|
|
3909
|
+
Column34("jsonb", { nullable: true })
|
|
3910
|
+
], CartItem.prototype, "metadata", 2);
|
|
3911
|
+
__decorateClass([
|
|
3912
|
+
Column34({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
|
3913
|
+
], CartItem.prototype, "createdAt", 2);
|
|
3914
|
+
__decorateClass([
|
|
3915
|
+
Column34({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
|
3916
|
+
], CartItem.prototype, "updatedAt", 2);
|
|
3917
|
+
__decorateClass([
|
|
3918
|
+
ManyToOne23(() => Cart, (c) => c.items, { onDelete: "CASCADE" }),
|
|
3919
|
+
JoinColumn23({ name: "cartId" })
|
|
3920
|
+
], CartItem.prototype, "cart", 2);
|
|
3921
|
+
__decorateClass([
|
|
3922
|
+
ManyToOne23(() => Product, { onDelete: "CASCADE" }),
|
|
3923
|
+
JoinColumn23({ name: "productId" })
|
|
3924
|
+
], CartItem.prototype, "product", 2);
|
|
3925
|
+
CartItem = __decorateClass([
|
|
3926
|
+
Entity34("cart_items")
|
|
3927
|
+
], CartItem);
|
|
3928
|
+
|
|
3929
|
+
// src/entities/wishlist.entity.ts
|
|
3930
|
+
import { Entity as Entity35, PrimaryGeneratedColumn as PrimaryGeneratedColumn35, Column as Column35, ManyToOne as ManyToOne24, OneToMany as OneToMany15, JoinColumn as JoinColumn24 } from "typeorm";
|
|
3931
|
+
var Wishlist = class {
|
|
3932
|
+
id;
|
|
3933
|
+
guestId;
|
|
3934
|
+
contactId;
|
|
3935
|
+
name;
|
|
3936
|
+
createdAt;
|
|
3937
|
+
updatedAt;
|
|
3938
|
+
contact;
|
|
3939
|
+
items;
|
|
3940
|
+
};
|
|
3941
|
+
__decorateClass([
|
|
3942
|
+
PrimaryGeneratedColumn35()
|
|
3943
|
+
], Wishlist.prototype, "id", 2);
|
|
3944
|
+
__decorateClass([
|
|
3945
|
+
Column35("varchar", { nullable: true })
|
|
3946
|
+
], Wishlist.prototype, "guestId", 2);
|
|
3947
|
+
__decorateClass([
|
|
3948
|
+
Column35("int", { nullable: true })
|
|
3949
|
+
], Wishlist.prototype, "contactId", 2);
|
|
3950
|
+
__decorateClass([
|
|
3951
|
+
Column35("varchar", { default: "default" })
|
|
3952
|
+
], Wishlist.prototype, "name", 2);
|
|
3953
|
+
__decorateClass([
|
|
3954
|
+
Column35({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
|
3955
|
+
], Wishlist.prototype, "createdAt", 2);
|
|
3956
|
+
__decorateClass([
|
|
3957
|
+
Column35({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
|
3958
|
+
], Wishlist.prototype, "updatedAt", 2);
|
|
3959
|
+
__decorateClass([
|
|
3960
|
+
ManyToOne24(() => Contact, { onDelete: "CASCADE" }),
|
|
3961
|
+
JoinColumn24({ name: "contactId" })
|
|
3962
|
+
], Wishlist.prototype, "contact", 2);
|
|
3963
|
+
__decorateClass([
|
|
3964
|
+
OneToMany15("WishlistItem", "wishlist")
|
|
3965
|
+
], Wishlist.prototype, "items", 2);
|
|
3966
|
+
Wishlist = __decorateClass([
|
|
3967
|
+
Entity35("wishlists")
|
|
3968
|
+
], Wishlist);
|
|
3969
|
+
|
|
3970
|
+
// src/entities/wishlist-item.entity.ts
|
|
3971
|
+
import { Entity as Entity36, PrimaryGeneratedColumn as PrimaryGeneratedColumn36, Column as Column36, ManyToOne as ManyToOne25, JoinColumn as JoinColumn25 } from "typeorm";
|
|
3972
|
+
var WishlistItem = class {
|
|
3973
|
+
id;
|
|
3974
|
+
wishlistId;
|
|
3975
|
+
productId;
|
|
3976
|
+
metadata;
|
|
3977
|
+
createdAt;
|
|
3978
|
+
updatedAt;
|
|
3979
|
+
wishlist;
|
|
3980
|
+
product;
|
|
3981
|
+
};
|
|
3982
|
+
__decorateClass([
|
|
3983
|
+
PrimaryGeneratedColumn36()
|
|
3984
|
+
], WishlistItem.prototype, "id", 2);
|
|
3985
|
+
__decorateClass([
|
|
3986
|
+
Column36("int")
|
|
3987
|
+
], WishlistItem.prototype, "wishlistId", 2);
|
|
3988
|
+
__decorateClass([
|
|
3989
|
+
Column36("int")
|
|
3990
|
+
], WishlistItem.prototype, "productId", 2);
|
|
3991
|
+
__decorateClass([
|
|
3992
|
+
Column36("jsonb", { nullable: true })
|
|
3993
|
+
], WishlistItem.prototype, "metadata", 2);
|
|
3994
|
+
__decorateClass([
|
|
3995
|
+
Column36({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
|
3996
|
+
], WishlistItem.prototype, "createdAt", 2);
|
|
3997
|
+
__decorateClass([
|
|
3998
|
+
Column36({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
|
|
3999
|
+
], WishlistItem.prototype, "updatedAt", 2);
|
|
4000
|
+
__decorateClass([
|
|
4001
|
+
ManyToOne25(() => Wishlist, (w) => w.items, { onDelete: "CASCADE" }),
|
|
4002
|
+
JoinColumn25({ name: "wishlistId" })
|
|
4003
|
+
], WishlistItem.prototype, "wishlist", 2);
|
|
4004
|
+
__decorateClass([
|
|
4005
|
+
ManyToOne25(() => Product, { onDelete: "CASCADE" }),
|
|
4006
|
+
JoinColumn25({ name: "productId" })
|
|
4007
|
+
], WishlistItem.prototype, "product", 2);
|
|
4008
|
+
WishlistItem = __decorateClass([
|
|
4009
|
+
Entity36("wishlist_items")
|
|
4010
|
+
], WishlistItem);
|
|
4011
|
+
|
|
4012
|
+
// src/entities/index.ts
|
|
4013
|
+
var CMS_ENTITY_MAP = {
|
|
4014
|
+
users: User,
|
|
4015
|
+
password_reset_tokens: PasswordResetToken,
|
|
4016
|
+
user_groups: UserGroup,
|
|
4017
|
+
permissions: Permission,
|
|
4018
|
+
blogs: Blog,
|
|
4019
|
+
tags: Tag,
|
|
4020
|
+
categories: Category,
|
|
4021
|
+
comments: Comment,
|
|
4022
|
+
contacts: Contact,
|
|
4023
|
+
addresses: Address,
|
|
4024
|
+
forms: Form,
|
|
4025
|
+
form_fields: FormField,
|
|
4026
|
+
form_submissions: FormSubmission,
|
|
4027
|
+
seos: Seo,
|
|
4028
|
+
configs: Config,
|
|
4029
|
+
media: Media,
|
|
3096
4030
|
pages: Page,
|
|
3097
4031
|
product_categories: ProductCategory,
|
|
3098
4032
|
collections: Collection,
|
|
@@ -3108,10 +4042,79 @@ var CMS_ENTITY_MAP = {
|
|
|
3108
4042
|
knowledge_base_documents: KnowledgeBaseDocument,
|
|
3109
4043
|
knowledge_base_chunks: KnowledgeBaseChunk,
|
|
3110
4044
|
chat_conversations: ChatConversation,
|
|
3111
|
-
chat_messages: ChatMessage
|
|
4045
|
+
chat_messages: ChatMessage,
|
|
4046
|
+
carts: Cart,
|
|
4047
|
+
cart_items: CartItem,
|
|
4048
|
+
wishlists: Wishlist,
|
|
4049
|
+
wishlist_items: WishlistItem
|
|
3112
4050
|
};
|
|
3113
4051
|
|
|
4052
|
+
// src/auth/permission-entities.ts
|
|
4053
|
+
var PERMISSION_ENTITY_INTERNAL_EXCLUDE = /* @__PURE__ */ new Set([
|
|
4054
|
+
"users",
|
|
4055
|
+
"password_reset_tokens",
|
|
4056
|
+
"user_groups",
|
|
4057
|
+
"permissions",
|
|
4058
|
+
"comments",
|
|
4059
|
+
"form_fields",
|
|
4060
|
+
"configs",
|
|
4061
|
+
"knowledge_base_chunks",
|
|
4062
|
+
"carts",
|
|
4063
|
+
"cart_items",
|
|
4064
|
+
"wishlists",
|
|
4065
|
+
"wishlist_items"
|
|
4066
|
+
]);
|
|
4067
|
+
var PERMISSION_LOGICAL_ENTITIES = [
|
|
4068
|
+
"users",
|
|
4069
|
+
"forms",
|
|
4070
|
+
"form_submissions",
|
|
4071
|
+
"dashboard",
|
|
4072
|
+
"upload",
|
|
4073
|
+
"settings",
|
|
4074
|
+
"analytics",
|
|
4075
|
+
"chat"
|
|
4076
|
+
];
|
|
4077
|
+
var ADMIN_GROUP_NAME = "Administrator";
|
|
4078
|
+
function isSuperAdminGroupName(name) {
|
|
4079
|
+
return name === ADMIN_GROUP_NAME;
|
|
4080
|
+
}
|
|
4081
|
+
function getPermissionableEntityKeys(entityMap) {
|
|
4082
|
+
const fromMap = Object.keys(entityMap).filter((k) => !PERMISSION_ENTITY_INTERNAL_EXCLUDE.has(k));
|
|
4083
|
+
const logical = PERMISSION_LOGICAL_ENTITIES.filter((k) => !fromMap.includes(k));
|
|
4084
|
+
return [...fromMap.sort(), ...logical].filter((k, i, a) => a.indexOf(k) === i);
|
|
4085
|
+
}
|
|
4086
|
+
function permissionRowsToRecord(rows) {
|
|
4087
|
+
const out = {};
|
|
4088
|
+
if (!rows?.length) return out;
|
|
4089
|
+
for (const p of rows) {
|
|
4090
|
+
out[p.entity] = {
|
|
4091
|
+
c: !!p.canCreate,
|
|
4092
|
+
r: !!p.canRead,
|
|
4093
|
+
u: !!p.canUpdate,
|
|
4094
|
+
d: !!p.canDelete
|
|
4095
|
+
};
|
|
4096
|
+
}
|
|
4097
|
+
return out;
|
|
4098
|
+
}
|
|
4099
|
+
function hasEntityPermission(record, entity, action) {
|
|
4100
|
+
const p = record?.[entity];
|
|
4101
|
+
if (!p) return false;
|
|
4102
|
+
if (action === "create") return p.c;
|
|
4103
|
+
if (action === "read") return p.r;
|
|
4104
|
+
if (action === "update") return p.u;
|
|
4105
|
+
return p.d;
|
|
4106
|
+
}
|
|
4107
|
+
|
|
3114
4108
|
// src/auth/helpers.ts
|
|
4109
|
+
var RBAC_ADMIN_ONLY_ENTITIES = /* @__PURE__ */ new Set(["users", "user_groups", "permissions"]);
|
|
4110
|
+
function sessionHasEntityAccess(user, entity, action) {
|
|
4111
|
+
if (!user?.email) return false;
|
|
4112
|
+
if (user.isRBACAdmin && RBAC_ADMIN_ONLY_ENTITIES.has(entity)) return true;
|
|
4113
|
+
return hasEntityPermission(user.entityPerms, entity, action);
|
|
4114
|
+
}
|
|
4115
|
+
function canManageRoles(user) {
|
|
4116
|
+
return !!(user?.email && user.isRBACAdmin);
|
|
4117
|
+
}
|
|
3115
4118
|
var OPEN_ENDPOINTS = [
|
|
3116
4119
|
{ "/api/contacts": ["POST"] },
|
|
3117
4120
|
{ "/api/form-submissions": ["POST"] },
|
|
@@ -3147,6 +4150,25 @@ function createAuthHelpers(getSession, NextResponse) {
|
|
|
3147
4150
|
}
|
|
3148
4151
|
return null;
|
|
3149
4152
|
},
|
|
4153
|
+
async requireEntityPermission(_req, entity, action) {
|
|
4154
|
+
const session = await getSession();
|
|
4155
|
+
if (!session?.user?.email) {
|
|
4156
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
4157
|
+
}
|
|
4158
|
+
const u = session.user;
|
|
4159
|
+
if (sessionHasEntityAccess(u, entity, action)) return null;
|
|
4160
|
+
return NextResponse.json({ error: "Forbidden", entity, action }, { status: 403 });
|
|
4161
|
+
},
|
|
4162
|
+
async requireAdminAccess() {
|
|
4163
|
+
const session = await getSession();
|
|
4164
|
+
if (!session?.user?.email) {
|
|
4165
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
4166
|
+
}
|
|
4167
|
+
const u = session.user;
|
|
4168
|
+
if (u.isRBACAdmin) return null;
|
|
4169
|
+
if (u.adminAccess === false) return NextResponse.json({ error: "Forbidden", reason: "admin_access" }, { status: 403 });
|
|
4170
|
+
return null;
|
|
4171
|
+
},
|
|
3150
4172
|
async getAuthenticatedUser() {
|
|
3151
4173
|
const session = await getSession();
|
|
3152
4174
|
return session?.user ?? null;
|
|
@@ -3154,6 +4176,36 @@ function createAuthHelpers(getSession, NextResponse) {
|
|
|
3154
4176
|
};
|
|
3155
4177
|
}
|
|
3156
4178
|
|
|
4179
|
+
// src/auth/seed-permissions.ts
|
|
4180
|
+
async function seedAdministratorPermissions(dataSource, entityMap) {
|
|
4181
|
+
const entities = getPermissionableEntityKeys(entityMap);
|
|
4182
|
+
const groupRepo = dataSource.getRepository(entityMap.user_groups);
|
|
4183
|
+
const permRepo = dataSource.getRepository(entityMap.permissions);
|
|
4184
|
+
const adminGroup = await groupRepo.findOne({ where: { name: ADMIN_GROUP_NAME, deleted: false } });
|
|
4185
|
+
if (!adminGroup) return;
|
|
4186
|
+
const fullCrud = { canCreate: true, canRead: true, canUpdate: true, canDelete: true };
|
|
4187
|
+
for (const entity of entities) {
|
|
4188
|
+
const existing = await permRepo.findOne({
|
|
4189
|
+
where: { groupId: adminGroup.id, entity }
|
|
4190
|
+
});
|
|
4191
|
+
if (existing) {
|
|
4192
|
+
existing.canCreate = true;
|
|
4193
|
+
existing.canRead = true;
|
|
4194
|
+
existing.canUpdate = true;
|
|
4195
|
+
existing.canDelete = true;
|
|
4196
|
+
await permRepo.save(existing);
|
|
4197
|
+
} else {
|
|
4198
|
+
await permRepo.save(
|
|
4199
|
+
permRepo.create({
|
|
4200
|
+
groupId: adminGroup.id,
|
|
4201
|
+
entity,
|
|
4202
|
+
...fullCrud
|
|
4203
|
+
})
|
|
4204
|
+
);
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
|
|
3157
4209
|
// src/auth/middleware.ts
|
|
3158
4210
|
var defaultPublicApiMethods = {
|
|
3159
4211
|
"/api/contacts": ["POST"],
|
|
@@ -3228,12 +4280,18 @@ function getNextAuthOptions(config) {
|
|
|
3228
4280
|
if (!user || user.blocked || user.deleted || !user.password) return null;
|
|
3229
4281
|
const valid = await comparePassword(credentials.password, user.password);
|
|
3230
4282
|
if (!valid) return null;
|
|
4283
|
+
const g = user.group;
|
|
4284
|
+
const isRBACAdmin = isSuperAdminGroupName(g?.name);
|
|
4285
|
+
const entityPerms = permissionRowsToRecord(g?.permissions);
|
|
4286
|
+
const adminAccess = user.adminAccess === true;
|
|
3231
4287
|
return {
|
|
3232
4288
|
id: user.id.toString(),
|
|
3233
4289
|
email: user.email,
|
|
3234
4290
|
name: user.name,
|
|
3235
4291
|
groupId: user.groupId ?? void 0,
|
|
3236
|
-
|
|
4292
|
+
isRBACAdmin,
|
|
4293
|
+
entityPerms,
|
|
4294
|
+
adminAccess
|
|
3237
4295
|
};
|
|
3238
4296
|
} catch {
|
|
3239
4297
|
return null;
|
|
@@ -3257,17 +4315,23 @@ function getNextAuthOptions(config) {
|
|
|
3257
4315
|
callbacks: {
|
|
3258
4316
|
async jwt({ token, user }) {
|
|
3259
4317
|
if (user) {
|
|
3260
|
-
|
|
3261
|
-
token.
|
|
3262
|
-
token.
|
|
4318
|
+
const u = user;
|
|
4319
|
+
token.id = u.id;
|
|
4320
|
+
token.groupId = u.groupId;
|
|
4321
|
+
token.isRBACAdmin = u.isRBACAdmin;
|
|
4322
|
+
token.entityPerms = u.entityPerms;
|
|
4323
|
+
token.adminAccess = u.adminAccess;
|
|
3263
4324
|
}
|
|
3264
4325
|
return token;
|
|
3265
4326
|
},
|
|
3266
4327
|
async session({ session, token }) {
|
|
3267
4328
|
if (session.user) {
|
|
3268
|
-
|
|
3269
|
-
session.user.
|
|
3270
|
-
session.user.
|
|
4329
|
+
const t = token;
|
|
4330
|
+
session.user.id = t.id;
|
|
4331
|
+
session.user.groupId = t.groupId;
|
|
4332
|
+
session.user.isRBACAdmin = t.isRBACAdmin;
|
|
4333
|
+
session.user.entityPerms = t.entityPerms;
|
|
4334
|
+
session.user.adminAccess = t.adminAccess;
|
|
3271
4335
|
}
|
|
3272
4336
|
return session;
|
|
3273
4337
|
}
|
|
@@ -3312,11 +4376,38 @@ function sanitizeBodyForEntity(repo, body) {
|
|
|
3312
4376
|
}
|
|
3313
4377
|
}
|
|
3314
4378
|
}
|
|
4379
|
+
function pickColumnUpdates(repo, body) {
|
|
4380
|
+
const cols = new Set(repo.metadata.columns.map((c) => c.propertyName));
|
|
4381
|
+
const out = {};
|
|
4382
|
+
for (const k of Object.keys(body)) {
|
|
4383
|
+
if (cols.has(k)) out[k] = body[k];
|
|
4384
|
+
}
|
|
4385
|
+
return out;
|
|
4386
|
+
}
|
|
4387
|
+
function buildSearchWhereClause(repo, search) {
|
|
4388
|
+
const cols = new Set(repo.metadata.columns.map((c) => c.propertyName));
|
|
4389
|
+
const term = ILike(`%${search}%`);
|
|
4390
|
+
const ors = [];
|
|
4391
|
+
for (const field of ["name", "title", "slug", "email", "filename"]) {
|
|
4392
|
+
if (cols.has(field)) ors.push({ [field]: term });
|
|
4393
|
+
}
|
|
4394
|
+
if (ors.length === 0) return {};
|
|
4395
|
+
return ors.length === 1 ? ors[0] : ors;
|
|
4396
|
+
}
|
|
3315
4397
|
function createCrudHandler(dataSource, entityMap, options) {
|
|
3316
|
-
const { requireAuth, json } = options;
|
|
4398
|
+
const { requireAuth, json, requireEntityPermission: reqPerm } = options;
|
|
4399
|
+
async function authz(req, resource, action) {
|
|
4400
|
+
const authError = await requireAuth(req);
|
|
4401
|
+
if (authError) return authError;
|
|
4402
|
+
if (reqPerm) {
|
|
4403
|
+
const pe = await reqPerm(req, resource, action);
|
|
4404
|
+
if (pe) return pe;
|
|
4405
|
+
}
|
|
4406
|
+
return null;
|
|
4407
|
+
}
|
|
3317
4408
|
return {
|
|
3318
4409
|
async GET(req, resource) {
|
|
3319
|
-
const authError = await
|
|
4410
|
+
const authError = await authz(req, resource, "read");
|
|
3320
4411
|
if (authError) return authError;
|
|
3321
4412
|
const entity = entityMap[resource];
|
|
3322
4413
|
if (!resource || !entity) {
|
|
@@ -3332,7 +4423,7 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
3332
4423
|
if (resource === "orders") {
|
|
3333
4424
|
const repo2 = dataSource.getRepository(entity);
|
|
3334
4425
|
const allowedSort = ["id", "orderNumber", "contactId", "status", "total", "currency", "createdAt", "updatedAt"];
|
|
3335
|
-
const
|
|
4426
|
+
const sortField2 = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
|
|
3336
4427
|
const sortOrderOrders = searchParams.get("sortOrder") === "asc" ? "ASC" : "DESC";
|
|
3337
4428
|
const statusFilter = searchParams.get("status")?.trim();
|
|
3338
4429
|
const dateFrom = searchParams.get("dateFrom")?.trim();
|
|
@@ -3347,7 +4438,7 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
3347
4438
|
return json({ total: 0, page, limit, totalPages: 0, data: [] });
|
|
3348
4439
|
}
|
|
3349
4440
|
}
|
|
3350
|
-
const qb = repo2.createQueryBuilder("order").leftJoinAndSelect("order.contact", "contact").leftJoinAndSelect("order.items", "items").leftJoinAndSelect("items.product", "product").leftJoinAndSelect("product.collection", "collection").orderBy(`order.${
|
|
4441
|
+
const qb = repo2.createQueryBuilder("order").leftJoinAndSelect("order.contact", "contact").leftJoinAndSelect("order.items", "items").leftJoinAndSelect("items.product", "product").leftJoinAndSelect("product.collection", "collection").orderBy(`order.${sortField2}`, sortOrderOrders).skip(skip).take(limit);
|
|
3351
4442
|
if (search && typeof search === "string" && search.trim()) {
|
|
3352
4443
|
const term = `%${search.trim()}%`;
|
|
3353
4444
|
qb.andWhere(
|
|
@@ -3378,14 +4469,14 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
3378
4469
|
if (resource === "payments") {
|
|
3379
4470
|
const repo2 = dataSource.getRepository(entity);
|
|
3380
4471
|
const allowedSort = ["id", "orderId", "amount", "currency", "status", "method", "paidAt", "createdAt", "updatedAt"];
|
|
3381
|
-
const
|
|
4472
|
+
const sortField2 = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
|
|
3382
4473
|
const sortOrderPayments = searchParams.get("sortOrder") === "asc" ? "ASC" : "DESC";
|
|
3383
4474
|
const statusFilter = searchParams.get("status")?.trim();
|
|
3384
4475
|
const dateFrom = searchParams.get("dateFrom")?.trim();
|
|
3385
4476
|
const dateTo = searchParams.get("dateTo")?.trim();
|
|
3386
4477
|
const methodFilter = searchParams.get("method")?.trim();
|
|
3387
4478
|
const orderNumberParam = searchParams.get("orderNumber")?.trim();
|
|
3388
|
-
const qb = repo2.createQueryBuilder("payment").leftJoinAndSelect("payment.order", "ord").leftJoinAndSelect("ord.contact", "orderContact").leftJoinAndSelect("payment.contact", "contact").orderBy(`payment.${
|
|
4479
|
+
const qb = repo2.createQueryBuilder("payment").leftJoinAndSelect("payment.order", "ord").leftJoinAndSelect("ord.contact", "orderContact").leftJoinAndSelect("payment.contact", "contact").orderBy(`payment.${sortField2}`, sortOrderPayments).skip(skip).take(limit);
|
|
3389
4480
|
if (search && typeof search === "string" && search.trim()) {
|
|
3390
4481
|
const term = `%${search.trim()}%`;
|
|
3391
4482
|
qb.andWhere(
|
|
@@ -3434,12 +4525,12 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
3434
4525
|
if (resource === "contacts") {
|
|
3435
4526
|
const repo2 = dataSource.getRepository(entity);
|
|
3436
4527
|
const allowedSort = ["id", "name", "email", "createdAt", "type"];
|
|
3437
|
-
const
|
|
4528
|
+
const sortField2 = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
|
|
3438
4529
|
const sortOrderContacts = searchParams.get("sortOrder") === "asc" ? "ASC" : "DESC";
|
|
3439
4530
|
const typeFilter2 = searchParams.get("type")?.trim();
|
|
3440
4531
|
const orderIdParam = searchParams.get("orderId")?.trim();
|
|
3441
4532
|
const includeSummary = searchParams.get("includeSummary") === "1";
|
|
3442
|
-
const qb = repo2.createQueryBuilder("contact").orderBy(`contact.${
|
|
4533
|
+
const qb = repo2.createQueryBuilder("contact").orderBy(`contact.${sortField2}`, sortOrderContacts).skip(skip).take(limit);
|
|
3443
4534
|
if (search && typeof search === "string" && search.trim()) {
|
|
3444
4535
|
const term = `%${search.trim()}%`;
|
|
3445
4536
|
qb.andWhere("(contact.name ILIKE :term OR contact.email ILIKE :term OR contact.phone ILIKE :term)", { term });
|
|
@@ -3473,6 +4564,8 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
3473
4564
|
}
|
|
3474
4565
|
const repo = dataSource.getRepository(entity);
|
|
3475
4566
|
const typeFilter = searchParams.get("type");
|
|
4567
|
+
const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
|
|
4568
|
+
const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
|
|
3476
4569
|
let where = {};
|
|
3477
4570
|
if (resource === "media") {
|
|
3478
4571
|
const mediaWhere = {};
|
|
@@ -3480,18 +4573,18 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
3480
4573
|
if (typeFilter) mediaWhere.mimeType = Like(`${typeFilter}/%`);
|
|
3481
4574
|
where = Object.keys(mediaWhere).length > 0 ? mediaWhere : {};
|
|
3482
4575
|
} else if (search) {
|
|
3483
|
-
where =
|
|
4576
|
+
where = buildSearchWhereClause(repo, search);
|
|
3484
4577
|
}
|
|
3485
4578
|
const [data, total] = await repo.findAndCount({
|
|
3486
4579
|
skip,
|
|
3487
4580
|
take: limit,
|
|
3488
|
-
order: { [
|
|
4581
|
+
order: { [sortField]: sortOrder },
|
|
3489
4582
|
where
|
|
3490
4583
|
});
|
|
3491
4584
|
return json({ total, page, limit, totalPages: Math.ceil(total / limit), data });
|
|
3492
4585
|
},
|
|
3493
4586
|
async POST(req, resource) {
|
|
3494
|
-
const authError = await
|
|
4587
|
+
const authError = await authz(req, resource, "create");
|
|
3495
4588
|
if (authError) return authError;
|
|
3496
4589
|
const entity = entityMap[resource];
|
|
3497
4590
|
if (!resource || !entity) {
|
|
@@ -3507,7 +4600,7 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
3507
4600
|
return json(created, { status: 201 });
|
|
3508
4601
|
},
|
|
3509
4602
|
async GET_METADATA(req, resource) {
|
|
3510
|
-
const authError = await
|
|
4603
|
+
const authError = await authz(req, resource, "read");
|
|
3511
4604
|
if (authError) return authError;
|
|
3512
4605
|
const entity = entityMap[resource];
|
|
3513
4606
|
if (!resource || !entity) {
|
|
@@ -3538,7 +4631,7 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
3538
4631
|
return json({ columns, uniqueColumns });
|
|
3539
4632
|
},
|
|
3540
4633
|
async BULK_POST(req, resource) {
|
|
3541
|
-
const authError = await
|
|
4634
|
+
const authError = await authz(req, resource, "update");
|
|
3542
4635
|
if (authError) return authError;
|
|
3543
4636
|
const entity = entityMap[resource];
|
|
3544
4637
|
if (!resource || !entity) {
|
|
@@ -3569,7 +4662,7 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
3569
4662
|
}
|
|
3570
4663
|
},
|
|
3571
4664
|
async GET_EXPORT(req, resource) {
|
|
3572
|
-
const authError = await
|
|
4665
|
+
const authError = await authz(req, resource, "read");
|
|
3573
4666
|
if (authError) return authError;
|
|
3574
4667
|
const entity = entityMap[resource];
|
|
3575
4668
|
if (!resource || !entity) {
|
|
@@ -3610,10 +4703,19 @@ function createCrudHandler(dataSource, entityMap, options) {
|
|
|
3610
4703
|
};
|
|
3611
4704
|
}
|
|
3612
4705
|
function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
3613
|
-
const { requireAuth, json } = options;
|
|
4706
|
+
const { requireAuth, json, requireEntityPermission: reqPerm } = options;
|
|
4707
|
+
async function authz(req, resource, action) {
|
|
4708
|
+
const authError = await requireAuth(req);
|
|
4709
|
+
if (authError) return authError;
|
|
4710
|
+
if (reqPerm) {
|
|
4711
|
+
const pe = await reqPerm(req, resource, action);
|
|
4712
|
+
if (pe) return pe;
|
|
4713
|
+
}
|
|
4714
|
+
return null;
|
|
4715
|
+
}
|
|
3614
4716
|
return {
|
|
3615
4717
|
async GET(req, resource, id) {
|
|
3616
|
-
const authError = await
|
|
4718
|
+
const authError = await authz(req, resource, "read");
|
|
3617
4719
|
if (authError) return authError;
|
|
3618
4720
|
const entity = entityMap[resource];
|
|
3619
4721
|
if (!entity) return json({ error: "Invalid resource" }, { status: 400 });
|
|
@@ -3656,23 +4758,111 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
|
|
|
3656
4758
|
if (!payment) return json({ message: "Not found" }, { status: 404 });
|
|
3657
4759
|
return json(payment);
|
|
3658
4760
|
}
|
|
4761
|
+
if (resource === "blogs") {
|
|
4762
|
+
const blog = await repo.findOne({
|
|
4763
|
+
where: { id: Number(id) },
|
|
4764
|
+
relations: ["category", "seo", "tags"]
|
|
4765
|
+
});
|
|
4766
|
+
return blog ? json(blog) : json({ message: "Not found" }, { status: 404 });
|
|
4767
|
+
}
|
|
3659
4768
|
const item = await repo.findOne({ where: { id: Number(id) } });
|
|
3660
4769
|
return item ? json(item) : json({ message: "Not found" }, { status: 404 });
|
|
3661
4770
|
},
|
|
3662
4771
|
async PUT(req, resource, id) {
|
|
3663
|
-
const authError = await
|
|
4772
|
+
const authError = await authz(req, resource, "update");
|
|
3664
4773
|
if (authError) return authError;
|
|
3665
4774
|
const entity = entityMap[resource];
|
|
3666
4775
|
if (!entity) return json({ error: "Invalid resource" }, { status: 400 });
|
|
3667
|
-
const
|
|
4776
|
+
const rawBody = await req.json();
|
|
3668
4777
|
const repo = dataSource.getRepository(entity);
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
4778
|
+
const numericId = Number(id);
|
|
4779
|
+
if (resource === "blogs" && rawBody && typeof rawBody === "object" && entityMap.categories && entityMap.seos && entityMap.tags) {
|
|
4780
|
+
const existing = await repo.findOne({ where: { id: numericId } });
|
|
4781
|
+
if (!existing) return json({ message: "Not found" }, { status: 404 });
|
|
4782
|
+
const updatePayload2 = pickColumnUpdates(repo, rawBody);
|
|
4783
|
+
if ("category" in rawBody) {
|
|
4784
|
+
const c = rawBody.category;
|
|
4785
|
+
if (typeof c === "string" && c.trim()) {
|
|
4786
|
+
const cat = await dataSource.getRepository(entityMap.categories).findOne({ where: { name: c.trim() } });
|
|
4787
|
+
updatePayload2.categoryId = cat?.id ?? null;
|
|
4788
|
+
} else {
|
|
4789
|
+
updatePayload2.categoryId = null;
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
const blogSlug = typeof updatePayload2.slug === "string" && updatePayload2.slug || existing.slug;
|
|
4793
|
+
const seoRepo = dataSource.getRepository(entityMap.seos);
|
|
4794
|
+
const seoField = (k) => {
|
|
4795
|
+
if (!(k in rawBody)) return void 0;
|
|
4796
|
+
const v = rawBody[k];
|
|
4797
|
+
if (v == null || v === "") return null;
|
|
4798
|
+
return String(v);
|
|
4799
|
+
};
|
|
4800
|
+
if ("metaTitle" in rawBody || "metaDescription" in rawBody || "metaKeywords" in rawBody || "ogImage" in rawBody) {
|
|
4801
|
+
const title = seoField("metaTitle");
|
|
4802
|
+
const description = seoField("metaDescription");
|
|
4803
|
+
const keywords = seoField("metaKeywords");
|
|
4804
|
+
const ogImage = seoField("ogImage");
|
|
4805
|
+
const exSeoId = existing.seoId;
|
|
4806
|
+
if (exSeoId) {
|
|
4807
|
+
const seo = await seoRepo.findOne({ where: { id: exSeoId } });
|
|
4808
|
+
if (seo) {
|
|
4809
|
+
const s = seo;
|
|
4810
|
+
if (title !== void 0) s.title = title;
|
|
4811
|
+
if (description !== void 0) s.description = description;
|
|
4812
|
+
if (keywords !== void 0) s.keywords = keywords;
|
|
4813
|
+
if (ogImage !== void 0) s.ogImage = ogImage;
|
|
4814
|
+
s.slug = blogSlug;
|
|
4815
|
+
await seoRepo.save(seo);
|
|
4816
|
+
}
|
|
4817
|
+
} else {
|
|
4818
|
+
let seoSlug = blogSlug;
|
|
4819
|
+
const taken = await seoRepo.findOne({ where: { slug: seoSlug } });
|
|
4820
|
+
if (taken) seoSlug = `blog-${numericId}-${blogSlug}`;
|
|
4821
|
+
const seo = await seoRepo.save(
|
|
4822
|
+
seoRepo.create({
|
|
4823
|
+
slug: seoSlug,
|
|
4824
|
+
title: title ?? null,
|
|
4825
|
+
description: description ?? null,
|
|
4826
|
+
keywords: keywords ?? null,
|
|
4827
|
+
ogImage: ogImage ?? null
|
|
4828
|
+
})
|
|
4829
|
+
);
|
|
4830
|
+
updatePayload2.seoId = seo.id;
|
|
4831
|
+
}
|
|
4832
|
+
}
|
|
4833
|
+
sanitizeBodyForEntity(repo, updatePayload2);
|
|
4834
|
+
await repo.update(numericId, updatePayload2);
|
|
4835
|
+
if (Array.isArray(rawBody.tags)) {
|
|
4836
|
+
const tagNames = rawBody.tags.map((t) => String(t).trim()).filter(Boolean);
|
|
4837
|
+
const tagRepo = dataSource.getRepository(entityMap.tags);
|
|
4838
|
+
const tagEntities = [];
|
|
4839
|
+
for (const name of tagNames) {
|
|
4840
|
+
let tag = await tagRepo.findOne({ where: { name } });
|
|
4841
|
+
if (!tag) tag = await tagRepo.save(tagRepo.create({ name }));
|
|
4842
|
+
tagEntities.push(tag);
|
|
4843
|
+
}
|
|
4844
|
+
const blog = await repo.findOne({ where: { id: numericId }, relations: ["tags"] });
|
|
4845
|
+
if (blog) {
|
|
4846
|
+
blog.tags = tagEntities;
|
|
4847
|
+
await repo.save(blog);
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
const updated2 = await repo.findOne({
|
|
4851
|
+
where: { id: numericId },
|
|
4852
|
+
relations: ["tags", "category", "seo"]
|
|
4853
|
+
});
|
|
4854
|
+
return updated2 ? json(updated2) : json({ message: "Not found" }, { status: 404 });
|
|
4855
|
+
}
|
|
4856
|
+
const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
|
|
4857
|
+
if (Object.keys(updatePayload).length > 0) {
|
|
4858
|
+
sanitizeBodyForEntity(repo, updatePayload);
|
|
4859
|
+
await repo.update(numericId, updatePayload);
|
|
4860
|
+
}
|
|
4861
|
+
const updated = await repo.findOne({ where: { id: numericId } });
|
|
3672
4862
|
return updated ? json(updated) : json({ message: "Not found" }, { status: 404 });
|
|
3673
4863
|
},
|
|
3674
4864
|
async DELETE(req, resource, id) {
|
|
3675
|
-
const authError = await
|
|
4865
|
+
const authError = await authz(req, resource, "delete");
|
|
3676
4866
|
if (authError) return authError;
|
|
3677
4867
|
const entity = entityMap[resource];
|
|
3678
4868
|
if (!entity) return json({ error: "Invalid resource" }, { status: 400 });
|
|
@@ -3696,13 +4886,20 @@ function createForgotPasswordHandler(config) {
|
|
|
3696
4886
|
const user = await userRepo.findOne({ where: { email }, select: ["email"] });
|
|
3697
4887
|
const msg = "If an account exists with this email, you will receive a reset link shortly.";
|
|
3698
4888
|
if (!user) return json({ message: msg }, { status: 200 });
|
|
3699
|
-
const
|
|
3700
|
-
const token =
|
|
4889
|
+
const crypto3 = await import("crypto");
|
|
4890
|
+
const token = crypto3.randomBytes(32).toString("hex");
|
|
3701
4891
|
const expiresAt = new Date(Date.now() + resetExpiryHours * 60 * 60 * 1e3);
|
|
3702
4892
|
const tokenRepo = dataSource.getRepository(entityMap.password_reset_tokens);
|
|
3703
4893
|
await tokenRepo.save(tokenRepo.create({ email: user.email, token, expiresAt }));
|
|
3704
4894
|
const resetLink = `${baseUrl}/admin/reset-password?token=${token}`;
|
|
3705
|
-
if (sendEmail)
|
|
4895
|
+
if (sendEmail)
|
|
4896
|
+
await sendEmail({
|
|
4897
|
+
to: user.email,
|
|
4898
|
+
subject: "Password reset",
|
|
4899
|
+
html: `<a href="${resetLink}">Reset password</a>`,
|
|
4900
|
+
text: resetLink,
|
|
4901
|
+
resetLink
|
|
4902
|
+
});
|
|
3706
4903
|
if (afterCreateToken) await afterCreateToken(user.email, resetLink);
|
|
3707
4904
|
return json({ message: msg }, { status: 200 });
|
|
3708
4905
|
} catch (err) {
|
|
@@ -3751,6 +4948,9 @@ function createInviteAcceptHandler(config) {
|
|
|
3751
4948
|
const user = await userRepo.findOne({ where: { email }, select: ["id", "blocked"] });
|
|
3752
4949
|
if (!user) return json({ error: "User not found" }, { status: 400 });
|
|
3753
4950
|
if (!user.blocked) return json({ error: "User is already active" }, { status: 400 });
|
|
4951
|
+
if (entityMap.contacts) {
|
|
4952
|
+
await linkUnclaimedContactToUser(dataSource, entityMap.contacts, user.id, email);
|
|
4953
|
+
}
|
|
3754
4954
|
if (beforeActivate) await beforeActivate(email, user.id);
|
|
3755
4955
|
const hashedPassword = await hashPassword(password);
|
|
3756
4956
|
await userRepo.update(user.id, { password: hashedPassword, blocked: false });
|
|
@@ -3811,12 +5011,17 @@ function createUserAuthApiRouter(config) {
|
|
|
3811
5011
|
}
|
|
3812
5012
|
|
|
3813
5013
|
// src/api/cms-handlers.ts
|
|
5014
|
+
init_email_queue();
|
|
3814
5015
|
import { MoreThanOrEqual, ILike as ILike2 } from "typeorm";
|
|
3815
5016
|
function createDashboardStatsHandler(config) {
|
|
3816
|
-
const { dataSource, entityMap, json, requireAuth, requirePermission } = config;
|
|
5017
|
+
const { dataSource, entityMap, json, requireAuth, requirePermission, requireEntityPermission } = config;
|
|
3817
5018
|
return async function GET(req) {
|
|
3818
5019
|
const authErr = await requireAuth(req);
|
|
3819
5020
|
if (authErr) return authErr;
|
|
5021
|
+
if (requireEntityPermission) {
|
|
5022
|
+
const pe = await requireEntityPermission(req, "dashboard", "read");
|
|
5023
|
+
if (pe) return pe;
|
|
5024
|
+
}
|
|
3820
5025
|
if (requirePermission) {
|
|
3821
5026
|
const permErr = await requirePermission(req, "view_dashboard");
|
|
3822
5027
|
if (permErr) return permErr;
|
|
@@ -3875,11 +5080,15 @@ function createAnalyticsHandlers(config) {
|
|
|
3875
5080
|
};
|
|
3876
5081
|
}
|
|
3877
5082
|
function createUploadHandler(config) {
|
|
3878
|
-
const { json, requireAuth, storage, localUploadDir = "public/uploads", allowedTypes, maxSizeBytes = 10 * 1024 * 1024 } = config;
|
|
5083
|
+
const { json, requireAuth, requireEntityPermission, storage, localUploadDir = "public/uploads", allowedTypes, maxSizeBytes = 10 * 1024 * 1024 } = config;
|
|
3879
5084
|
const allowed = allowedTypes ?? ["image/jpeg", "image/png", "image/gif", "image/webp", "application/pdf", "text/plain"];
|
|
3880
5085
|
return async function POST(req) {
|
|
3881
5086
|
const authErr = await requireAuth(req);
|
|
3882
5087
|
if (authErr) return authErr;
|
|
5088
|
+
if (requireEntityPermission) {
|
|
5089
|
+
const pe = await requireEntityPermission(req, "upload", "create");
|
|
5090
|
+
if (pe) return pe;
|
|
5091
|
+
}
|
|
3883
5092
|
try {
|
|
3884
5093
|
const formData = await req.formData();
|
|
3885
5094
|
const file = formData.get("file");
|
|
@@ -3960,13 +5169,17 @@ function normalizeFieldRow(f, formId) {
|
|
|
3960
5169
|
};
|
|
3961
5170
|
}
|
|
3962
5171
|
function createFormSaveHandlers(config) {
|
|
3963
|
-
const { dataSource, entityMap, json, requireAuth } = config;
|
|
5172
|
+
const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
|
|
3964
5173
|
const formRepo = () => dataSource.getRepository(entityMap.forms);
|
|
3965
5174
|
const fieldRepo = () => dataSource.getRepository(entityMap.form_fields);
|
|
3966
5175
|
return {
|
|
3967
5176
|
async GET(req, id) {
|
|
3968
5177
|
const authErr = await requireAuth(req);
|
|
3969
5178
|
if (authErr) return authErr;
|
|
5179
|
+
if (requireEntityPermission) {
|
|
5180
|
+
const pe = await requireEntityPermission(req, "forms", "read");
|
|
5181
|
+
if (pe) return pe;
|
|
5182
|
+
}
|
|
3970
5183
|
try {
|
|
3971
5184
|
const formId = Number(id);
|
|
3972
5185
|
if (!Number.isInteger(formId) || formId <= 0) return json({ error: "Invalid form id" }, { status: 400 });
|
|
@@ -3986,6 +5199,10 @@ function createFormSaveHandlers(config) {
|
|
|
3986
5199
|
async POST(req) {
|
|
3987
5200
|
const authErr = await requireAuth(req);
|
|
3988
5201
|
if (authErr) return authErr;
|
|
5202
|
+
if (requireEntityPermission) {
|
|
5203
|
+
const pe = await requireEntityPermission(req, "forms", "create");
|
|
5204
|
+
if (pe) return pe;
|
|
5205
|
+
}
|
|
3989
5206
|
try {
|
|
3990
5207
|
const body = await req.json();
|
|
3991
5208
|
if (!body || typeof body !== "object") return json({ error: "Invalid request payload" }, { status: 400 });
|
|
@@ -4006,6 +5223,10 @@ function createFormSaveHandlers(config) {
|
|
|
4006
5223
|
async PUT(req, id) {
|
|
4007
5224
|
const authErr = await requireAuth(req);
|
|
4008
5225
|
if (authErr) return authErr;
|
|
5226
|
+
if (requireEntityPermission) {
|
|
5227
|
+
const pe = await requireEntityPermission(req, "forms", "update");
|
|
5228
|
+
if (pe) return pe;
|
|
5229
|
+
}
|
|
4009
5230
|
try {
|
|
4010
5231
|
const formId = Number(id);
|
|
4011
5232
|
if (!Number.isInteger(formId) || formId <= 0) return json({ error: "Invalid form id" }, { status: 400 });
|
|
@@ -4034,10 +5255,14 @@ function createFormSaveHandlers(config) {
|
|
|
4034
5255
|
};
|
|
4035
5256
|
}
|
|
4036
5257
|
function createFormSubmissionGetByIdHandler(config) {
|
|
4037
|
-
const { dataSource, entityMap, json, requireAuth } = config;
|
|
5258
|
+
const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
|
|
4038
5259
|
return async function GET(req, id) {
|
|
4039
5260
|
const authErr = await requireAuth(req);
|
|
4040
5261
|
if (authErr) return authErr;
|
|
5262
|
+
if (requireEntityPermission) {
|
|
5263
|
+
const pe = await requireEntityPermission(req, "form_submissions", "read");
|
|
5264
|
+
if (pe) return pe;
|
|
5265
|
+
}
|
|
4041
5266
|
try {
|
|
4042
5267
|
const submissionId = Number(id);
|
|
4043
5268
|
if (!Number.isInteger(submissionId) || submissionId <= 0) return json({ error: "Invalid id" }, { status: 400 });
|
|
@@ -4064,10 +5289,14 @@ function createFormSubmissionGetByIdHandler(config) {
|
|
|
4064
5289
|
};
|
|
4065
5290
|
}
|
|
4066
5291
|
function createFormSubmissionListHandler(config) {
|
|
4067
|
-
const { dataSource, entityMap, json, requireAuth } = config;
|
|
5292
|
+
const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
|
|
4068
5293
|
return async function GET(req) {
|
|
4069
5294
|
const authErr = await requireAuth(req);
|
|
4070
5295
|
if (authErr) return authErr;
|
|
5296
|
+
if (requireEntityPermission) {
|
|
5297
|
+
const pe = await requireEntityPermission(req, "form_submissions", "read");
|
|
5298
|
+
if (pe) return pe;
|
|
5299
|
+
}
|
|
4071
5300
|
try {
|
|
4072
5301
|
const repo = dataSource.getRepository(entityMap.form_submissions);
|
|
4073
5302
|
const { searchParams } = new URL(req.url);
|
|
@@ -4088,6 +5317,11 @@ function createFormSubmissionListHandler(config) {
|
|
|
4088
5317
|
}
|
|
4089
5318
|
};
|
|
4090
5319
|
}
|
|
5320
|
+
function formatSubmissionFieldValue(raw) {
|
|
5321
|
+
if (raw == null || raw === "") return "\u2014";
|
|
5322
|
+
if (typeof raw === "object") return JSON.stringify(raw);
|
|
5323
|
+
return String(raw);
|
|
5324
|
+
}
|
|
4091
5325
|
function pickContactFromSubmission(fields, data) {
|
|
4092
5326
|
let email = null;
|
|
4093
5327
|
let name = null;
|
|
@@ -4163,6 +5397,50 @@ function createFormSubmissionHandler(config) {
|
|
|
4163
5397
|
userAgent: userAgent?.slice(0, 500) ?? null
|
|
4164
5398
|
})
|
|
4165
5399
|
);
|
|
5400
|
+
const formWithName = form;
|
|
5401
|
+
const formName = formWithName.name ?? "Form";
|
|
5402
|
+
let contactName = "Unknown";
|
|
5403
|
+
let contactEmail = "";
|
|
5404
|
+
if (Number.isInteger(contactId)) {
|
|
5405
|
+
const contactRepo = dataSource.getRepository(entityMap.contacts);
|
|
5406
|
+
const contact = await contactRepo.findOne({ where: { id: contactId }, select: ["name", "email"] });
|
|
5407
|
+
if (contact) {
|
|
5408
|
+
contactName = contact.name ?? contactName;
|
|
5409
|
+
contactEmail = contact.email ?? contactEmail;
|
|
5410
|
+
}
|
|
5411
|
+
} else {
|
|
5412
|
+
const contactData = pickContactFromSubmission(activeFields, data);
|
|
5413
|
+
if (contactData) {
|
|
5414
|
+
contactName = contactData.name;
|
|
5415
|
+
contactEmail = contactData.email;
|
|
5416
|
+
}
|
|
5417
|
+
}
|
|
5418
|
+
if (config.getCms && config.getCompanyDetails && config.getRecipientForChannel) {
|
|
5419
|
+
try {
|
|
5420
|
+
const cms = await config.getCms();
|
|
5421
|
+
const to = await config.getRecipientForChannel("crm");
|
|
5422
|
+
if (to) {
|
|
5423
|
+
const companyDetails = await config.getCompanyDetails();
|
|
5424
|
+
const formFieldRows = activeFields.map((f) => ({
|
|
5425
|
+
label: f.label && String(f.label).trim() || `Field ${f.id}`,
|
|
5426
|
+
value: formatSubmissionFieldValue(data[String(f.id)])
|
|
5427
|
+
}));
|
|
5428
|
+
await queueEmail(cms, {
|
|
5429
|
+
to,
|
|
5430
|
+
templateName: "formSubmission",
|
|
5431
|
+
ctx: {
|
|
5432
|
+
formName,
|
|
5433
|
+
contactName,
|
|
5434
|
+
contactEmail,
|
|
5435
|
+
formData: data,
|
|
5436
|
+
formFieldRows,
|
|
5437
|
+
companyDetails: companyDetails ?? {}
|
|
5438
|
+
}
|
|
5439
|
+
});
|
|
5440
|
+
}
|
|
5441
|
+
} catch {
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
4166
5444
|
return json(created, { status: 201 });
|
|
4167
5445
|
} catch {
|
|
4168
5446
|
return json({ error: "Server Error" }, { status: 500 });
|
|
@@ -4170,12 +5448,34 @@ function createFormSubmissionHandler(config) {
|
|
|
4170
5448
|
};
|
|
4171
5449
|
}
|
|
4172
5450
|
function createUsersApiHandlers(config) {
|
|
4173
|
-
const { dataSource, entityMap, json, requireAuth, baseUrl } = config;
|
|
5451
|
+
const { dataSource, entityMap, json, requireAuth, requireEntityPermission, baseUrl, getCms, getCompanyDetails } = config;
|
|
5452
|
+
async function trySendInviteEmail(toEmail, inviteLink, inviteeName) {
|
|
5453
|
+
if (!getCms) return;
|
|
5454
|
+
try {
|
|
5455
|
+
const cms = await getCms();
|
|
5456
|
+
const companyDetails = getCompanyDetails ? await getCompanyDetails() : {};
|
|
5457
|
+
await queueEmail(cms, {
|
|
5458
|
+
to: toEmail,
|
|
5459
|
+
templateName: "invite",
|
|
5460
|
+
ctx: {
|
|
5461
|
+
inviteLink,
|
|
5462
|
+
email: toEmail,
|
|
5463
|
+
inviteeName: inviteeName.trim(),
|
|
5464
|
+
companyDetails: companyDetails ?? {}
|
|
5465
|
+
}
|
|
5466
|
+
});
|
|
5467
|
+
} catch {
|
|
5468
|
+
}
|
|
5469
|
+
}
|
|
4174
5470
|
const userRepo = () => dataSource.getRepository(entityMap.users);
|
|
4175
5471
|
return {
|
|
4176
5472
|
async list(req) {
|
|
4177
5473
|
const authErr = await requireAuth(req);
|
|
4178
5474
|
if (authErr) return authErr;
|
|
5475
|
+
if (requireEntityPermission) {
|
|
5476
|
+
const pe = await requireEntityPermission(req, "users", "read");
|
|
5477
|
+
if (pe) return pe;
|
|
5478
|
+
}
|
|
4179
5479
|
try {
|
|
4180
5480
|
const url = new URL(req.url);
|
|
4181
5481
|
const page = Math.max(1, parseInt(url.searchParams.get("page") || "1", 10));
|
|
@@ -4201,16 +5501,40 @@ function createUsersApiHandlers(config) {
|
|
|
4201
5501
|
async create(req) {
|
|
4202
5502
|
const authErr = await requireAuth(req);
|
|
4203
5503
|
if (authErr) return authErr;
|
|
5504
|
+
if (requireEntityPermission) {
|
|
5505
|
+
const pe = await requireEntityPermission(req, "users", "create");
|
|
5506
|
+
if (pe) return pe;
|
|
5507
|
+
}
|
|
4204
5508
|
try {
|
|
4205
5509
|
const body = await req.json();
|
|
4206
5510
|
if (!body?.name || !body?.email) return json({ error: "Name and email are required" }, { status: 400 });
|
|
4207
5511
|
const existing = await userRepo().findOne({ where: { email: body.email } });
|
|
4208
5512
|
if (existing) return json({ error: "User with this email already exists" }, { status: 400 });
|
|
5513
|
+
const groupRepo = dataSource.getRepository(entityMap.user_groups);
|
|
5514
|
+
const customerG = await groupRepo.findOne({ where: { name: "Customer", deleted: false } });
|
|
5515
|
+
const gid = body.groupId ?? null;
|
|
5516
|
+
const isCustomer = !!(customerG && gid === customerG.id);
|
|
5517
|
+
const adminAccess = isCustomer ? false : body.adminAccess === false ? false : true;
|
|
4209
5518
|
const newUser = await userRepo().save(
|
|
4210
|
-
userRepo().create({
|
|
5519
|
+
userRepo().create({
|
|
5520
|
+
name: body.name,
|
|
5521
|
+
email: body.email,
|
|
5522
|
+
password: null,
|
|
5523
|
+
blocked: true,
|
|
5524
|
+
groupId: gid,
|
|
5525
|
+
adminAccess
|
|
5526
|
+
})
|
|
4211
5527
|
);
|
|
5528
|
+
if (entityMap.contacts) {
|
|
5529
|
+
await linkUnclaimedContactToUser(dataSource, entityMap.contacts, newUser.id, newUser.email);
|
|
5530
|
+
}
|
|
4212
5531
|
const emailToken = Buffer.from(newUser.email).toString("base64");
|
|
4213
5532
|
const inviteLink = `${baseUrl}/admin/invite?token=${emailToken}`;
|
|
5533
|
+
await trySendInviteEmail(
|
|
5534
|
+
newUser.email,
|
|
5535
|
+
inviteLink,
|
|
5536
|
+
newUser.name ?? ""
|
|
5537
|
+
);
|
|
4214
5538
|
return json({ message: "User created successfully (blocked until password is set)", user: newUser, inviteLink }, { status: 201 });
|
|
4215
5539
|
} catch {
|
|
4216
5540
|
return json({ error: "Server Error" }, { status: 500 });
|
|
@@ -4219,6 +5543,10 @@ function createUsersApiHandlers(config) {
|
|
|
4219
5543
|
async getById(_req, id) {
|
|
4220
5544
|
const authErr = await requireAuth(new Request(_req.url));
|
|
4221
5545
|
if (authErr) return authErr;
|
|
5546
|
+
if (requireEntityPermission) {
|
|
5547
|
+
const pe = await requireEntityPermission(_req, "users", "read");
|
|
5548
|
+
if (pe) return pe;
|
|
5549
|
+
}
|
|
4222
5550
|
try {
|
|
4223
5551
|
const user = await userRepo().findOne({
|
|
4224
5552
|
where: { id: parseInt(id, 10) },
|
|
@@ -4234,6 +5562,10 @@ function createUsersApiHandlers(config) {
|
|
|
4234
5562
|
async update(req, id) {
|
|
4235
5563
|
const authErr = await requireAuth(req);
|
|
4236
5564
|
if (authErr) return authErr;
|
|
5565
|
+
if (requireEntityPermission) {
|
|
5566
|
+
const pe = await requireEntityPermission(req, "users", "update");
|
|
5567
|
+
if (pe) return pe;
|
|
5568
|
+
}
|
|
4237
5569
|
try {
|
|
4238
5570
|
const body = await req.json();
|
|
4239
5571
|
const { password: _p, ...safe } = body;
|
|
@@ -4251,6 +5583,10 @@ function createUsersApiHandlers(config) {
|
|
|
4251
5583
|
async delete(_req, id) {
|
|
4252
5584
|
const authErr = await requireAuth(new Request(_req.url));
|
|
4253
5585
|
if (authErr) return authErr;
|
|
5586
|
+
if (requireEntityPermission) {
|
|
5587
|
+
const pe = await requireEntityPermission(_req, "users", "delete");
|
|
5588
|
+
if (pe) return pe;
|
|
5589
|
+
}
|
|
4254
5590
|
try {
|
|
4255
5591
|
const r = await userRepo().delete(parseInt(id, 10));
|
|
4256
5592
|
if (r.affected === 0) return json({ error: "User not found" }, { status: 404 });
|
|
@@ -4262,11 +5598,16 @@ function createUsersApiHandlers(config) {
|
|
|
4262
5598
|
async regenerateInvite(_req, id) {
|
|
4263
5599
|
const authErr = await requireAuth(new Request(_req.url));
|
|
4264
5600
|
if (authErr) return authErr;
|
|
5601
|
+
if (requireEntityPermission) {
|
|
5602
|
+
const pe = await requireEntityPermission(_req, "users", "update");
|
|
5603
|
+
if (pe) return pe;
|
|
5604
|
+
}
|
|
4265
5605
|
try {
|
|
4266
|
-
const user = await userRepo().findOne({ where: { id: parseInt(id, 10) }, select: ["email"] });
|
|
5606
|
+
const user = await userRepo().findOne({ where: { id: parseInt(id, 10) }, select: ["email", "name"] });
|
|
4267
5607
|
if (!user) return json({ error: "User not found" }, { status: 404 });
|
|
4268
5608
|
const emailToken = Buffer.from(user.email).toString("base64");
|
|
4269
5609
|
const inviteLink = `${baseUrl}/admin/invite?token=${emailToken}`;
|
|
5610
|
+
await trySendInviteEmail(user.email, inviteLink, user.name ?? "");
|
|
4270
5611
|
return json({ message: "New invite link generated successfully", inviteLink });
|
|
4271
5612
|
} catch {
|
|
4272
5613
|
return json({ error: "Server Error" }, { status: 500 });
|
|
@@ -4516,8 +5857,167 @@ ${contextParts.join("\n\n")}` : "You are a helpful assistant for the company. If
|
|
|
4516
5857
|
};
|
|
4517
5858
|
}
|
|
4518
5859
|
|
|
5860
|
+
// src/api/admin-roles-handlers.ts
|
|
5861
|
+
function createAdminRolesHandlers(config) {
|
|
5862
|
+
const { dataSource, entityMap, json, getSessionUser } = config;
|
|
5863
|
+
const baseEntities = getPermissionableEntityKeys(entityMap);
|
|
5864
|
+
const allowEntities = /* @__PURE__ */ new Set([...baseEntities, "users"]);
|
|
5865
|
+
const groupRepo = () => dataSource.getRepository(entityMap.user_groups);
|
|
5866
|
+
const permRepo = () => dataSource.getRepository(entityMap.permissions);
|
|
5867
|
+
const userRepo = () => dataSource.getRepository(entityMap.users);
|
|
5868
|
+
async function gate() {
|
|
5869
|
+
const u = await getSessionUser();
|
|
5870
|
+
if (!u?.email) return json({ error: "Unauthorized" }, { status: 401 });
|
|
5871
|
+
if (!canManageRoles(u)) return json({ error: "Forbidden" }, { status: 403 });
|
|
5872
|
+
return null;
|
|
5873
|
+
}
|
|
5874
|
+
return {
|
|
5875
|
+
async list() {
|
|
5876
|
+
const err = await gate();
|
|
5877
|
+
if (err) return err;
|
|
5878
|
+
const groups = await groupRepo().find({
|
|
5879
|
+
where: { deleted: false },
|
|
5880
|
+
order: { id: "ASC" },
|
|
5881
|
+
relations: ["permissions"]
|
|
5882
|
+
});
|
|
5883
|
+
const entities = [...allowEntities].sort();
|
|
5884
|
+
return json({
|
|
5885
|
+
entities,
|
|
5886
|
+
groups: groups.map((g) => ({
|
|
5887
|
+
id: g.id,
|
|
5888
|
+
name: g.name,
|
|
5889
|
+
permissions: (g.permissions ?? []).filter((p) => !p.deleted).map((p) => ({
|
|
5890
|
+
entity: p.entity,
|
|
5891
|
+
canCreate: p.canCreate,
|
|
5892
|
+
canRead: p.canRead,
|
|
5893
|
+
canUpdate: p.canUpdate,
|
|
5894
|
+
canDelete: p.canDelete
|
|
5895
|
+
}))
|
|
5896
|
+
}))
|
|
5897
|
+
});
|
|
5898
|
+
},
|
|
5899
|
+
async createGroup(req) {
|
|
5900
|
+
const err = await gate();
|
|
5901
|
+
if (err) return err;
|
|
5902
|
+
try {
|
|
5903
|
+
const body = await req.json();
|
|
5904
|
+
const name = body?.name?.trim();
|
|
5905
|
+
if (!name) return json({ error: "Name is required" }, { status: 400 });
|
|
5906
|
+
const repo = groupRepo();
|
|
5907
|
+
const existing = await repo.findOne({ where: { name } });
|
|
5908
|
+
if (existing) return json({ error: "Group name already exists" }, { status: 400 });
|
|
5909
|
+
const g = await repo.save(repo.create({ name }));
|
|
5910
|
+
return json({ id: g.id, name: g.name, permissions: [] }, { status: 201 });
|
|
5911
|
+
} catch {
|
|
5912
|
+
return json({ error: "Server error" }, { status: 500 });
|
|
5913
|
+
}
|
|
5914
|
+
},
|
|
5915
|
+
async patchGroup(req, idStr) {
|
|
5916
|
+
const err = await gate();
|
|
5917
|
+
if (err) return err;
|
|
5918
|
+
const id = parseInt(idStr, 10);
|
|
5919
|
+
if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
|
|
5920
|
+
try {
|
|
5921
|
+
const body = await req.json();
|
|
5922
|
+
const name = body?.name?.trim();
|
|
5923
|
+
if (!name) return json({ error: "Name is required" }, { status: 400 });
|
|
5924
|
+
const repo = groupRepo();
|
|
5925
|
+
const g = await repo.findOne({ where: { id, deleted: false } });
|
|
5926
|
+
if (!g) return json({ error: "Not found" }, { status: 404 });
|
|
5927
|
+
if (isSuperAdminGroupName(g.name) && !isSuperAdminGroupName(name)) {
|
|
5928
|
+
return json({ error: "Cannot rename the administrator group" }, { status: 400 });
|
|
5929
|
+
}
|
|
5930
|
+
const dup = await repo.findOne({ where: { name } });
|
|
5931
|
+
if (dup && dup.id !== id) return json({ error: "Name already in use" }, { status: 400 });
|
|
5932
|
+
g.name = name;
|
|
5933
|
+
await repo.save(g);
|
|
5934
|
+
return json({ id: g.id, name: g.name });
|
|
5935
|
+
} catch {
|
|
5936
|
+
return json({ error: "Server error" }, { status: 500 });
|
|
5937
|
+
}
|
|
5938
|
+
},
|
|
5939
|
+
async deleteGroup(idStr) {
|
|
5940
|
+
const err = await gate();
|
|
5941
|
+
if (err) return err;
|
|
5942
|
+
const id = parseInt(idStr, 10);
|
|
5943
|
+
if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
|
|
5944
|
+
const repo = groupRepo();
|
|
5945
|
+
const g = await repo.findOne({ where: { id, deleted: false } });
|
|
5946
|
+
if (!g) return json({ error: "Not found" }, { status: 404 });
|
|
5947
|
+
if (isSuperAdminGroupName(g.name)) return json({ error: "Cannot delete the administrator group" }, { status: 400 });
|
|
5948
|
+
const userCount = await userRepo().count({ where: { groupId: id } });
|
|
5949
|
+
if (userCount > 0) return json({ error: "Reassign users before deleting this group" }, { status: 409 });
|
|
5950
|
+
await permRepo().delete({ groupId: id });
|
|
5951
|
+
await repo.update(id, { deleted: true, deletedAt: /* @__PURE__ */ new Date() });
|
|
5952
|
+
return json({ ok: true });
|
|
5953
|
+
},
|
|
5954
|
+
async putPermissions(req, idStr) {
|
|
5955
|
+
const err = await gate();
|
|
5956
|
+
if (err) return err;
|
|
5957
|
+
const groupId = parseInt(idStr, 10);
|
|
5958
|
+
if (!Number.isFinite(groupId)) return json({ error: "Invalid id" }, { status: 400 });
|
|
5959
|
+
const groupRepository = groupRepo();
|
|
5960
|
+
const g = await groupRepository.findOne({ where: { id: groupId, deleted: false } });
|
|
5961
|
+
if (!g) return json({ error: "Group not found" }, { status: 404 });
|
|
5962
|
+
try {
|
|
5963
|
+
const body = await req.json();
|
|
5964
|
+
const rows = body?.permissions;
|
|
5965
|
+
if (!Array.isArray(rows)) return json({ error: "permissions array required" }, { status: 400 });
|
|
5966
|
+
for (const r of rows) {
|
|
5967
|
+
if (!r?.entity || !allowEntities.has(r.entity)) {
|
|
5968
|
+
return json({ error: `Invalid entity: ${r?.entity ?? ""}` }, { status: 400 });
|
|
5969
|
+
}
|
|
5970
|
+
}
|
|
5971
|
+
await dataSource.transaction(async (em) => {
|
|
5972
|
+
await em.getRepository(entityMap.permissions).delete({ groupId });
|
|
5973
|
+
for (const r of rows) {
|
|
5974
|
+
await em.getRepository(entityMap.permissions).save(
|
|
5975
|
+
em.getRepository(entityMap.permissions).create({
|
|
5976
|
+
groupId,
|
|
5977
|
+
entity: r.entity,
|
|
5978
|
+
canCreate: !!r.canCreate,
|
|
5979
|
+
canRead: !!r.canRead,
|
|
5980
|
+
canUpdate: !!r.canUpdate,
|
|
5981
|
+
canDelete: !!r.canDelete
|
|
5982
|
+
})
|
|
5983
|
+
);
|
|
5984
|
+
}
|
|
5985
|
+
});
|
|
5986
|
+
const updated = await groupRepository.findOne({
|
|
5987
|
+
where: { id: groupId },
|
|
5988
|
+
relations: ["permissions"]
|
|
5989
|
+
});
|
|
5990
|
+
return json({
|
|
5991
|
+
id: groupId,
|
|
5992
|
+
permissions: (updated?.permissions ?? []).map((p) => ({
|
|
5993
|
+
entity: p.entity,
|
|
5994
|
+
canCreate: p.canCreate,
|
|
5995
|
+
canRead: p.canRead,
|
|
5996
|
+
canUpdate: p.canUpdate,
|
|
5997
|
+
canDelete: p.canDelete
|
|
5998
|
+
}))
|
|
5999
|
+
});
|
|
6000
|
+
} catch {
|
|
6001
|
+
return json({ error: "Server error" }, { status: 500 });
|
|
6002
|
+
}
|
|
6003
|
+
}
|
|
6004
|
+
};
|
|
6005
|
+
}
|
|
6006
|
+
|
|
4519
6007
|
// src/api/cms-api-handler.ts
|
|
4520
|
-
var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
|
|
6008
|
+
var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
|
|
6009
|
+
"users",
|
|
6010
|
+
"password_reset_tokens",
|
|
6011
|
+
"user_groups",
|
|
6012
|
+
"permissions",
|
|
6013
|
+
"comments",
|
|
6014
|
+
"form_fields",
|
|
6015
|
+
"configs",
|
|
6016
|
+
"carts",
|
|
6017
|
+
"cart_items",
|
|
6018
|
+
"wishlists",
|
|
6019
|
+
"wishlist_items"
|
|
6020
|
+
]);
|
|
4521
6021
|
function createCmsApiHandler(config) {
|
|
4522
6022
|
const {
|
|
4523
6023
|
dataSource,
|
|
@@ -4538,7 +6038,9 @@ function createCmsApiHandler(config) {
|
|
|
4538
6038
|
userAvatar,
|
|
4539
6039
|
userProfile,
|
|
4540
6040
|
settings: settingsConfig,
|
|
4541
|
-
chat: chatConfig
|
|
6041
|
+
chat: chatConfig,
|
|
6042
|
+
requireEntityPermission: reqEntityPerm,
|
|
6043
|
+
getSessionUser
|
|
4542
6044
|
} = config;
|
|
4543
6045
|
const analytics = analyticsConfig ?? (getCms ? {
|
|
4544
6046
|
json: config.json,
|
|
@@ -4559,27 +6061,51 @@ function createCmsApiHandler(config) {
|
|
|
4559
6061
|
...userAuthConfig,
|
|
4560
6062
|
sendEmail: async (opts) => {
|
|
4561
6063
|
const cms = await getCms();
|
|
6064
|
+
const queue = cms.getPlugin("queue");
|
|
6065
|
+
const companyDetails = config.getCompanyDetails ? await config.getCompanyDetails() : {};
|
|
6066
|
+
const resetLink = typeof opts.resetLink === "string" && opts.resetLink.trim() || typeof opts.text === "string" && opts.text.trim() || (typeof opts.html === "string" ? opts.html.match(/href\s*=\s*["']([^"']+)["']/)?.[1] ?? "" : "");
|
|
6067
|
+
const ctx = { resetLink, companyDetails };
|
|
6068
|
+
if (queue) {
|
|
6069
|
+
const { queueEmail: queueEmail2 } = await Promise.resolve().then(() => (init_email_queue(), email_queue_exports));
|
|
6070
|
+
await queueEmail2(cms, { to: opts.to, templateName: "passwordReset", ctx });
|
|
6071
|
+
return;
|
|
6072
|
+
}
|
|
4562
6073
|
const email = cms.getPlugin("email");
|
|
4563
6074
|
if (!email?.send) return;
|
|
4564
|
-
const
|
|
4565
|
-
|
|
4566
|
-
await email.send({ subject, html, text, to: opts.to });
|
|
6075
|
+
const rendered = email.renderTemplate("passwordReset", ctx);
|
|
6076
|
+
await email.send({ subject: rendered.subject, html: rendered.html, text: rendered.text, to: opts.to });
|
|
4567
6077
|
}
|
|
4568
6078
|
} : userAuthConfig;
|
|
4569
|
-
const crudOpts = {
|
|
6079
|
+
const crudOpts = {
|
|
6080
|
+
requireAuth: config.requireAuth,
|
|
6081
|
+
json: config.json,
|
|
6082
|
+
requireEntityPermission: reqEntityPerm
|
|
6083
|
+
};
|
|
4570
6084
|
const crud = createCrudHandler(dataSource, entityMap, crudOpts);
|
|
4571
6085
|
const crudById = createCrudByIdHandler(dataSource, entityMap, crudOpts);
|
|
6086
|
+
const mergePerm = (c) => !c ? void 0 : reqEntityPerm ? { ...c, requireEntityPermission: reqEntityPerm } : c;
|
|
6087
|
+
const adminRoles = getSessionUser && createAdminRolesHandlers({
|
|
6088
|
+
dataSource,
|
|
6089
|
+
entityMap,
|
|
6090
|
+
json: config.json,
|
|
6091
|
+
getSessionUser
|
|
6092
|
+
});
|
|
4572
6093
|
const userAuthRouter = userAuth ? createUserAuthApiRouter(userAuth) : null;
|
|
4573
|
-
const dashboardGet = dashboard ? createDashboardStatsHandler(dashboard) : null;
|
|
6094
|
+
const dashboardGet = dashboard ? createDashboardStatsHandler(mergePerm(dashboard) ?? dashboard) : null;
|
|
4574
6095
|
const analyticsHandlers = analytics ? createAnalyticsHandlers(analytics) : null;
|
|
4575
|
-
const uploadPost = upload ? createUploadHandler(upload) : null;
|
|
6096
|
+
const uploadPost = upload ? createUploadHandler(mergePerm(upload) ?? upload) : null;
|
|
4576
6097
|
const blogBySlugGet = blogBySlug ? createBlogBySlugHandler(blogBySlug) : null;
|
|
4577
6098
|
const formBySlugGet = formBySlug ? createFormBySlugHandler(formBySlug) : null;
|
|
4578
|
-
const formSaveHandlers = formSaveConfig ? createFormSaveHandlers(formSaveConfig) : null;
|
|
6099
|
+
const formSaveHandlers = formSaveConfig ? createFormSaveHandlers(mergePerm(formSaveConfig) ?? formSaveConfig) : null;
|
|
4579
6100
|
const formSubmissionPost = formSubmissionConfig ? createFormSubmissionHandler(formSubmissionConfig) : null;
|
|
4580
|
-
const formSubmissionGetById = formSubmissionGetByIdConfig ? createFormSubmissionGetByIdHandler(formSubmissionGetByIdConfig) : null;
|
|
4581
|
-
const formSubmissionList = formSubmissionGetByIdConfig ? createFormSubmissionListHandler(formSubmissionGetByIdConfig) : null;
|
|
4582
|
-
const
|
|
6101
|
+
const formSubmissionGetById = formSubmissionGetByIdConfig ? createFormSubmissionGetByIdHandler(mergePerm(formSubmissionGetByIdConfig) ?? formSubmissionGetByIdConfig) : null;
|
|
6102
|
+
const formSubmissionList = formSubmissionGetByIdConfig ? createFormSubmissionListHandler(mergePerm(formSubmissionGetByIdConfig) ?? formSubmissionGetByIdConfig) : null;
|
|
6103
|
+
const usersApiMerged = usersApi && getCms ? {
|
|
6104
|
+
...usersApi,
|
|
6105
|
+
getCms: usersApi.getCms ?? getCms,
|
|
6106
|
+
getCompanyDetails: usersApi.getCompanyDetails ?? config.getCompanyDetails
|
|
6107
|
+
} : usersApi;
|
|
6108
|
+
const usersHandlers = usersApiMerged ? createUsersApiHandlers(mergePerm(usersApiMerged) ?? usersApiMerged) : null;
|
|
4583
6109
|
const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
|
|
4584
6110
|
const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
|
|
4585
6111
|
const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
|
|
@@ -4590,13 +6116,41 @@ function createCmsApiHandler(config) {
|
|
|
4590
6116
|
}
|
|
4591
6117
|
return {
|
|
4592
6118
|
async handle(method, path, req) {
|
|
6119
|
+
const perm = reqEntityPerm;
|
|
6120
|
+
async function analyticsGate() {
|
|
6121
|
+
const a = await config.requireAuth(req);
|
|
6122
|
+
if (a) return a;
|
|
6123
|
+
if (perm) return perm(req, "analytics", "read");
|
|
6124
|
+
return null;
|
|
6125
|
+
}
|
|
6126
|
+
if (path[0] === "admin" && path[1] === "roles") {
|
|
6127
|
+
if (!adminRoles) return config.json({ error: "Not found" }, { status: 404 });
|
|
6128
|
+
if (path.length === 2 && method === "GET") return adminRoles.list();
|
|
6129
|
+
if (path.length === 2 && method === "POST") return adminRoles.createGroup(req);
|
|
6130
|
+
if (path.length === 3 && method === "PATCH") return adminRoles.patchGroup(req, path[2]);
|
|
6131
|
+
if (path.length === 3 && method === "DELETE") return adminRoles.deleteGroup(path[2]);
|
|
6132
|
+
if (path.length === 4 && path[3] === "permissions" && method === "PUT") return adminRoles.putPermissions(req, path[2]);
|
|
6133
|
+
return config.json({ error: "Not found" }, { status: 404 });
|
|
6134
|
+
}
|
|
4593
6135
|
if (path[0] === "dashboard" && path[1] === "stats" && path.length === 2 && method === "GET" && dashboardGet) {
|
|
4594
6136
|
return dashboardGet(req);
|
|
4595
6137
|
}
|
|
4596
6138
|
if (path[0] === "analytics" && analyticsHandlers) {
|
|
4597
|
-
if (path.length === 1 && method === "GET")
|
|
4598
|
-
|
|
4599
|
-
|
|
6139
|
+
if (path.length === 1 && method === "GET") {
|
|
6140
|
+
const g = await analyticsGate();
|
|
6141
|
+
if (g) return g;
|
|
6142
|
+
return analyticsHandlers.GET(req);
|
|
6143
|
+
}
|
|
6144
|
+
if (path.length === 2 && path[1] === "property-id" && method === "GET") {
|
|
6145
|
+
const g = await analyticsGate();
|
|
6146
|
+
if (g) return g;
|
|
6147
|
+
return analyticsHandlers.propertyId();
|
|
6148
|
+
}
|
|
6149
|
+
if (path.length === 2 && path[1] === "permissions" && method === "GET") {
|
|
6150
|
+
const g = await analyticsGate();
|
|
6151
|
+
if (g) return g;
|
|
6152
|
+
return analyticsHandlers.permissions();
|
|
6153
|
+
}
|
|
4600
6154
|
}
|
|
4601
6155
|
if (path[0] === "upload" && path.length === 1 && method === "POST" && uploadPost) return uploadPost(req);
|
|
4602
6156
|
if (path[0] === "blogs" && path[1] === "slug" && path.length === 3 && method === "GET" && blogBySlugGet) {
|
|
@@ -4640,8 +6194,24 @@ function createCmsApiHandler(config) {
|
|
|
4640
6194
|
return userAuthRouter.POST(req, path[1]);
|
|
4641
6195
|
}
|
|
4642
6196
|
if (path[0] === "settings" && path.length === 2 && settingsHandlers) {
|
|
4643
|
-
|
|
4644
|
-
|
|
6197
|
+
const group = path[1];
|
|
6198
|
+
const isPublic = settingsConfig?.publicGetGroups?.includes(group);
|
|
6199
|
+
if (method === "GET") {
|
|
6200
|
+
if (!isPublic && perm) {
|
|
6201
|
+
const a = await config.requireAuth(req);
|
|
6202
|
+
if (a) return a;
|
|
6203
|
+
const pe = await perm(req, "settings", "read");
|
|
6204
|
+
if (pe) return pe;
|
|
6205
|
+
}
|
|
6206
|
+
return settingsHandlers.GET(req, group);
|
|
6207
|
+
}
|
|
6208
|
+
if (method === "PUT") {
|
|
6209
|
+
if (perm) {
|
|
6210
|
+
const pe = await perm(req, "settings", "update");
|
|
6211
|
+
if (pe) return pe;
|
|
6212
|
+
}
|
|
6213
|
+
return settingsHandlers.PUT(req, group);
|
|
6214
|
+
}
|
|
4645
6215
|
}
|
|
4646
6216
|
if (path[0] === "chat" && chatHandlers) {
|
|
4647
6217
|
if (path.length === 2 && path[1] === "identify" && method === "POST") return chatHandlers.identify(req);
|
|
@@ -4679,21 +6249,1025 @@ function createCmsApiHandler(config) {
|
|
|
4679
6249
|
};
|
|
4680
6250
|
}
|
|
4681
6251
|
|
|
6252
|
+
// src/api/storefront-handlers.ts
|
|
6253
|
+
import { In, IsNull as IsNull2 } from "typeorm";
|
|
6254
|
+
|
|
6255
|
+
// src/lib/is-valid-signup-email.ts
|
|
6256
|
+
var MAX_EMAIL = 254;
|
|
6257
|
+
var MAX_LOCAL = 64;
|
|
6258
|
+
function isValidSignupEmail(email) {
|
|
6259
|
+
if (!email || email.length > MAX_EMAIL) return false;
|
|
6260
|
+
const at = email.indexOf("@");
|
|
6261
|
+
if (at <= 0 || at !== email.lastIndexOf("@")) return false;
|
|
6262
|
+
const local = email.slice(0, at);
|
|
6263
|
+
const domain = email.slice(at + 1);
|
|
6264
|
+
if (!local || local.length > MAX_LOCAL || !domain || domain.length > 253) return false;
|
|
6265
|
+
if (local.startsWith(".") || local.endsWith(".") || local.includes("..")) return false;
|
|
6266
|
+
if (domain.startsWith(".") || domain.endsWith(".") || domain.includes("..")) return false;
|
|
6267
|
+
if (!/^[a-z0-9._%+-]+$/i.test(local)) return false;
|
|
6268
|
+
if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/i.test(domain)) return false;
|
|
6269
|
+
const tld = domain.split(".").pop();
|
|
6270
|
+
return tld.length >= 2;
|
|
6271
|
+
}
|
|
6272
|
+
|
|
6273
|
+
// src/api/storefront-handlers.ts
|
|
6274
|
+
init_email_queue();
|
|
6275
|
+
var GUEST_COOKIE = "guest_id";
|
|
6276
|
+
var ONE_YEAR = 60 * 60 * 24 * 365;
|
|
6277
|
+
function parseCookies(header) {
|
|
6278
|
+
const out = {};
|
|
6279
|
+
if (!header) return out;
|
|
6280
|
+
for (const part of header.split(";")) {
|
|
6281
|
+
const i = part.indexOf("=");
|
|
6282
|
+
if (i === -1) continue;
|
|
6283
|
+
const k = part.slice(0, i).trim();
|
|
6284
|
+
const v = part.slice(i + 1).trim();
|
|
6285
|
+
out[k] = decodeURIComponent(v);
|
|
6286
|
+
}
|
|
6287
|
+
return out;
|
|
6288
|
+
}
|
|
6289
|
+
function guestCookieHeader(name, token) {
|
|
6290
|
+
return `${name}=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ONE_YEAR}`;
|
|
6291
|
+
}
|
|
6292
|
+
function orderNumber() {
|
|
6293
|
+
return `ORD-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
6294
|
+
}
|
|
6295
|
+
var SIGNUP_VERIFY_EXPIRY_HOURS = 72;
|
|
6296
|
+
function createStorefrontApiHandler(config) {
|
|
6297
|
+
const { dataSource, entityMap, json, getSessionUser, getCms, getCompanyDetails, publicSiteUrl } = config;
|
|
6298
|
+
const cookieName = config.guestCookieName ?? GUEST_COOKIE;
|
|
6299
|
+
const cartRepo = () => dataSource.getRepository(entityMap.carts);
|
|
6300
|
+
const cartItemRepo = () => dataSource.getRepository(entityMap.cart_items);
|
|
6301
|
+
const productRepo = () => dataSource.getRepository(entityMap.products);
|
|
6302
|
+
const contactRepo = () => dataSource.getRepository(entityMap.contacts);
|
|
6303
|
+
const addressRepo = () => dataSource.getRepository(entityMap.addresses);
|
|
6304
|
+
const orderRepo = () => dataSource.getRepository(entityMap.orders);
|
|
6305
|
+
const orderItemRepo = () => dataSource.getRepository(entityMap.order_items);
|
|
6306
|
+
const wishlistRepo = () => dataSource.getRepository(entityMap.wishlists);
|
|
6307
|
+
const wishlistItemRepo = () => dataSource.getRepository(entityMap.wishlist_items);
|
|
6308
|
+
const userRepo = () => dataSource.getRepository(entityMap.users);
|
|
6309
|
+
const tokenRepo = () => dataSource.getRepository(entityMap.password_reset_tokens);
|
|
6310
|
+
const collectionRepo = () => dataSource.getRepository(entityMap.collections);
|
|
6311
|
+
const groupRepo = () => dataSource.getRepository(entityMap.user_groups);
|
|
6312
|
+
async function ensureContactForUser(userId) {
|
|
6313
|
+
let c = await contactRepo().findOne({ where: { userId, deleted: false } });
|
|
6314
|
+
if (c) return c;
|
|
6315
|
+
const u = await userRepo().findOne({ where: { id: userId } });
|
|
6316
|
+
if (!u) return null;
|
|
6317
|
+
const unclaimed = await contactRepo().findOne({
|
|
6318
|
+
where: { email: u.email, userId: IsNull2(), deleted: false }
|
|
6319
|
+
});
|
|
6320
|
+
if (unclaimed) {
|
|
6321
|
+
await contactRepo().update(unclaimed.id, { userId });
|
|
6322
|
+
return { id: unclaimed.id };
|
|
6323
|
+
}
|
|
6324
|
+
const created = await contactRepo().save(
|
|
6325
|
+
contactRepo().create({
|
|
6326
|
+
name: u.name,
|
|
6327
|
+
email: u.email,
|
|
6328
|
+
phone: null,
|
|
6329
|
+
userId,
|
|
6330
|
+
deleted: false
|
|
6331
|
+
})
|
|
6332
|
+
);
|
|
6333
|
+
return { id: created.id };
|
|
6334
|
+
}
|
|
6335
|
+
async function getOrCreateCart(req) {
|
|
6336
|
+
const u = await getSessionUser();
|
|
6337
|
+
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
6338
|
+
if (Number.isFinite(uid)) {
|
|
6339
|
+
const contact = await ensureContactForUser(uid);
|
|
6340
|
+
if (!contact) return { cart: {}, setCookie: null, err: json({ error: "User not found" }, { status: 400 }) };
|
|
6341
|
+
let cart2 = await cartRepo().findOne({
|
|
6342
|
+
where: { contactId: contact.id },
|
|
6343
|
+
relations: ["items", "items.product"]
|
|
6344
|
+
});
|
|
6345
|
+
if (!cart2) {
|
|
6346
|
+
cart2 = await cartRepo().save(
|
|
6347
|
+
cartRepo().create({ contactId: contact.id, guestToken: null, currency: "INR" })
|
|
6348
|
+
);
|
|
6349
|
+
cart2 = await cartRepo().findOne({
|
|
6350
|
+
where: { id: cart2.id },
|
|
6351
|
+
relations: ["items", "items.product"]
|
|
6352
|
+
});
|
|
6353
|
+
}
|
|
6354
|
+
return { cart: cart2, setCookie: null, err: null };
|
|
6355
|
+
}
|
|
6356
|
+
const cookies = parseCookies(req.headers.get("cookie"));
|
|
6357
|
+
let token = cookies[cookieName] || "";
|
|
6358
|
+
if (!token) {
|
|
6359
|
+
token = crypto.randomUUID();
|
|
6360
|
+
let cart2 = await cartRepo().findOne({
|
|
6361
|
+
where: { guestToken: token },
|
|
6362
|
+
relations: ["items", "items.product"]
|
|
6363
|
+
});
|
|
6364
|
+
if (!cart2) {
|
|
6365
|
+
cart2 = await cartRepo().save(
|
|
6366
|
+
cartRepo().create({ guestToken: token, contactId: null, currency: "INR" })
|
|
6367
|
+
);
|
|
6368
|
+
cart2 = await cartRepo().findOne({
|
|
6369
|
+
where: { id: cart2.id },
|
|
6370
|
+
relations: ["items", "items.product"]
|
|
6371
|
+
});
|
|
6372
|
+
}
|
|
6373
|
+
return { cart: cart2, setCookie: guestCookieHeader(cookieName, token), err: null };
|
|
6374
|
+
}
|
|
6375
|
+
let cart = await cartRepo().findOne({
|
|
6376
|
+
where: { guestToken: token },
|
|
6377
|
+
relations: ["items", "items.product"]
|
|
6378
|
+
});
|
|
6379
|
+
if (!cart) {
|
|
6380
|
+
cart = await cartRepo().save(
|
|
6381
|
+
cartRepo().create({ guestToken: token, contactId: null, currency: "INR" })
|
|
6382
|
+
);
|
|
6383
|
+
cart = await cartRepo().findOne({
|
|
6384
|
+
where: { id: cart.id },
|
|
6385
|
+
relations: ["items", "items.product"]
|
|
6386
|
+
});
|
|
6387
|
+
}
|
|
6388
|
+
return { cart, setCookie: null, err: null };
|
|
6389
|
+
}
|
|
6390
|
+
function primaryProductImageUrl(metadata) {
|
|
6391
|
+
const meta = metadata;
|
|
6392
|
+
const images = meta?.images;
|
|
6393
|
+
if (!Array.isArray(images) || !images.length) return null;
|
|
6394
|
+
const sorted = images.filter((i) => i?.url);
|
|
6395
|
+
if (!sorted.length) return null;
|
|
6396
|
+
const di = sorted.findIndex((i) => i.isDefault);
|
|
6397
|
+
if (di > 0) {
|
|
6398
|
+
const [d] = sorted.splice(di, 1);
|
|
6399
|
+
sorted.unshift(d);
|
|
6400
|
+
}
|
|
6401
|
+
return sorted[0].url;
|
|
6402
|
+
}
|
|
6403
|
+
function serializeCart(cart) {
|
|
6404
|
+
const items = cart.items || [];
|
|
6405
|
+
return {
|
|
6406
|
+
id: cart.id,
|
|
6407
|
+
currency: cart.currency,
|
|
6408
|
+
items: items.map((it) => {
|
|
6409
|
+
const p = it.product;
|
|
6410
|
+
return {
|
|
6411
|
+
id: it.id,
|
|
6412
|
+
productId: it.productId,
|
|
6413
|
+
quantity: it.quantity,
|
|
6414
|
+
metadata: it.metadata,
|
|
6415
|
+
product: p ? {
|
|
6416
|
+
id: p.id,
|
|
6417
|
+
name: p.name,
|
|
6418
|
+
slug: p.slug,
|
|
6419
|
+
price: p.price,
|
|
6420
|
+
sku: p.sku,
|
|
6421
|
+
image: primaryProductImageUrl(p.metadata)
|
|
6422
|
+
} : null
|
|
6423
|
+
};
|
|
6424
|
+
})
|
|
6425
|
+
};
|
|
6426
|
+
}
|
|
6427
|
+
function serializeProduct(p) {
|
|
6428
|
+
return {
|
|
6429
|
+
id: p.id,
|
|
6430
|
+
name: p.name,
|
|
6431
|
+
slug: p.slug,
|
|
6432
|
+
sku: p.sku,
|
|
6433
|
+
hsn: p.hsn,
|
|
6434
|
+
price: p.price,
|
|
6435
|
+
compareAtPrice: p.compareAtPrice,
|
|
6436
|
+
status: p.status,
|
|
6437
|
+
collectionId: p.collectionId,
|
|
6438
|
+
metadata: p.metadata
|
|
6439
|
+
};
|
|
6440
|
+
}
|
|
6441
|
+
return {
|
|
6442
|
+
async handle(method, path, req) {
|
|
6443
|
+
try {
|
|
6444
|
+
let serializeAddress2 = function(a) {
|
|
6445
|
+
return {
|
|
6446
|
+
id: a.id,
|
|
6447
|
+
contactId: a.contactId,
|
|
6448
|
+
tag: a.tag,
|
|
6449
|
+
line1: a.line1,
|
|
6450
|
+
line2: a.line2,
|
|
6451
|
+
city: a.city,
|
|
6452
|
+
state: a.state,
|
|
6453
|
+
postalCode: a.postalCode,
|
|
6454
|
+
country: a.country
|
|
6455
|
+
};
|
|
6456
|
+
};
|
|
6457
|
+
var serializeAddress = serializeAddress2;
|
|
6458
|
+
if (path[0] === "products" && path.length === 1 && method === "GET") {
|
|
6459
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
6460
|
+
const collectionSlug = url.searchParams.get("collection")?.trim();
|
|
6461
|
+
const collectionId = url.searchParams.get("collectionId");
|
|
6462
|
+
const limit = Math.min(100, Math.max(1, parseInt(url.searchParams.get("limit") || "20", 10)));
|
|
6463
|
+
const offset = Math.max(0, parseInt(url.searchParams.get("offset") || "0", 10));
|
|
6464
|
+
const where = { status: "available", deleted: false };
|
|
6465
|
+
let collectionFilter = null;
|
|
6466
|
+
if (collectionSlug) {
|
|
6467
|
+
let col = null;
|
|
6468
|
+
if (/^\d+$/.test(collectionSlug)) {
|
|
6469
|
+
col = await collectionRepo().findOne({
|
|
6470
|
+
where: {
|
|
6471
|
+
id: parseInt(collectionSlug, 10),
|
|
6472
|
+
active: true,
|
|
6473
|
+
deleted: false
|
|
6474
|
+
}
|
|
6475
|
+
});
|
|
6476
|
+
} else {
|
|
6477
|
+
col = await collectionRepo().createQueryBuilder("c").where("LOWER(c.slug) = LOWER(:slug)", { slug: collectionSlug }).andWhere("c.active = :a", { a: true }).andWhere("c.deleted = :d", { d: false }).getOne();
|
|
6478
|
+
}
|
|
6479
|
+
if (!col) {
|
|
6480
|
+
return json({ products: [], total: 0, collection: null });
|
|
6481
|
+
}
|
|
6482
|
+
where.collectionId = col.id;
|
|
6483
|
+
collectionFilter = { name: col.name, slug: col.slug };
|
|
6484
|
+
} else if (collectionId) {
|
|
6485
|
+
const cid = parseInt(collectionId, 10);
|
|
6486
|
+
if (Number.isFinite(cid)) where.collectionId = cid;
|
|
6487
|
+
}
|
|
6488
|
+
const [items, total] = await productRepo().findAndCount({
|
|
6489
|
+
where,
|
|
6490
|
+
order: { id: "ASC" },
|
|
6491
|
+
take: limit,
|
|
6492
|
+
skip: offset
|
|
6493
|
+
});
|
|
6494
|
+
return json({
|
|
6495
|
+
products: items.map(serializeProduct),
|
|
6496
|
+
total,
|
|
6497
|
+
...collectionFilter && { collection: collectionFilter }
|
|
6498
|
+
});
|
|
6499
|
+
}
|
|
6500
|
+
if (path[0] === "products" && path.length === 2 && method === "GET") {
|
|
6501
|
+
const idOrSlug = path[1];
|
|
6502
|
+
const byId = /^\d+$/.test(idOrSlug);
|
|
6503
|
+
const product = await productRepo().findOne({
|
|
6504
|
+
where: byId ? { id: parseInt(idOrSlug, 10), status: "available", deleted: false } : { slug: idOrSlug, status: "available", deleted: false },
|
|
6505
|
+
relations: ["attributes", "attributes.attribute"]
|
|
6506
|
+
});
|
|
6507
|
+
if (!product) return json({ error: "Not found" }, { status: 404 });
|
|
6508
|
+
const p = product;
|
|
6509
|
+
const attrRows = p.attributes ?? [];
|
|
6510
|
+
const attributeTags = attrRows.map((pa) => ({
|
|
6511
|
+
name: pa.attribute?.name ?? "",
|
|
6512
|
+
value: String(pa.value ?? "")
|
|
6513
|
+
})).filter((t) => t.name || t.value);
|
|
6514
|
+
return json({ ...serializeProduct(p), attributes: attributeTags });
|
|
6515
|
+
}
|
|
6516
|
+
if (path[0] === "collections" && path.length === 1 && method === "GET") {
|
|
6517
|
+
const items = await collectionRepo().find({
|
|
6518
|
+
where: { active: true, deleted: false },
|
|
6519
|
+
order: { sortOrder: "ASC", id: "ASC" }
|
|
6520
|
+
});
|
|
6521
|
+
const ids = items.map((c) => c.id);
|
|
6522
|
+
const countByCollection = {};
|
|
6523
|
+
if (ids.length > 0) {
|
|
6524
|
+
const rows = await productRepo().createQueryBuilder("p").select("p.collectionId", "collectionId").addSelect("COUNT(p.id)", "cnt").where("p.collectionId IN (:...ids)", { ids }).andWhere("p.status = :status", { status: "available" }).andWhere("p.deleted = :del", { del: false }).groupBy("p.collectionId").getRawMany();
|
|
6525
|
+
for (const r of rows) {
|
|
6526
|
+
const cid = r.collectionId;
|
|
6527
|
+
if (cid != null) countByCollection[Number(cid)] = parseInt(String(r.cnt), 10);
|
|
6528
|
+
}
|
|
6529
|
+
}
|
|
6530
|
+
return json({
|
|
6531
|
+
collections: items.map((c) => {
|
|
6532
|
+
const col = c;
|
|
6533
|
+
const id = col.id;
|
|
6534
|
+
return {
|
|
6535
|
+
id,
|
|
6536
|
+
name: col.name,
|
|
6537
|
+
slug: col.slug,
|
|
6538
|
+
description: col.description,
|
|
6539
|
+
image: col.image,
|
|
6540
|
+
productCount: countByCollection[id] ?? 0
|
|
6541
|
+
};
|
|
6542
|
+
})
|
|
6543
|
+
});
|
|
6544
|
+
}
|
|
6545
|
+
if (path[0] === "collections" && path.length === 2 && method === "GET") {
|
|
6546
|
+
const idOrSlug = path[1];
|
|
6547
|
+
const byId = /^\d+$/.test(idOrSlug);
|
|
6548
|
+
const collection = await collectionRepo().findOne({
|
|
6549
|
+
where: byId ? { id: parseInt(idOrSlug, 10), active: true, deleted: false } : { slug: idOrSlug, active: true, deleted: false }
|
|
6550
|
+
});
|
|
6551
|
+
if (!collection) return json({ error: "Not found" }, { status: 404 });
|
|
6552
|
+
const col = collection;
|
|
6553
|
+
const products = await productRepo().find({
|
|
6554
|
+
where: { collectionId: col.id, status: "available", deleted: false },
|
|
6555
|
+
order: { id: "ASC" }
|
|
6556
|
+
});
|
|
6557
|
+
return json({
|
|
6558
|
+
id: col.id,
|
|
6559
|
+
name: col.name,
|
|
6560
|
+
slug: col.slug,
|
|
6561
|
+
description: col.description,
|
|
6562
|
+
image: col.image,
|
|
6563
|
+
products: products.map((p) => serializeProduct(p))
|
|
6564
|
+
});
|
|
6565
|
+
}
|
|
6566
|
+
if (path[0] === "profile" && path.length === 1 && method === "GET") {
|
|
6567
|
+
const u = await getSessionUser();
|
|
6568
|
+
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
6569
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
6570
|
+
const user = await userRepo().findOne({ where: { id: uid }, select: ["id", "name", "email"] });
|
|
6571
|
+
if (!user) return json({ error: "Not found" }, { status: 404 });
|
|
6572
|
+
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
6573
|
+
return json({
|
|
6574
|
+
user: { id: user.id, name: user.name, email: user.email },
|
|
6575
|
+
contact: contact ? {
|
|
6576
|
+
id: contact.id,
|
|
6577
|
+
name: contact.name,
|
|
6578
|
+
email: contact.email,
|
|
6579
|
+
phone: contact.phone
|
|
6580
|
+
} : null
|
|
6581
|
+
});
|
|
6582
|
+
}
|
|
6583
|
+
if (path[0] === "profile" && path.length === 1 && method === "PUT") {
|
|
6584
|
+
const u = await getSessionUser();
|
|
6585
|
+
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
6586
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
6587
|
+
const b = await req.json().catch(() => ({}));
|
|
6588
|
+
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
6589
|
+
if (contact) {
|
|
6590
|
+
const updates = {};
|
|
6591
|
+
if (typeof b.name === "string" && b.name.trim()) updates.name = b.name.trim();
|
|
6592
|
+
if (b.phone !== void 0) updates.phone = b.phone === null || b.phone === "" ? null : String(b.phone);
|
|
6593
|
+
if (Object.keys(updates).length) await contactRepo().update(contact.id, updates);
|
|
6594
|
+
}
|
|
6595
|
+
const user = await userRepo().findOne({ where: { id: uid }, select: ["id", "name", "email"] });
|
|
6596
|
+
if (user && typeof b.name === "string" && b.name.trim()) {
|
|
6597
|
+
await userRepo().update(uid, { name: b.name.trim() });
|
|
6598
|
+
}
|
|
6599
|
+
const updatedContact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
6600
|
+
const updatedUser = await userRepo().findOne({ where: { id: uid }, select: ["id", "name", "email"] });
|
|
6601
|
+
return json({
|
|
6602
|
+
user: updatedUser ? { id: updatedUser.id, name: updatedUser.name, email: updatedUser.email } : null,
|
|
6603
|
+
contact: updatedContact ? {
|
|
6604
|
+
id: updatedContact.id,
|
|
6605
|
+
name: updatedContact.name,
|
|
6606
|
+
email: updatedContact.email,
|
|
6607
|
+
phone: updatedContact.phone
|
|
6608
|
+
} : null
|
|
6609
|
+
});
|
|
6610
|
+
}
|
|
6611
|
+
async function getContactForAddresses() {
|
|
6612
|
+
const u = await getSessionUser();
|
|
6613
|
+
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
6614
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
6615
|
+
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
6616
|
+
if (!contact) return json({ error: "Contact not found" }, { status: 404 });
|
|
6617
|
+
return { contactId: contact.id };
|
|
6618
|
+
}
|
|
6619
|
+
if (path[0] === "addresses" && path.length === 1 && method === "GET") {
|
|
6620
|
+
const contactOrErr = await getContactForAddresses();
|
|
6621
|
+
if (contactOrErr instanceof Response) return contactOrErr;
|
|
6622
|
+
const list = await addressRepo().find({
|
|
6623
|
+
where: { contactId: contactOrErr.contactId },
|
|
6624
|
+
order: { id: "ASC" }
|
|
6625
|
+
});
|
|
6626
|
+
return json({ addresses: list.map((a) => serializeAddress2(a)) });
|
|
6627
|
+
}
|
|
6628
|
+
if (path[0] === "addresses" && path.length === 1 && method === "POST") {
|
|
6629
|
+
const contactOrErr = await getContactForAddresses();
|
|
6630
|
+
if (contactOrErr instanceof Response) return contactOrErr;
|
|
6631
|
+
const b = await req.json().catch(() => ({}));
|
|
6632
|
+
const created = await addressRepo().save(
|
|
6633
|
+
addressRepo().create({
|
|
6634
|
+
contactId: contactOrErr.contactId,
|
|
6635
|
+
tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
|
|
6636
|
+
line1: typeof b.line1 === "string" ? b.line1.trim() || null : null,
|
|
6637
|
+
line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
|
|
6638
|
+
city: typeof b.city === "string" ? b.city.trim() || null : null,
|
|
6639
|
+
state: typeof b.state === "string" ? b.state.trim() || null : null,
|
|
6640
|
+
postalCode: typeof b.postalCode === "string" ? b.postalCode.trim() || null : null,
|
|
6641
|
+
country: typeof b.country === "string" ? b.country.trim() || null : null
|
|
6642
|
+
})
|
|
6643
|
+
);
|
|
6644
|
+
return json(serializeAddress2(created));
|
|
6645
|
+
}
|
|
6646
|
+
if (path[0] === "addresses" && path.length === 2 && (method === "PATCH" || method === "PUT")) {
|
|
6647
|
+
const contactOrErr = await getContactForAddresses();
|
|
6648
|
+
if (contactOrErr instanceof Response) return contactOrErr;
|
|
6649
|
+
const id = parseInt(path[1], 10);
|
|
6650
|
+
if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
|
|
6651
|
+
const existing = await addressRepo().findOne({ where: { id, contactId: contactOrErr.contactId } });
|
|
6652
|
+
if (!existing) return json({ error: "Not found" }, { status: 404 });
|
|
6653
|
+
const b = await req.json().catch(() => ({}));
|
|
6654
|
+
const updates = {};
|
|
6655
|
+
if (b.tag !== void 0) updates.tag = typeof b.tag === "string" ? b.tag.trim() || null : null;
|
|
6656
|
+
if (b.line1 !== void 0) updates.line1 = typeof b.line1 === "string" ? b.line1.trim() || null : null;
|
|
6657
|
+
if (b.line2 !== void 0) updates.line2 = typeof b.line2 === "string" ? b.line2.trim() || null : null;
|
|
6658
|
+
if (b.city !== void 0) updates.city = typeof b.city === "string" ? b.city.trim() || null : null;
|
|
6659
|
+
if (b.state !== void 0) updates.state = typeof b.state === "string" ? b.state.trim() || null : null;
|
|
6660
|
+
if (b.postalCode !== void 0) updates.postalCode = typeof b.postalCode === "string" ? b.postalCode.trim() || null : null;
|
|
6661
|
+
if (b.country !== void 0) updates.country = typeof b.country === "string" ? b.country.trim() || null : null;
|
|
6662
|
+
if (Object.keys(updates).length) await addressRepo().update(id, updates);
|
|
6663
|
+
const updated = await addressRepo().findOne({ where: { id } });
|
|
6664
|
+
return json(serializeAddress2(updated));
|
|
6665
|
+
}
|
|
6666
|
+
if (path[0] === "addresses" && path.length === 2 && method === "DELETE") {
|
|
6667
|
+
const contactOrErr = await getContactForAddresses();
|
|
6668
|
+
if (contactOrErr instanceof Response) return contactOrErr;
|
|
6669
|
+
const id = parseInt(path[1], 10);
|
|
6670
|
+
if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
|
|
6671
|
+
const existing = await addressRepo().findOne({ where: { id, contactId: contactOrErr.contactId } });
|
|
6672
|
+
if (!existing) return json({ error: "Not found" }, { status: 404 });
|
|
6673
|
+
await addressRepo().delete(id);
|
|
6674
|
+
return json({ deleted: true });
|
|
6675
|
+
}
|
|
6676
|
+
if (path[0] === "verify-email" && path.length === 1 && method === "POST") {
|
|
6677
|
+
const b = await req.json().catch(() => ({}));
|
|
6678
|
+
const token = typeof b.token === "string" ? b.token.trim() : "";
|
|
6679
|
+
if (!token) return json({ error: "token is required" }, { status: 400 });
|
|
6680
|
+
const record = await tokenRepo().findOne({ where: { token } });
|
|
6681
|
+
if (!record || record.expiresAt < /* @__PURE__ */ new Date()) {
|
|
6682
|
+
return json({ error: "Invalid or expired link. Please sign up again or contact support." }, { status: 400 });
|
|
6683
|
+
}
|
|
6684
|
+
const email = record.email;
|
|
6685
|
+
const user = await userRepo().findOne({ where: { email }, select: ["id", "blocked"] });
|
|
6686
|
+
if (!user) return json({ error: "User not found" }, { status: 400 });
|
|
6687
|
+
await userRepo().update(user.id, { blocked: false, updatedAt: /* @__PURE__ */ new Date() });
|
|
6688
|
+
await tokenRepo().delete({ email });
|
|
6689
|
+
return json({ success: true, message: "Email verified. You can sign in." });
|
|
6690
|
+
}
|
|
6691
|
+
if (path[0] === "register" && path.length === 1 && method === "POST") {
|
|
6692
|
+
if (!config.hashPassword) return json({ error: "Registration not configured" }, { status: 501 });
|
|
6693
|
+
const b = await req.json().catch(() => ({}));
|
|
6694
|
+
const name = typeof b.name === "string" ? b.name.trim() : "";
|
|
6695
|
+
const email = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
|
|
6696
|
+
const password = typeof b.password === "string" ? b.password : "";
|
|
6697
|
+
if (!name || !email || !password) return json({ error: "name, email and password are required" }, { status: 400 });
|
|
6698
|
+
if (!isValidSignupEmail(email)) return json({ error: "Invalid email address" }, { status: 400 });
|
|
6699
|
+
const existing = await userRepo().findOne({ where: { email } });
|
|
6700
|
+
if (existing) return json({ error: "User with this email already exists" }, { status: 400 });
|
|
6701
|
+
const customerG = await groupRepo().findOne({ where: { name: "Customer", deleted: false } });
|
|
6702
|
+
const groupId = customerG ? customerG.id : null;
|
|
6703
|
+
const hashed = await config.hashPassword(password);
|
|
6704
|
+
const requireEmailVerification = Boolean(getCms);
|
|
6705
|
+
const newUser = await userRepo().save(
|
|
6706
|
+
userRepo().create({
|
|
6707
|
+
name,
|
|
6708
|
+
email,
|
|
6709
|
+
password: hashed,
|
|
6710
|
+
blocked: requireEmailVerification,
|
|
6711
|
+
groupId,
|
|
6712
|
+
adminAccess: false
|
|
6713
|
+
})
|
|
6714
|
+
);
|
|
6715
|
+
const userId = newUser.id;
|
|
6716
|
+
await linkUnclaimedContactToUser(dataSource, entityMap.contacts, userId, email);
|
|
6717
|
+
let emailVerificationSent = false;
|
|
6718
|
+
if (requireEmailVerification && getCms) {
|
|
6719
|
+
try {
|
|
6720
|
+
const crypto3 = await import("crypto");
|
|
6721
|
+
const rawToken = crypto3.randomBytes(32).toString("hex");
|
|
6722
|
+
const expiresAt = new Date(Date.now() + SIGNUP_VERIFY_EXPIRY_HOURS * 60 * 60 * 1e3);
|
|
6723
|
+
await tokenRepo().save(
|
|
6724
|
+
tokenRepo().create({ email, token: rawToken, expiresAt })
|
|
6725
|
+
);
|
|
6726
|
+
const cms = await getCms();
|
|
6727
|
+
const companyDetails = getCompanyDetails ? await getCompanyDetails() : {};
|
|
6728
|
+
const base = (publicSiteUrl || "").replace(/\/$/, "").trim() || "http://localhost:3000";
|
|
6729
|
+
const verifyEmailUrl = `${base}/verify-email?token=${encodeURIComponent(rawToken)}`;
|
|
6730
|
+
await queueEmail(cms, {
|
|
6731
|
+
to: email,
|
|
6732
|
+
templateName: "signup",
|
|
6733
|
+
ctx: { name, verifyEmailUrl, companyDetails: companyDetails ?? {} }
|
|
6734
|
+
});
|
|
6735
|
+
emailVerificationSent = true;
|
|
6736
|
+
} catch {
|
|
6737
|
+
await userRepo().update(userId, { blocked: false, updatedAt: /* @__PURE__ */ new Date() });
|
|
6738
|
+
}
|
|
6739
|
+
}
|
|
6740
|
+
return json({
|
|
6741
|
+
success: true,
|
|
6742
|
+
userId,
|
|
6743
|
+
emailVerificationSent
|
|
6744
|
+
});
|
|
6745
|
+
}
|
|
6746
|
+
if (path[0] === "cart" && path.length === 1 && method === "GET") {
|
|
6747
|
+
const { cart, setCookie, err } = await getOrCreateCart(req);
|
|
6748
|
+
if (err) return err;
|
|
6749
|
+
const body = serializeCart(cart);
|
|
6750
|
+
if (setCookie) return json(body, { headers: { "Set-Cookie": setCookie } });
|
|
6751
|
+
return json(body);
|
|
6752
|
+
}
|
|
6753
|
+
if (path[0] === "cart" && path[1] === "items" && path.length === 2 && method === "POST") {
|
|
6754
|
+
const body = await req.json().catch(() => ({}));
|
|
6755
|
+
const productId = Number(body.productId);
|
|
6756
|
+
const quantity = Math.max(1, Number(body.quantity) || 1);
|
|
6757
|
+
if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
|
|
6758
|
+
const product = await productRepo().findOne({ where: { id: productId, deleted: false } });
|
|
6759
|
+
if (!product) return json({ error: "Product not found" }, { status: 404 });
|
|
6760
|
+
const { cart, setCookie, err } = await getOrCreateCart(req);
|
|
6761
|
+
if (err) return err;
|
|
6762
|
+
const cartId = cart.id;
|
|
6763
|
+
const existing = await cartItemRepo().findOne({ where: { cartId, productId } });
|
|
6764
|
+
if (existing) {
|
|
6765
|
+
await cartItemRepo().update(existing.id, {
|
|
6766
|
+
quantity: existing.quantity + quantity
|
|
6767
|
+
});
|
|
6768
|
+
} else {
|
|
6769
|
+
await cartItemRepo().save(
|
|
6770
|
+
cartItemRepo().create({ cartId, productId, quantity })
|
|
6771
|
+
);
|
|
6772
|
+
}
|
|
6773
|
+
await cartRepo().update(cartId, { updatedAt: /* @__PURE__ */ new Date() });
|
|
6774
|
+
const fresh = await cartRepo().findOne({
|
|
6775
|
+
where: { id: cartId },
|
|
6776
|
+
relations: ["items", "items.product"]
|
|
6777
|
+
});
|
|
6778
|
+
const out = serializeCart(fresh);
|
|
6779
|
+
if (setCookie) return json(out, { headers: { "Set-Cookie": setCookie } });
|
|
6780
|
+
return json(out);
|
|
6781
|
+
}
|
|
6782
|
+
if (path[0] === "cart" && path[1] === "items" && path.length === 3) {
|
|
6783
|
+
const itemId = parseInt(path[2], 10);
|
|
6784
|
+
if (!Number.isFinite(itemId)) return json({ error: "Invalid item id" }, { status: 400 });
|
|
6785
|
+
const { cart, setCookie, err } = await getOrCreateCart(req);
|
|
6786
|
+
if (err) return err;
|
|
6787
|
+
const cartId = cart.id;
|
|
6788
|
+
const item = await cartItemRepo().findOne({ where: { id: itemId, cartId } });
|
|
6789
|
+
if (!item) return json({ error: "Not found" }, { status: 404 });
|
|
6790
|
+
if (method === "DELETE") {
|
|
6791
|
+
await cartItemRepo().delete(itemId);
|
|
6792
|
+
await cartRepo().update(cartId, { updatedAt: /* @__PURE__ */ new Date() });
|
|
6793
|
+
const fresh = await cartRepo().findOne({
|
|
6794
|
+
where: { id: cartId },
|
|
6795
|
+
relations: ["items", "items.product"]
|
|
6796
|
+
});
|
|
6797
|
+
const out = serializeCart(fresh);
|
|
6798
|
+
if (setCookie) return json(out, { headers: { "Set-Cookie": setCookie } });
|
|
6799
|
+
return json(out);
|
|
6800
|
+
}
|
|
6801
|
+
if (method === "PATCH") {
|
|
6802
|
+
const b = await req.json().catch(() => ({}));
|
|
6803
|
+
const q = Math.max(0, Number(b.quantity) || 0);
|
|
6804
|
+
if (q === 0) await cartItemRepo().delete(itemId);
|
|
6805
|
+
else await cartItemRepo().update(itemId, { quantity: q });
|
|
6806
|
+
await cartRepo().update(cartId, { updatedAt: /* @__PURE__ */ new Date() });
|
|
6807
|
+
const fresh = await cartRepo().findOne({
|
|
6808
|
+
where: { id: cartId },
|
|
6809
|
+
relations: ["items", "items.product"]
|
|
6810
|
+
});
|
|
6811
|
+
const out = serializeCart(fresh);
|
|
6812
|
+
if (setCookie) return json(out, { headers: { "Set-Cookie": setCookie } });
|
|
6813
|
+
return json(out);
|
|
6814
|
+
}
|
|
6815
|
+
}
|
|
6816
|
+
if (path[0] === "cart" && path[1] === "merge" && method === "POST") {
|
|
6817
|
+
const u = await getSessionUser();
|
|
6818
|
+
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
6819
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
6820
|
+
const contact = await ensureContactForUser(uid);
|
|
6821
|
+
if (!contact) return json({ error: "Contact not found" }, { status: 400 });
|
|
6822
|
+
const cookies = parseCookies(req.headers.get("cookie"));
|
|
6823
|
+
const guestToken = cookies[cookieName];
|
|
6824
|
+
if (!guestToken) return json({ merged: false, message: "No guest cart" });
|
|
6825
|
+
const guestCart = await cartRepo().findOne({
|
|
6826
|
+
where: { guestToken },
|
|
6827
|
+
relations: ["items"]
|
|
6828
|
+
});
|
|
6829
|
+
if (!guestCart || !(guestCart.items || []).length) {
|
|
6830
|
+
let uc = await cartRepo().findOne({
|
|
6831
|
+
where: { contactId: contact.id },
|
|
6832
|
+
relations: ["items", "items.product"]
|
|
6833
|
+
});
|
|
6834
|
+
if (!uc) uc = { items: [] };
|
|
6835
|
+
return json(
|
|
6836
|
+
{ merged: false, cart: serializeCart(uc) },
|
|
6837
|
+
{ headers: { "Set-Cookie": `${cookieName}=; Path=/; Max-Age=0` } }
|
|
6838
|
+
);
|
|
6839
|
+
}
|
|
6840
|
+
let userCart = await cartRepo().findOne({ where: { contactId: contact.id } });
|
|
6841
|
+
if (!userCart) {
|
|
6842
|
+
userCart = await cartRepo().save(
|
|
6843
|
+
cartRepo().create({ contactId: contact.id, guestToken: null, currency: guestCart.currency })
|
|
6844
|
+
);
|
|
6845
|
+
}
|
|
6846
|
+
const uidCart = userCart.id;
|
|
6847
|
+
const gItems = guestCart.items || [];
|
|
6848
|
+
for (const gi of gItems) {
|
|
6849
|
+
const existing = await cartItemRepo().findOne({
|
|
6850
|
+
where: { cartId: uidCart, productId: gi.productId }
|
|
6851
|
+
});
|
|
6852
|
+
if (existing) {
|
|
6853
|
+
await cartItemRepo().update(existing.id, {
|
|
6854
|
+
quantity: existing.quantity + gi.quantity
|
|
6855
|
+
});
|
|
6856
|
+
} else {
|
|
6857
|
+
await cartItemRepo().save(
|
|
6858
|
+
cartItemRepo().create({
|
|
6859
|
+
cartId: uidCart,
|
|
6860
|
+
productId: gi.productId,
|
|
6861
|
+
quantity: gi.quantity,
|
|
6862
|
+
metadata: gi.metadata
|
|
6863
|
+
})
|
|
6864
|
+
);
|
|
6865
|
+
}
|
|
6866
|
+
}
|
|
6867
|
+
await cartRepo().delete(guestCart.id);
|
|
6868
|
+
await cartRepo().update(uidCart, { updatedAt: /* @__PURE__ */ new Date() });
|
|
6869
|
+
const fresh = await cartRepo().findOne({
|
|
6870
|
+
where: { id: uidCart },
|
|
6871
|
+
relations: ["items", "items.product"]
|
|
6872
|
+
});
|
|
6873
|
+
const guestWishlist = await wishlistRepo().findOne({
|
|
6874
|
+
where: { guestId: guestToken },
|
|
6875
|
+
relations: ["items"]
|
|
6876
|
+
});
|
|
6877
|
+
if (guestWishlist && (guestWishlist.items || []).length > 0) {
|
|
6878
|
+
const userWishlist = await getDefaultWishlist(contact.id);
|
|
6879
|
+
const gItems2 = guestWishlist.items || [];
|
|
6880
|
+
for (const gi of gItems2) {
|
|
6881
|
+
const pid = gi.productId;
|
|
6882
|
+
const ex = await wishlistItemRepo().findOne({ where: { wishlistId: userWishlist.id, productId: pid } });
|
|
6883
|
+
if (!ex) await wishlistItemRepo().save(wishlistItemRepo().create({ wishlistId: userWishlist.id, productId: pid }));
|
|
6884
|
+
}
|
|
6885
|
+
await wishlistRepo().delete(guestWishlist.id);
|
|
6886
|
+
}
|
|
6887
|
+
return json({ merged: true, cart: serializeCart(fresh) }, { headers: { "Set-Cookie": `${cookieName}=; Path=/; Max-Age=0` } });
|
|
6888
|
+
}
|
|
6889
|
+
async function getDefaultWishlist(contactId) {
|
|
6890
|
+
let w = await wishlistRepo().findOne({ where: { contactId, name: "default" } });
|
|
6891
|
+
if (!w) {
|
|
6892
|
+
w = await wishlistRepo().save(wishlistRepo().create({ contactId, guestId: null, name: "default" }));
|
|
6893
|
+
}
|
|
6894
|
+
return w;
|
|
6895
|
+
}
|
|
6896
|
+
async function getOrCreateWishlist(req2) {
|
|
6897
|
+
const u = await getSessionUser();
|
|
6898
|
+
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
6899
|
+
if (Number.isFinite(uid)) {
|
|
6900
|
+
const contact = await ensureContactForUser(uid);
|
|
6901
|
+
if (!contact) return { wishlist: {}, setCookie: null, err: json({ error: "User not found" }, { status: 400 }) };
|
|
6902
|
+
const w2 = await getDefaultWishlist(contact.id);
|
|
6903
|
+
const wishlist = await wishlistRepo().findOne({ where: { id: w2.id } });
|
|
6904
|
+
return { wishlist, setCookie: null, err: null };
|
|
6905
|
+
}
|
|
6906
|
+
const cookies = parseCookies(req2.headers.get("cookie"));
|
|
6907
|
+
let token = cookies[cookieName] || "";
|
|
6908
|
+
if (!token) {
|
|
6909
|
+
token = crypto.randomUUID();
|
|
6910
|
+
let w2 = await wishlistRepo().findOne({ where: { guestId: token } });
|
|
6911
|
+
if (!w2) {
|
|
6912
|
+
w2 = await wishlistRepo().save(wishlistRepo().create({ guestId: token, contactId: null, name: "default" }));
|
|
6913
|
+
}
|
|
6914
|
+
return { wishlist: w2, setCookie: guestCookieHeader(cookieName, token), err: null };
|
|
6915
|
+
}
|
|
6916
|
+
let w = await wishlistRepo().findOne({ where: { guestId: token } });
|
|
6917
|
+
if (!w) {
|
|
6918
|
+
w = await wishlistRepo().save(wishlistRepo().create({ guestId: token, contactId: null, name: "default" }));
|
|
6919
|
+
}
|
|
6920
|
+
return { wishlist: w, setCookie: null, err: null };
|
|
6921
|
+
}
|
|
6922
|
+
if (path[0] === "wishlist" && path.length === 1 && method === "GET") {
|
|
6923
|
+
const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
|
|
6924
|
+
if (err) return err;
|
|
6925
|
+
const items = await wishlistItemRepo().find({
|
|
6926
|
+
where: { wishlistId: wishlist.id },
|
|
6927
|
+
relations: ["product"]
|
|
6928
|
+
});
|
|
6929
|
+
const body = {
|
|
6930
|
+
wishlistId: wishlist.id,
|
|
6931
|
+
items: items.map((it) => {
|
|
6932
|
+
const p = it.product;
|
|
6933
|
+
return {
|
|
6934
|
+
id: it.id,
|
|
6935
|
+
productId: it.productId,
|
|
6936
|
+
product: p ? {
|
|
6937
|
+
id: p.id,
|
|
6938
|
+
name: p.name,
|
|
6939
|
+
slug: p.slug,
|
|
6940
|
+
price: p.price,
|
|
6941
|
+
sku: p.sku,
|
|
6942
|
+
image: primaryProductImageUrl(p.metadata)
|
|
6943
|
+
} : null
|
|
6944
|
+
};
|
|
6945
|
+
})
|
|
6946
|
+
};
|
|
6947
|
+
if (setCookie) return json(body, { headers: { "Set-Cookie": setCookie } });
|
|
6948
|
+
return json(body);
|
|
6949
|
+
}
|
|
6950
|
+
if (path[0] === "wishlist" && path[1] === "items" && path.length === 2 && method === "POST") {
|
|
6951
|
+
const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
|
|
6952
|
+
if (err) return err;
|
|
6953
|
+
const b = await req.json().catch(() => ({}));
|
|
6954
|
+
const productId = Number(b.productId);
|
|
6955
|
+
if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
|
|
6956
|
+
const wid = wishlist.id;
|
|
6957
|
+
const ex = await wishlistItemRepo().findOne({ where: { wishlistId: wid, productId } });
|
|
6958
|
+
if (!ex) await wishlistItemRepo().save(wishlistItemRepo().create({ wishlistId: wid, productId }));
|
|
6959
|
+
if (setCookie) return json({ ok: true }, { headers: { "Set-Cookie": setCookie } });
|
|
6960
|
+
return json({ ok: true });
|
|
6961
|
+
}
|
|
6962
|
+
if (path[0] === "wishlist" && path[1] === "items" && path.length === 3 && method === "DELETE") {
|
|
6963
|
+
const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
|
|
6964
|
+
if (err) return err;
|
|
6965
|
+
const productId = parseInt(path[2], 10);
|
|
6966
|
+
await wishlistItemRepo().delete({ wishlistId: wishlist.id, productId });
|
|
6967
|
+
if (setCookie) return json({ ok: true }, { headers: { "Set-Cookie": setCookie } });
|
|
6968
|
+
return json({ ok: true });
|
|
6969
|
+
}
|
|
6970
|
+
if (path[0] === "checkout" && path[1] === "order" && path.length === 2 && method === "POST") {
|
|
6971
|
+
const b = await req.json().catch(() => ({}));
|
|
6972
|
+
const u = await getSessionUser();
|
|
6973
|
+
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
6974
|
+
let contactId;
|
|
6975
|
+
let cart;
|
|
6976
|
+
if (Number.isFinite(uid)) {
|
|
6977
|
+
const contact = await ensureContactForUser(uid);
|
|
6978
|
+
if (!contact) return json({ error: "Contact required" }, { status: 400 });
|
|
6979
|
+
contactId = contact.id;
|
|
6980
|
+
cart = await cartRepo().findOne({
|
|
6981
|
+
where: { contactId },
|
|
6982
|
+
relations: ["items", "items.product"]
|
|
6983
|
+
});
|
|
6984
|
+
} else {
|
|
6985
|
+
const email = (b.email || "").trim();
|
|
6986
|
+
const name = (b.name || "").trim();
|
|
6987
|
+
if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
|
|
6988
|
+
let contact = await contactRepo().findOne({ where: { email, deleted: false } });
|
|
6989
|
+
if (contact && contact.userId != null) {
|
|
6990
|
+
return json({ error: "Please sign in to complete checkout" }, { status: 400 });
|
|
6991
|
+
}
|
|
6992
|
+
if (!contact) {
|
|
6993
|
+
contact = await contactRepo().save(
|
|
6994
|
+
contactRepo().create({
|
|
6995
|
+
name,
|
|
6996
|
+
email,
|
|
6997
|
+
phone: b.phone || null,
|
|
6998
|
+
userId: null,
|
|
6999
|
+
deleted: false
|
|
7000
|
+
})
|
|
7001
|
+
);
|
|
7002
|
+
} else if (name) await contactRepo().update(contact.id, { name, phone: b.phone || contact.phone });
|
|
7003
|
+
contactId = contact.id;
|
|
7004
|
+
const cookies = parseCookies(req.headers.get("cookie"));
|
|
7005
|
+
const guestToken = cookies[cookieName];
|
|
7006
|
+
if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
|
|
7007
|
+
cart = await cartRepo().findOne({
|
|
7008
|
+
where: { guestToken },
|
|
7009
|
+
relations: ["items", "items.product"]
|
|
7010
|
+
});
|
|
7011
|
+
}
|
|
7012
|
+
if (!cart || !(cart.items || []).length) {
|
|
7013
|
+
return json({ error: "Cart is empty" }, { status: 400 });
|
|
7014
|
+
}
|
|
7015
|
+
let subtotal = 0;
|
|
7016
|
+
const lines = [];
|
|
7017
|
+
for (const it of cart.items || []) {
|
|
7018
|
+
const p = it.product;
|
|
7019
|
+
if (!p || p.deleted || p.status !== "available") continue;
|
|
7020
|
+
const unit = Number(p.price);
|
|
7021
|
+
const qty = it.quantity || 1;
|
|
7022
|
+
const lineTotal = unit * qty;
|
|
7023
|
+
subtotal += lineTotal;
|
|
7024
|
+
lines.push({ productId: p.id, quantity: qty, unitPrice: unit, tax: 0, total: lineTotal });
|
|
7025
|
+
}
|
|
7026
|
+
if (!lines.length) return json({ error: "No available items in cart" }, { status: 400 });
|
|
7027
|
+
const total = subtotal;
|
|
7028
|
+
const cartId = cart.id;
|
|
7029
|
+
const ord = await orderRepo().save(
|
|
7030
|
+
orderRepo().create({
|
|
7031
|
+
orderNumber: orderNumber(),
|
|
7032
|
+
contactId,
|
|
7033
|
+
billingAddressId: b.billingAddressId ?? null,
|
|
7034
|
+
shippingAddressId: b.shippingAddressId ?? null,
|
|
7035
|
+
status: "pending",
|
|
7036
|
+
subtotal,
|
|
7037
|
+
tax: 0,
|
|
7038
|
+
discount: 0,
|
|
7039
|
+
total,
|
|
7040
|
+
currency: cart.currency || "INR",
|
|
7041
|
+
metadata: { cartId }
|
|
7042
|
+
})
|
|
7043
|
+
);
|
|
7044
|
+
const oid = ord.id;
|
|
7045
|
+
for (const line of lines) {
|
|
7046
|
+
await orderItemRepo().save(
|
|
7047
|
+
orderItemRepo().create({
|
|
7048
|
+
orderId: oid,
|
|
7049
|
+
productId: line.productId,
|
|
7050
|
+
quantity: line.quantity,
|
|
7051
|
+
unitPrice: line.unitPrice,
|
|
7052
|
+
tax: line.tax,
|
|
7053
|
+
total: line.total
|
|
7054
|
+
})
|
|
7055
|
+
);
|
|
7056
|
+
}
|
|
7057
|
+
return json({
|
|
7058
|
+
orderId: oid,
|
|
7059
|
+
orderNumber: ord.orderNumber,
|
|
7060
|
+
total,
|
|
7061
|
+
currency: cart.currency || "INR"
|
|
7062
|
+
});
|
|
7063
|
+
}
|
|
7064
|
+
if (path[0] === "checkout" && path.length === 1 && method === "POST") {
|
|
7065
|
+
const b = await req.json().catch(() => ({}));
|
|
7066
|
+
const u = await getSessionUser();
|
|
7067
|
+
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
7068
|
+
let contactId;
|
|
7069
|
+
let cart;
|
|
7070
|
+
if (Number.isFinite(uid)) {
|
|
7071
|
+
const contact = await ensureContactForUser(uid);
|
|
7072
|
+
if (!contact) return json({ error: "Contact required" }, { status: 400 });
|
|
7073
|
+
contactId = contact.id;
|
|
7074
|
+
cart = await cartRepo().findOne({
|
|
7075
|
+
where: { contactId },
|
|
7076
|
+
relations: ["items", "items.product"]
|
|
7077
|
+
});
|
|
7078
|
+
} else {
|
|
7079
|
+
const email = (b.email || "").trim();
|
|
7080
|
+
const name = (b.name || "").trim();
|
|
7081
|
+
if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
|
|
7082
|
+
let contact = await contactRepo().findOne({ where: { email, deleted: false } });
|
|
7083
|
+
if (contact && contact.userId != null) {
|
|
7084
|
+
return json({ error: "Please sign in to complete checkout" }, { status: 400 });
|
|
7085
|
+
}
|
|
7086
|
+
if (!contact) {
|
|
7087
|
+
contact = await contactRepo().save(
|
|
7088
|
+
contactRepo().create({
|
|
7089
|
+
name,
|
|
7090
|
+
email,
|
|
7091
|
+
phone: b.phone || null,
|
|
7092
|
+
userId: null,
|
|
7093
|
+
deleted: false
|
|
7094
|
+
})
|
|
7095
|
+
);
|
|
7096
|
+
} else if (name) await contactRepo().update(contact.id, { name, phone: b.phone || contact.phone });
|
|
7097
|
+
contactId = contact.id;
|
|
7098
|
+
const cookies = parseCookies(req.headers.get("cookie"));
|
|
7099
|
+
const guestToken = cookies[cookieName];
|
|
7100
|
+
if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
|
|
7101
|
+
cart = await cartRepo().findOne({
|
|
7102
|
+
where: { guestToken },
|
|
7103
|
+
relations: ["items", "items.product"]
|
|
7104
|
+
});
|
|
7105
|
+
}
|
|
7106
|
+
if (!cart || !(cart.items || []).length) {
|
|
7107
|
+
return json({ error: "Cart is empty" }, { status: 400 });
|
|
7108
|
+
}
|
|
7109
|
+
let subtotal = 0;
|
|
7110
|
+
const lines = [];
|
|
7111
|
+
for (const it of cart.items || []) {
|
|
7112
|
+
const p = it.product;
|
|
7113
|
+
if (!p || p.deleted || p.status !== "available") continue;
|
|
7114
|
+
const unit = Number(p.price);
|
|
7115
|
+
const qty = it.quantity || 1;
|
|
7116
|
+
const lineTotal = unit * qty;
|
|
7117
|
+
subtotal += lineTotal;
|
|
7118
|
+
lines.push({ productId: p.id, quantity: qty, unitPrice: unit, tax: 0, total: lineTotal });
|
|
7119
|
+
}
|
|
7120
|
+
if (!lines.length) return json({ error: "No available items in cart" }, { status: 400 });
|
|
7121
|
+
const total = subtotal;
|
|
7122
|
+
const ord = await orderRepo().save(
|
|
7123
|
+
orderRepo().create({
|
|
7124
|
+
orderNumber: orderNumber(),
|
|
7125
|
+
contactId,
|
|
7126
|
+
billingAddressId: b.billingAddressId ?? null,
|
|
7127
|
+
shippingAddressId: b.shippingAddressId ?? null,
|
|
7128
|
+
status: "pending",
|
|
7129
|
+
subtotal,
|
|
7130
|
+
tax: 0,
|
|
7131
|
+
discount: 0,
|
|
7132
|
+
total,
|
|
7133
|
+
currency: cart.currency || "INR"
|
|
7134
|
+
})
|
|
7135
|
+
);
|
|
7136
|
+
const oid = ord.id;
|
|
7137
|
+
for (const line of lines) {
|
|
7138
|
+
await orderItemRepo().save(
|
|
7139
|
+
orderItemRepo().create({
|
|
7140
|
+
orderId: oid,
|
|
7141
|
+
productId: line.productId,
|
|
7142
|
+
quantity: line.quantity,
|
|
7143
|
+
unitPrice: line.unitPrice,
|
|
7144
|
+
tax: line.tax,
|
|
7145
|
+
total: line.total
|
|
7146
|
+
})
|
|
7147
|
+
);
|
|
7148
|
+
}
|
|
7149
|
+
await cartItemRepo().delete({ cartId: cart.id });
|
|
7150
|
+
await cartRepo().delete(cart.id);
|
|
7151
|
+
return json({
|
|
7152
|
+
orderId: oid,
|
|
7153
|
+
orderNumber: ord.orderNumber,
|
|
7154
|
+
total
|
|
7155
|
+
});
|
|
7156
|
+
}
|
|
7157
|
+
if (path[0] === "orders" && path.length === 1 && method === "GET") {
|
|
7158
|
+
const u = await getSessionUser();
|
|
7159
|
+
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
7160
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
7161
|
+
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
7162
|
+
if (!contact) return json({ orders: [] });
|
|
7163
|
+
const orders = await orderRepo().find({
|
|
7164
|
+
where: { contactId: contact.id, deleted: false },
|
|
7165
|
+
order: { createdAt: "DESC" },
|
|
7166
|
+
take: 50
|
|
7167
|
+
});
|
|
7168
|
+
const orderIds = orders.map((o) => o.id);
|
|
7169
|
+
const previewByOrder = {};
|
|
7170
|
+
if (orderIds.length) {
|
|
7171
|
+
const oItems = await orderItemRepo().find({
|
|
7172
|
+
where: { orderId: In(orderIds) },
|
|
7173
|
+
relations: ["product"],
|
|
7174
|
+
order: { id: "ASC" }
|
|
7175
|
+
});
|
|
7176
|
+
for (const oi of oItems) {
|
|
7177
|
+
const oid = oi.orderId;
|
|
7178
|
+
if (!previewByOrder[oid]) previewByOrder[oid] = [];
|
|
7179
|
+
if (previewByOrder[oid].length >= 4) continue;
|
|
7180
|
+
const url = primaryProductImageUrl(oi.product?.metadata);
|
|
7181
|
+
if (url && !previewByOrder[oid].includes(url)) previewByOrder[oid].push(url);
|
|
7182
|
+
}
|
|
7183
|
+
}
|
|
7184
|
+
return json({
|
|
7185
|
+
orders: orders.map((o) => {
|
|
7186
|
+
const ol = o;
|
|
7187
|
+
return {
|
|
7188
|
+
id: ol.id,
|
|
7189
|
+
orderNumber: ol.orderNumber,
|
|
7190
|
+
status: ol.status,
|
|
7191
|
+
total: ol.total,
|
|
7192
|
+
currency: ol.currency,
|
|
7193
|
+
createdAt: ol.createdAt,
|
|
7194
|
+
previewImages: previewByOrder[ol.id] ?? []
|
|
7195
|
+
};
|
|
7196
|
+
})
|
|
7197
|
+
});
|
|
7198
|
+
}
|
|
7199
|
+
if (path[0] === "orders" && path.length === 2 && method === "GET") {
|
|
7200
|
+
const u = await getSessionUser();
|
|
7201
|
+
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
7202
|
+
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
7203
|
+
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
7204
|
+
if (!contact) return json({ error: "Not found" }, { status: 404 });
|
|
7205
|
+
const orderId = parseInt(path[1], 10);
|
|
7206
|
+
const order = await orderRepo().findOne({
|
|
7207
|
+
where: { id: orderId, contactId: contact.id, deleted: false },
|
|
7208
|
+
relations: ["items", "items.product"]
|
|
7209
|
+
});
|
|
7210
|
+
if (!order) return json({ error: "Not found" }, { status: 404 });
|
|
7211
|
+
const o = order;
|
|
7212
|
+
const lines = (o.items || []).map((line) => {
|
|
7213
|
+
const p = line.product;
|
|
7214
|
+
return {
|
|
7215
|
+
id: line.id,
|
|
7216
|
+
productId: line.productId,
|
|
7217
|
+
quantity: line.quantity,
|
|
7218
|
+
unitPrice: line.unitPrice,
|
|
7219
|
+
tax: line.tax,
|
|
7220
|
+
total: line.total,
|
|
7221
|
+
product: p ? {
|
|
7222
|
+
name: p.name,
|
|
7223
|
+
slug: p.slug,
|
|
7224
|
+
sku: p.sku,
|
|
7225
|
+
image: primaryProductImageUrl(p.metadata)
|
|
7226
|
+
} : null
|
|
7227
|
+
};
|
|
7228
|
+
});
|
|
7229
|
+
return json({
|
|
7230
|
+
order: {
|
|
7231
|
+
id: o.id,
|
|
7232
|
+
orderNumber: o.orderNumber,
|
|
7233
|
+
status: o.status,
|
|
7234
|
+
subtotal: o.subtotal,
|
|
7235
|
+
tax: o.tax,
|
|
7236
|
+
discount: o.discount,
|
|
7237
|
+
total: o.total,
|
|
7238
|
+
currency: o.currency,
|
|
7239
|
+
createdAt: o.createdAt,
|
|
7240
|
+
items: lines
|
|
7241
|
+
}
|
|
7242
|
+
});
|
|
7243
|
+
}
|
|
7244
|
+
return json({ error: "Not found" }, { status: 404 });
|
|
7245
|
+
} catch {
|
|
7246
|
+
return json({ error: "Server error" }, { status: 500 });
|
|
7247
|
+
}
|
|
7248
|
+
}
|
|
7249
|
+
};
|
|
7250
|
+
}
|
|
7251
|
+
|
|
4682
7252
|
// src/admin/config.ts
|
|
4683
7253
|
var DEFAULT_ADMIN_NAV = [
|
|
4684
7254
|
{ href: "/admin/dashboard", label: "Dashboard" },
|
|
4685
7255
|
{ href: "/admin/contacts", label: "Contacts" },
|
|
4686
7256
|
{ href: "/admin/blogs", label: "Blogs" },
|
|
4687
7257
|
{ href: "/admin/users", label: "Users" },
|
|
7258
|
+
{ href: "/admin/roles", label: "Roles" },
|
|
4688
7259
|
{ href: "/admin/forms", label: "Forms" },
|
|
4689
7260
|
{ href: "/admin/submissions", label: "Submissions" },
|
|
4690
7261
|
{ href: "/admin/pages", label: "Pages" }
|
|
4691
7262
|
];
|
|
4692
7263
|
export {
|
|
7264
|
+
ADMIN_GROUP_NAME,
|
|
4693
7265
|
Attribute,
|
|
4694
7266
|
Blog,
|
|
4695
7267
|
Brand,
|
|
4696
7268
|
CMS_ENTITY_MAP,
|
|
7269
|
+
Cart,
|
|
7270
|
+
CartItem,
|
|
4697
7271
|
Category,
|
|
4698
7272
|
ChatConversation,
|
|
4699
7273
|
ChatMessage,
|
|
@@ -4722,12 +7296,17 @@ export {
|
|
|
4722
7296
|
ProductAttribute,
|
|
4723
7297
|
ProductCategory,
|
|
4724
7298
|
ProductTax,
|
|
7299
|
+
RBAC_ADMIN_ONLY_ENTITIES,
|
|
4725
7300
|
Seo,
|
|
4726
7301
|
Tag,
|
|
4727
7302
|
Tax,
|
|
4728
7303
|
User,
|
|
4729
7304
|
UserGroup,
|
|
7305
|
+
Wishlist,
|
|
7306
|
+
WishlistItem,
|
|
4730
7307
|
analyticsPlugin,
|
|
7308
|
+
cachePlugin,
|
|
7309
|
+
canManageRoles,
|
|
4731
7310
|
cn,
|
|
4732
7311
|
createAnalyticsHandlers,
|
|
4733
7312
|
createAuthHelpers,
|
|
@@ -4744,6 +7323,7 @@ export {
|
|
|
4744
7323
|
createInviteAcceptHandler,
|
|
4745
7324
|
createSetPasswordHandler,
|
|
4746
7325
|
createSettingsApiHandlers,
|
|
7326
|
+
createStorefrontApiHandler,
|
|
4747
7327
|
createUploadHandler,
|
|
4748
7328
|
createUserAuthApiRouter,
|
|
4749
7329
|
createUserAvatarHandler,
|
|
@@ -4757,14 +7337,32 @@ export {
|
|
|
4757
7337
|
formatDateOnly,
|
|
4758
7338
|
formatDateTime,
|
|
4759
7339
|
generateSlug,
|
|
7340
|
+
getCompanyDetailsFromSettings,
|
|
4760
7341
|
getNextAuthOptions,
|
|
7342
|
+
getPermissionableEntityKeys,
|
|
4761
7343
|
getRequiredPermission,
|
|
7344
|
+
hasEntityPermission,
|
|
4762
7345
|
isOpenEndpoint,
|
|
4763
7346
|
isPublicMethod,
|
|
7347
|
+
isSuperAdminGroupName,
|
|
7348
|
+
joinRecipientsForSend,
|
|
7349
|
+
linkUnclaimedContactToUser,
|
|
4764
7350
|
llmPlugin,
|
|
4765
7351
|
localStoragePlugin,
|
|
7352
|
+
mergeEmailLayoutCompanyDetails,
|
|
7353
|
+
parseEmailRecipientsFromConfig,
|
|
4766
7354
|
paymentPlugin,
|
|
7355
|
+
permissionRowsToRecord,
|
|
7356
|
+
queueEmail,
|
|
7357
|
+
queueOrderPlacedEmails,
|
|
7358
|
+
queuePlugin,
|
|
7359
|
+
registerEmailQueueProcessor,
|
|
7360
|
+
renderEmail,
|
|
7361
|
+
renderLayout,
|
|
4767
7362
|
s3StoragePlugin,
|
|
7363
|
+
seedAdministratorPermissions,
|
|
7364
|
+
serializeEmailRecipients,
|
|
7365
|
+
sessionHasEntityAccess,
|
|
4768
7366
|
smsPlugin,
|
|
4769
7367
|
truncateText,
|
|
4770
7368
|
validateSlug
|