@infuro/cms-core 1.0.9 → 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.
Files changed (41) hide show
  1. package/dist/admin.cjs +2562 -1176
  2. package/dist/admin.cjs.map +1 -1
  3. package/dist/admin.d.cts +41 -2
  4. package/dist/admin.d.ts +41 -2
  5. package/dist/admin.js +2596 -1214
  6. package/dist/admin.js.map +1 -1
  7. package/dist/api.cjs +1695 -151
  8. package/dist/api.cjs.map +1 -1
  9. package/dist/api.d.cts +2 -1
  10. package/dist/api.d.ts +2 -1
  11. package/dist/api.js +1689 -146
  12. package/dist/api.js.map +1 -1
  13. package/dist/auth.cjs +153 -9
  14. package/dist/auth.cjs.map +1 -1
  15. package/dist/auth.d.cts +17 -27
  16. package/dist/auth.d.ts +17 -27
  17. package/dist/auth.js +143 -8
  18. package/dist/auth.js.map +1 -1
  19. package/dist/cli.cjs +1 -1
  20. package/dist/cli.cjs.map +1 -1
  21. package/dist/cli.js +1 -1
  22. package/dist/cli.js.map +1 -1
  23. package/dist/helpers-dlrF_49e.d.cts +60 -0
  24. package/dist/helpers-dlrF_49e.d.ts +60 -0
  25. package/dist/{index-P5ajDo8-.d.ts → index-C_CZLmHD.d.cts} +88 -1
  26. package/dist/{index-P5ajDo8-.d.cts → index-DeO4AnAj.d.ts} +88 -1
  27. package/dist/index.cjs +3340 -715
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +154 -5
  30. package/dist/index.d.ts +154 -5
  31. package/dist/index.js +2821 -223
  32. package/dist/index.js.map +1 -1
  33. package/dist/migrations/1772178563555-ChatAndKnowledgeBase.ts +33 -17
  34. package/dist/migrations/1774300000000-RbacSeedGroupsAndPermissionUnique.ts +24 -0
  35. package/dist/migrations/1774300000001-SeedAdministratorUsersPermission.ts +35 -0
  36. package/dist/migrations/1774400000000-CustomerAdminAccessContactUser.ts +37 -0
  37. package/dist/migrations/1774400000001-StorefrontCartWishlist.ts +100 -0
  38. package/dist/migrations/1774400000002-WishlistGuestId.ts +29 -0
  39. package/dist/migrations/1774500000000-ProductCollectionHsn.ts +15 -0
  40. package/package.json +13 -7
  41. /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-service.ts
21
- var email_service_exports = {};
22
- __export(email_service_exports, {
23
- EmailService: () => EmailService,
24
- emailTemplates: () => emailTemplates
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
- import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";
27
- import nodemailer from "nodemailer";
28
- var EmailService, emailTemplates;
29
- var init_email_service = __esm({
30
- "src/plugins/email/email-service.ts"() {
31
- "use strict";
32
- EmailService = class {
33
- config;
34
- sesClient;
35
- transporter;
36
- constructor(config) {
37
- this.config = config;
38
- if (config.type === "AWS") {
39
- if (!config.region || !config.accessKeyId || !config.secretAccessKey) {
40
- throw new Error("AWS SES configuration incomplete");
41
- }
42
- this.sesClient = new SESClient({
43
- region: config.region,
44
- credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey }
45
- });
46
- } else if (config.type === "SMTP" || config.type === "GMAIL") {
47
- if (!config.user || !config.password) throw new Error("SMTP configuration incomplete");
48
- this.transporter = nodemailer.createTransport({
49
- host: config.type === "GMAIL" ? "smtp.gmail.com" : void 0,
50
- port: 587,
51
- secure: false,
52
- auth: { user: config.user, pass: config.password }
53
- });
54
- } else {
55
- throw new Error(`Unsupported email type: ${config.type}`);
56
- }
57
- }
58
- async send(emailData) {
59
- try {
60
- if (this.config.type === "AWS" && this.sesClient) {
61
- await this.sesClient.send(
62
- new SendEmailCommand({
63
- Source: emailData.from || this.config.from,
64
- Destination: { ToAddresses: [emailData.to || this.config.to] },
65
- Message: {
66
- Subject: { Data: emailData.subject, Charset: "UTF-8" },
67
- Body: {
68
- Html: { Data: emailData.html, Charset: "UTF-8" },
69
- ...emailData.text && { Text: { Data: emailData.text, Charset: "UTF-8" } }
70
- }
71
- }
72
- })
73
- );
74
- return true;
75
- }
76
- if ((this.config.type === "SMTP" || this.config.type === "GMAIL") && this.transporter) {
77
- await this.transporter.sendMail({
78
- from: emailData.from || this.config.from,
79
- to: emailData.to || this.config.to,
80
- subject: emailData.subject,
81
- html: emailData.html,
82
- text: emailData.text
83
- });
84
- return true;
85
- }
86
- return false;
87
- } catch (error) {
88
- console.error("Email sending failed:", error);
89
- return false;
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 crypto from "crypto";
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 = crypto.createHmac("sha256", secret).update(typeof payload === "string" ? payload : payload.toString("utf8")).digest("hex");
224
- return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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}">&#128231; ${escapeHtml(supportEmail)}</div>`);
484
+ }
485
+ if (supportPhone) {
486
+ parts.push(`<div style="font-size:13px;color:#444;margin-top:2px;">&#128222; ${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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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
- init_email_service();
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 ManyToOne12, JoinColumn as JoinColumn12 } from "typeorm";
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
- ManyToOne12(() => Page, { onDelete: "SET NULL" }),
2296
- JoinColumn12({ name: "parentId" })
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
- ManyToOne12(() => Seo, { onDelete: "SET NULL" }),
2303
- JoinColumn12({ name: "seoId" })
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 ManyToOne13, OneToMany as OneToMany9, JoinColumn as JoinColumn13 } from "typeorm";
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
- ManyToOne13(() => ProductCategory, (c) => c.children, { onDelete: "SET NULL" }),
2404
- JoinColumn13({ name: "parentId" })
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 ManyToOne15, OneToMany as OneToMany11, JoinColumn as JoinColumn15 } from "typeorm";
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 ManyToOne14, JoinColumn as JoinColumn14 } from "typeorm";
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
- ManyToOne14(() => Seo, { onDelete: "SET NULL" }),
2495
- JoinColumn14({ name: "seoId" })
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
- ManyToOne15(() => Seo, { onDelete: "SET NULL" }),
2588
- JoinColumn15({ name: "seoId" })
3343
+ ManyToOne16(() => Seo, { onDelete: "SET NULL" }),
3344
+ JoinColumn16({ name: "seoId" })
2589
3345
  ], Collection.prototype, "seo", 2);
2590
3346
  __decorateClass([
2591
- ManyToOne15(() => ProductCategory, (c) => c.collections, { onDelete: "SET NULL" }),
2592
- JoinColumn15({ name: "categoryId" })
3347
+ ManyToOne16(() => ProductCategory, (c) => c.collections, { onDelete: "SET NULL" }),
3348
+ JoinColumn16({ name: "categoryId" })
2593
3349
  ], Collection.prototype, "category", 2);
2594
3350
  __decorateClass([
2595
- ManyToOne15(() => Brand, (b) => b.collections, { onDelete: "SET NULL" }),
2596
- JoinColumn15({ name: "brandId" })
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 ManyToOne16, OneToMany as OneToMany12, JoinColumn as JoinColumn16 } from "typeorm";
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
- ManyToOne16(() => Seo, { onDelete: "SET NULL" }),
2701
- JoinColumn16({ name: "seoId" })
3460
+ ManyToOne17(() => Seo, { onDelete: "SET NULL" }),
3461
+ JoinColumn17({ name: "seoId" })
2702
3462
  ], Product.prototype, "seo", 2);
2703
3463
  __decorateClass([
2704
- ManyToOne16(() => Collection, (c) => c.products, { onDelete: "SET NULL" }),
2705
- JoinColumn16({ name: "collectionId" })
3464
+ ManyToOne17(() => Collection, (c) => c.products, { onDelete: "SET NULL" }),
3465
+ JoinColumn17({ name: "collectionId" })
2706
3466
  ], Product.prototype, "collection", 2);
2707
3467
  __decorateClass([
2708
- ManyToOne16(() => Brand, (b) => b.products, { onDelete: "SET NULL" }),
2709
- JoinColumn16({ name: "brandId" })
3468
+ ManyToOne17(() => Brand, (b) => b.products, { onDelete: "SET NULL" }),
3469
+ JoinColumn17({ name: "brandId" })
2710
3470
  ], Product.prototype, "brand", 2);
2711
3471
  __decorateClass([
2712
- ManyToOne16(() => ProductCategory, (c) => c.products, { onDelete: "SET NULL" }),
2713
- JoinColumn16({ name: "categoryId" })
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 ManyToOne17, JoinColumn as JoinColumn17 } from "typeorm";
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
- ManyToOne17(() => Product, (p) => p.attributes, { onDelete: "CASCADE" }),
2829
- JoinColumn17({ name: "productId" })
3588
+ ManyToOne18(() => Product, (p) => p.attributes, { onDelete: "CASCADE" }),
3589
+ JoinColumn18({ name: "productId" })
2830
3590
  ], ProductAttribute.prototype, "product", 2);
2831
3591
  __decorateClass([
2832
- ManyToOne17(() => Attribute, { onDelete: "CASCADE" }),
2833
- JoinColumn17({ name: "attributeId" })
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 ManyToOne18, JoinColumn as JoinColumn18 } from "typeorm";
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
- ManyToOne18(() => Product, (p) => p.taxes, { onDelete: "CASCADE" }),
2939
- JoinColumn18({ name: "productId" })
3698
+ ManyToOne19(() => Product, (p) => p.taxes, { onDelete: "CASCADE" }),
3699
+ JoinColumn19({ name: "productId" })
2940
3700
  ], ProductTax.prototype, "product", 2);
2941
3701
  __decorateClass([
2942
- ManyToOne18(() => Tax, { onDelete: "CASCADE" }),
2943
- JoinColumn18({ name: "taxId" })
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 ManyToOne19, JoinColumn as JoinColumn19 } from "typeorm";
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
- ManyToOne19(() => Order, (o) => o.items, { onDelete: "CASCADE" }),
2997
- JoinColumn19({ name: "orderId" })
3756
+ ManyToOne20(() => Order, (o) => o.items, { onDelete: "CASCADE" }),
3757
+ JoinColumn20({ name: "orderId" })
2998
3758
  ], OrderItem.prototype, "order", 2);
2999
3759
  __decorateClass([
3000
- ManyToOne19(() => Product, { onDelete: "CASCADE" }),
3001
- JoinColumn19({ name: "productId" })
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 ManyToOne20, JoinColumn as JoinColumn20 } from "typeorm";
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
- ManyToOne20(() => KnowledgeBaseDocument, (d) => d.chunks, { onDelete: "CASCADE" }),
3037
- JoinColumn20({ name: "documentId" })
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/index.ts
3079
- var CMS_ENTITY_MAP = {
3080
- users: User,
3081
- password_reset_tokens: PasswordResetToken,
3082
- user_groups: UserGroup,
3083
- permissions: Permission,
3084
- blogs: Blog,
3085
- tags: Tag,
3086
- categories: Category,
3087
- comments: Comment,
3088
- contacts: Contact,
3089
- addresses: Address,
3090
- forms: Form,
3091
- form_fields: FormField,
3092
- form_submissions: FormSubmission,
3093
- seos: Seo,
3094
- configs: Config,
3095
- media: Media,
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
- permissions: ["admin"]
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
- token.id = user.id;
3261
- token.groupId = user.groupId;
3262
- token.permissions = user.permissions;
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
- session.user.id = token.id;
3269
- session.user.groupId = token.groupId;
3270
- session.user.permissions = token.permissions;
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 requireAuth(req);
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 sortField = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
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.${sortField}`, sortOrderOrders).skip(skip).take(limit);
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 sortField = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
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.${sortField}`, sortOrderPayments).skip(skip).take(limit);
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 sortField = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
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.${sortField}`, sortOrderContacts).skip(skip).take(limit);
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 = [{ name: ILike(`%${search}%`) }, { title: ILike(`%${search}%`) }];
4576
+ where = buildSearchWhereClause(repo, search);
3484
4577
  }
3485
4578
  const [data, total] = await repo.findAndCount({
3486
4579
  skip,
3487
4580
  take: limit,
3488
- order: { [sortFieldRaw]: sortOrder },
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 requireAuth(req);
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 requireAuth(req);
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 requireAuth(req);
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 requireAuth(req);
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 requireAuth(req);
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 requireAuth(req);
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 body = await req.json();
4776
+ const rawBody = await req.json();
3668
4777
  const repo = dataSource.getRepository(entity);
3669
- if (body && typeof body === "object") sanitizeBodyForEntity(repo, body);
3670
- await repo.update(Number(id), body);
3671
- const updated = await repo.findOne({ where: { id: Number(id) } });
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 requireAuth(req);
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 crypto2 = await import("crypto");
3700
- const token = crypto2.randomBytes(32).toString("hex");
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) await sendEmail({ to: user.email, subject: "Password reset", html: `<a href="${resetLink}">Reset password</a>`, text: resetLink });
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({ name: body.name, email: body.email, password: null, blocked: true, groupId: body.groupId ?? null })
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(["users", "password_reset_tokens", "user_groups", "permissions", "comments", "form_fields", "configs"]);
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 { emailTemplates: emailTemplates2 } = await Promise.resolve().then(() => (init_email_service(), email_service_exports));
4565
- const { subject, html, text } = emailTemplates2.passwordReset({ resetLink: opts.text || opts.html });
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 = { requireAuth: config.requireAuth, json: config.json };
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 usersHandlers = usersApi ? createUsersApiHandlers(usersApi) : null;
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") return analyticsHandlers.GET(req);
4598
- if (path.length === 2 && path[1] === "property-id" && method === "GET") return analyticsHandlers.propertyId();
4599
- if (path.length === 2 && path[1] === "permissions" && method === "GET") return analyticsHandlers.permissions();
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
- if (method === "GET") return settingsHandlers.GET(req, path[1]);
4644
- if (method === "PUT") return settingsHandlers.PUT(req, path[1]);
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