@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/api.cjs CHANGED
@@ -30,105 +30,91 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
- // src/plugins/email/email-service.ts
34
- var email_service_exports = {};
35
- __export(email_service_exports, {
36
- EmailService: () => EmailService,
37
- emailTemplates: () => emailTemplates
33
+ // src/plugins/email/email-queue.ts
34
+ var email_queue_exports = {};
35
+ __export(email_queue_exports, {
36
+ queueEmail: () => queueEmail,
37
+ queueOrderPlacedEmails: () => queueOrderPlacedEmails,
38
+ registerEmailQueueProcessor: () => registerEmailQueueProcessor
38
39
  });
39
- var import_client_ses, import_nodemailer, EmailService, emailTemplates;
40
- var init_email_service = __esm({
41
- "src/plugins/email/email-service.ts"() {
42
- "use strict";
43
- import_client_ses = require("@aws-sdk/client-ses");
44
- import_nodemailer = __toESM(require("nodemailer"), 1);
45
- EmailService = class {
46
- config;
47
- sesClient;
48
- transporter;
49
- constructor(config) {
50
- this.config = config;
51
- if (config.type === "AWS") {
52
- if (!config.region || !config.accessKeyId || !config.secretAccessKey) {
53
- throw new Error("AWS SES configuration incomplete");
54
- }
55
- this.sesClient = new import_client_ses.SESClient({
56
- region: config.region,
57
- credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey }
58
- });
59
- } else if (config.type === "SMTP" || config.type === "GMAIL") {
60
- if (!config.user || !config.password) throw new Error("SMTP configuration incomplete");
61
- this.transporter = import_nodemailer.default.createTransport({
62
- host: config.type === "GMAIL" ? "smtp.gmail.com" : void 0,
63
- port: 587,
64
- secure: false,
65
- auth: { user: config.user, pass: config.password }
66
- });
67
- } else {
68
- throw new Error(`Unsupported email type: ${config.type}`);
69
- }
70
- }
71
- async send(emailData) {
72
- try {
73
- if (this.config.type === "AWS" && this.sesClient) {
74
- await this.sesClient.send(
75
- new import_client_ses.SendEmailCommand({
76
- Source: emailData.from || this.config.from,
77
- Destination: { ToAddresses: [emailData.to || this.config.to] },
78
- Message: {
79
- Subject: { Data: emailData.subject, Charset: "UTF-8" },
80
- Body: {
81
- Html: { Data: emailData.html, Charset: "UTF-8" },
82
- ...emailData.text && { Text: { Data: emailData.text, Charset: "UTF-8" } }
83
- }
84
- }
85
- })
86
- );
87
- return true;
88
- }
89
- if ((this.config.type === "SMTP" || this.config.type === "GMAIL") && this.transporter) {
90
- await this.transporter.sendMail({
91
- from: emailData.from || this.config.from,
92
- to: emailData.to || this.config.to,
93
- subject: emailData.subject,
94
- html: emailData.html,
95
- text: emailData.text
96
- });
97
- return true;
98
- }
99
- return false;
100
- } catch (error) {
101
- console.error("Email sending failed:", error);
102
- return false;
40
+ function registerEmailQueueProcessor(cms) {
41
+ const queue = cms.getPlugin("queue");
42
+ const email = cms.getPlugin("email");
43
+ if (!queue || !email) return;
44
+ queue.registerProcessor(EMAIL_QUEUE_NAME, async (data) => {
45
+ const payload = data;
46
+ const { to, templateName, ctx, subject, html, text } = payload;
47
+ if (!to) return;
48
+ if (templateName && ctx) {
49
+ const rendered = email.renderTemplate(templateName, ctx);
50
+ await email.send({ to, subject: rendered.subject, html: rendered.html, text: rendered.text });
51
+ } else if (subject != null && html != null) {
52
+ await email.send({ to, subject, html, text });
53
+ }
54
+ });
55
+ }
56
+ async function queueEmail(cms, payload) {
57
+ const queue = cms.getPlugin("queue");
58
+ if (queue) {
59
+ await queue.add(EMAIL_QUEUE_NAME, payload);
60
+ return;
61
+ }
62
+ const email = cms.getPlugin("email");
63
+ if (email && payload.templateName && payload.ctx) {
64
+ const rendered = email.renderTemplate(payload.templateName, payload.ctx);
65
+ await email.send({ to: payload.to, subject: rendered.subject, html: rendered.html, text: rendered.text });
66
+ } else if (email && payload.subject != null && payload.html != null) {
67
+ await email.send({ to: payload.to, subject: payload.subject, html: payload.html, text: payload.text });
68
+ }
69
+ }
70
+ async function queueOrderPlacedEmails(cms, payload) {
71
+ const { orderNumber: orderNumber2, total, currency, customerName, customerEmail, salesTeamEmails, companyDetails, lineItems } = payload;
72
+ const base = {
73
+ orderNumber: orderNumber2,
74
+ total: total != null ? String(total) : void 0,
75
+ currency,
76
+ customerName,
77
+ companyDetails: companyDetails ?? {},
78
+ lineItems: lineItems ?? []
79
+ };
80
+ const customerLower = customerEmail?.trim().toLowerCase() ?? "";
81
+ const jobs = [];
82
+ if (customerEmail?.trim()) {
83
+ jobs.push(
84
+ queueEmail(cms, {
85
+ to: customerEmail.trim(),
86
+ templateName: "orderPlaced",
87
+ ctx: { ...base, audience: "customer" }
88
+ })
89
+ );
90
+ }
91
+ const seen = /* @__PURE__ */ new Set();
92
+ for (const raw of salesTeamEmails) {
93
+ const to = raw.trim();
94
+ if (!to) continue;
95
+ const key = to.toLowerCase();
96
+ if (seen.has(key)) continue;
97
+ seen.add(key);
98
+ if (customerLower && key === customerLower) continue;
99
+ jobs.push(
100
+ queueEmail(cms, {
101
+ to,
102
+ templateName: "orderPlaced",
103
+ ctx: {
104
+ ...base,
105
+ audience: "sales",
106
+ internalCustomerEmail: customerEmail?.trim() || void 0
103
107
  }
104
- }
105
- };
106
- emailTemplates = {
107
- formSubmission: (data) => ({
108
- subject: `New Form Submission: ${data.formName}`,
109
- 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>`,
110
- text: `New Form Submission
111
- Form: ${data.formName}
112
- Contact: ${data.contactName} (${data.contactEmail})
113
- ${JSON.stringify(data.formData, null, 2)}`
114
- }),
115
- contactSubmission: (data) => ({
116
- subject: `New Contact Form Submission from ${data.name}`,
117
- 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>` : ""}`,
118
- text: `New Contact Form Submission
119
- Name: ${data.name}
120
- Email: ${data.email}
121
- ${data.phone ? `Phone: ${data.phone}
122
- ` : ""}${data.message ? `Message: ${data.message}` : ""}`
123
- }),
124
- passwordReset: (data) => ({
125
- subject: "Reset your password",
126
- 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>`,
127
- text: `Reset your password: ${data.resetLink}
128
-
129
- This link expires in 1 hour.`
130
108
  })
131
- };
109
+ );
110
+ }
111
+ await Promise.all(jobs);
112
+ }
113
+ var EMAIL_QUEUE_NAME;
114
+ var init_email_queue = __esm({
115
+ "src/plugins/email/email-queue.ts"() {
116
+ "use strict";
117
+ EMAIL_QUEUE_NAME = "email";
132
118
  }
133
119
  });
134
120
 
@@ -147,6 +133,7 @@ __export(api_exports, {
147
133
  createInviteAcceptHandler: () => createInviteAcceptHandler,
148
134
  createSetPasswordHandler: () => createSetPasswordHandler,
149
135
  createSettingsApiHandlers: () => createSettingsApiHandlers,
136
+ createStorefrontApiHandler: () => createStorefrontApiHandler,
150
137
  createUploadHandler: () => createUploadHandler,
151
138
  createUserAuthApiRouter: () => createUserAuthApiRouter,
152
139
  createUserAvatarHandler: () => createUserAvatarHandler,
@@ -191,11 +178,38 @@ function sanitizeBodyForEntity(repo, body) {
191
178
  }
192
179
  }
193
180
  }
181
+ function pickColumnUpdates(repo, body) {
182
+ const cols = new Set(repo.metadata.columns.map((c) => c.propertyName));
183
+ const out = {};
184
+ for (const k of Object.keys(body)) {
185
+ if (cols.has(k)) out[k] = body[k];
186
+ }
187
+ return out;
188
+ }
189
+ function buildSearchWhereClause(repo, search) {
190
+ const cols = new Set(repo.metadata.columns.map((c) => c.propertyName));
191
+ const term = (0, import_typeorm.ILike)(`%${search}%`);
192
+ const ors = [];
193
+ for (const field of ["name", "title", "slug", "email", "filename"]) {
194
+ if (cols.has(field)) ors.push({ [field]: term });
195
+ }
196
+ if (ors.length === 0) return {};
197
+ return ors.length === 1 ? ors[0] : ors;
198
+ }
194
199
  function createCrudHandler(dataSource, entityMap, options) {
195
- const { requireAuth, json } = options;
200
+ const { requireAuth, json, requireEntityPermission: reqPerm } = options;
201
+ async function authz(req, resource, action) {
202
+ const authError = await requireAuth(req);
203
+ if (authError) return authError;
204
+ if (reqPerm) {
205
+ const pe = await reqPerm(req, resource, action);
206
+ if (pe) return pe;
207
+ }
208
+ return null;
209
+ }
196
210
  return {
197
211
  async GET(req, resource) {
198
- const authError = await requireAuth(req);
212
+ const authError = await authz(req, resource, "read");
199
213
  if (authError) return authError;
200
214
  const entity = entityMap[resource];
201
215
  if (!resource || !entity) {
@@ -211,7 +225,7 @@ function createCrudHandler(dataSource, entityMap, options) {
211
225
  if (resource === "orders") {
212
226
  const repo2 = dataSource.getRepository(entity);
213
227
  const allowedSort = ["id", "orderNumber", "contactId", "status", "total", "currency", "createdAt", "updatedAt"];
214
- const sortField = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
228
+ const sortField2 = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
215
229
  const sortOrderOrders = searchParams.get("sortOrder") === "asc" ? "ASC" : "DESC";
216
230
  const statusFilter = searchParams.get("status")?.trim();
217
231
  const dateFrom = searchParams.get("dateFrom")?.trim();
@@ -226,7 +240,7 @@ function createCrudHandler(dataSource, entityMap, options) {
226
240
  return json({ total: 0, page, limit, totalPages: 0, data: [] });
227
241
  }
228
242
  }
229
- 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);
243
+ 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);
230
244
  if (search && typeof search === "string" && search.trim()) {
231
245
  const term = `%${search.trim()}%`;
232
246
  qb.andWhere(
@@ -257,14 +271,14 @@ function createCrudHandler(dataSource, entityMap, options) {
257
271
  if (resource === "payments") {
258
272
  const repo2 = dataSource.getRepository(entity);
259
273
  const allowedSort = ["id", "orderId", "amount", "currency", "status", "method", "paidAt", "createdAt", "updatedAt"];
260
- const sortField = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
274
+ const sortField2 = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
261
275
  const sortOrderPayments = searchParams.get("sortOrder") === "asc" ? "ASC" : "DESC";
262
276
  const statusFilter = searchParams.get("status")?.trim();
263
277
  const dateFrom = searchParams.get("dateFrom")?.trim();
264
278
  const dateTo = searchParams.get("dateTo")?.trim();
265
279
  const methodFilter = searchParams.get("method")?.trim();
266
280
  const orderNumberParam = searchParams.get("orderNumber")?.trim();
267
- 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);
281
+ 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);
268
282
  if (search && typeof search === "string" && search.trim()) {
269
283
  const term = `%${search.trim()}%`;
270
284
  qb.andWhere(
@@ -313,12 +327,12 @@ function createCrudHandler(dataSource, entityMap, options) {
313
327
  if (resource === "contacts") {
314
328
  const repo2 = dataSource.getRepository(entity);
315
329
  const allowedSort = ["id", "name", "email", "createdAt", "type"];
316
- const sortField = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
330
+ const sortField2 = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
317
331
  const sortOrderContacts = searchParams.get("sortOrder") === "asc" ? "ASC" : "DESC";
318
332
  const typeFilter2 = searchParams.get("type")?.trim();
319
333
  const orderIdParam = searchParams.get("orderId")?.trim();
320
334
  const includeSummary = searchParams.get("includeSummary") === "1";
321
- const qb = repo2.createQueryBuilder("contact").orderBy(`contact.${sortField}`, sortOrderContacts).skip(skip).take(limit);
335
+ const qb = repo2.createQueryBuilder("contact").orderBy(`contact.${sortField2}`, sortOrderContacts).skip(skip).take(limit);
322
336
  if (search && typeof search === "string" && search.trim()) {
323
337
  const term = `%${search.trim()}%`;
324
338
  qb.andWhere("(contact.name ILIKE :term OR contact.email ILIKE :term OR contact.phone ILIKE :term)", { term });
@@ -352,6 +366,8 @@ function createCrudHandler(dataSource, entityMap, options) {
352
366
  }
353
367
  const repo = dataSource.getRepository(entity);
354
368
  const typeFilter = searchParams.get("type");
369
+ const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
370
+ const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
355
371
  let where = {};
356
372
  if (resource === "media") {
357
373
  const mediaWhere = {};
@@ -359,18 +375,18 @@ function createCrudHandler(dataSource, entityMap, options) {
359
375
  if (typeFilter) mediaWhere.mimeType = (0, import_typeorm.Like)(`${typeFilter}/%`);
360
376
  where = Object.keys(mediaWhere).length > 0 ? mediaWhere : {};
361
377
  } else if (search) {
362
- where = [{ name: (0, import_typeorm.ILike)(`%${search}%`) }, { title: (0, import_typeorm.ILike)(`%${search}%`) }];
378
+ where = buildSearchWhereClause(repo, search);
363
379
  }
364
380
  const [data, total] = await repo.findAndCount({
365
381
  skip,
366
382
  take: limit,
367
- order: { [sortFieldRaw]: sortOrder },
383
+ order: { [sortField]: sortOrder },
368
384
  where
369
385
  });
370
386
  return json({ total, page, limit, totalPages: Math.ceil(total / limit), data });
371
387
  },
372
388
  async POST(req, resource) {
373
- const authError = await requireAuth(req);
389
+ const authError = await authz(req, resource, "create");
374
390
  if (authError) return authError;
375
391
  const entity = entityMap[resource];
376
392
  if (!resource || !entity) {
@@ -386,7 +402,7 @@ function createCrudHandler(dataSource, entityMap, options) {
386
402
  return json(created, { status: 201 });
387
403
  },
388
404
  async GET_METADATA(req, resource) {
389
- const authError = await requireAuth(req);
405
+ const authError = await authz(req, resource, "read");
390
406
  if (authError) return authError;
391
407
  const entity = entityMap[resource];
392
408
  if (!resource || !entity) {
@@ -417,7 +433,7 @@ function createCrudHandler(dataSource, entityMap, options) {
417
433
  return json({ columns, uniqueColumns });
418
434
  },
419
435
  async BULK_POST(req, resource) {
420
- const authError = await requireAuth(req);
436
+ const authError = await authz(req, resource, "update");
421
437
  if (authError) return authError;
422
438
  const entity = entityMap[resource];
423
439
  if (!resource || !entity) {
@@ -448,7 +464,7 @@ function createCrudHandler(dataSource, entityMap, options) {
448
464
  }
449
465
  },
450
466
  async GET_EXPORT(req, resource) {
451
- const authError = await requireAuth(req);
467
+ const authError = await authz(req, resource, "read");
452
468
  if (authError) return authError;
453
469
  const entity = entityMap[resource];
454
470
  if (!resource || !entity) {
@@ -489,10 +505,19 @@ function createCrudHandler(dataSource, entityMap, options) {
489
505
  };
490
506
  }
491
507
  function createCrudByIdHandler(dataSource, entityMap, options) {
492
- const { requireAuth, json } = options;
508
+ const { requireAuth, json, requireEntityPermission: reqPerm } = options;
509
+ async function authz(req, resource, action) {
510
+ const authError = await requireAuth(req);
511
+ if (authError) return authError;
512
+ if (reqPerm) {
513
+ const pe = await reqPerm(req, resource, action);
514
+ if (pe) return pe;
515
+ }
516
+ return null;
517
+ }
493
518
  return {
494
519
  async GET(req, resource, id) {
495
- const authError = await requireAuth(req);
520
+ const authError = await authz(req, resource, "read");
496
521
  if (authError) return authError;
497
522
  const entity = entityMap[resource];
498
523
  if (!entity) return json({ error: "Invalid resource" }, { status: 400 });
@@ -535,23 +560,111 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
535
560
  if (!payment) return json({ message: "Not found" }, { status: 404 });
536
561
  return json(payment);
537
562
  }
563
+ if (resource === "blogs") {
564
+ const blog = await repo.findOne({
565
+ where: { id: Number(id) },
566
+ relations: ["category", "seo", "tags"]
567
+ });
568
+ return blog ? json(blog) : json({ message: "Not found" }, { status: 404 });
569
+ }
538
570
  const item = await repo.findOne({ where: { id: Number(id) } });
539
571
  return item ? json(item) : json({ message: "Not found" }, { status: 404 });
540
572
  },
541
573
  async PUT(req, resource, id) {
542
- const authError = await requireAuth(req);
574
+ const authError = await authz(req, resource, "update");
543
575
  if (authError) return authError;
544
576
  const entity = entityMap[resource];
545
577
  if (!entity) return json({ error: "Invalid resource" }, { status: 400 });
546
- const body = await req.json();
578
+ const rawBody = await req.json();
547
579
  const repo = dataSource.getRepository(entity);
548
- if (body && typeof body === "object") sanitizeBodyForEntity(repo, body);
549
- await repo.update(Number(id), body);
550
- const updated = await repo.findOne({ where: { id: Number(id) } });
580
+ const numericId = Number(id);
581
+ if (resource === "blogs" && rawBody && typeof rawBody === "object" && entityMap.categories && entityMap.seos && entityMap.tags) {
582
+ const existing = await repo.findOne({ where: { id: numericId } });
583
+ if (!existing) return json({ message: "Not found" }, { status: 404 });
584
+ const updatePayload2 = pickColumnUpdates(repo, rawBody);
585
+ if ("category" in rawBody) {
586
+ const c = rawBody.category;
587
+ if (typeof c === "string" && c.trim()) {
588
+ const cat = await dataSource.getRepository(entityMap.categories).findOne({ where: { name: c.trim() } });
589
+ updatePayload2.categoryId = cat?.id ?? null;
590
+ } else {
591
+ updatePayload2.categoryId = null;
592
+ }
593
+ }
594
+ const blogSlug = typeof updatePayload2.slug === "string" && updatePayload2.slug || existing.slug;
595
+ const seoRepo = dataSource.getRepository(entityMap.seos);
596
+ const seoField = (k) => {
597
+ if (!(k in rawBody)) return void 0;
598
+ const v = rawBody[k];
599
+ if (v == null || v === "") return null;
600
+ return String(v);
601
+ };
602
+ if ("metaTitle" in rawBody || "metaDescription" in rawBody || "metaKeywords" in rawBody || "ogImage" in rawBody) {
603
+ const title = seoField("metaTitle");
604
+ const description = seoField("metaDescription");
605
+ const keywords = seoField("metaKeywords");
606
+ const ogImage = seoField("ogImage");
607
+ const exSeoId = existing.seoId;
608
+ if (exSeoId) {
609
+ const seo = await seoRepo.findOne({ where: { id: exSeoId } });
610
+ if (seo) {
611
+ const s = seo;
612
+ if (title !== void 0) s.title = title;
613
+ if (description !== void 0) s.description = description;
614
+ if (keywords !== void 0) s.keywords = keywords;
615
+ if (ogImage !== void 0) s.ogImage = ogImage;
616
+ s.slug = blogSlug;
617
+ await seoRepo.save(seo);
618
+ }
619
+ } else {
620
+ let seoSlug = blogSlug;
621
+ const taken = await seoRepo.findOne({ where: { slug: seoSlug } });
622
+ if (taken) seoSlug = `blog-${numericId}-${blogSlug}`;
623
+ const seo = await seoRepo.save(
624
+ seoRepo.create({
625
+ slug: seoSlug,
626
+ title: title ?? null,
627
+ description: description ?? null,
628
+ keywords: keywords ?? null,
629
+ ogImage: ogImage ?? null
630
+ })
631
+ );
632
+ updatePayload2.seoId = seo.id;
633
+ }
634
+ }
635
+ sanitizeBodyForEntity(repo, updatePayload2);
636
+ await repo.update(numericId, updatePayload2);
637
+ if (Array.isArray(rawBody.tags)) {
638
+ const tagNames = rawBody.tags.map((t) => String(t).trim()).filter(Boolean);
639
+ const tagRepo = dataSource.getRepository(entityMap.tags);
640
+ const tagEntities = [];
641
+ for (const name of tagNames) {
642
+ let tag = await tagRepo.findOne({ where: { name } });
643
+ if (!tag) tag = await tagRepo.save(tagRepo.create({ name }));
644
+ tagEntities.push(tag);
645
+ }
646
+ const blog = await repo.findOne({ where: { id: numericId }, relations: ["tags"] });
647
+ if (blog) {
648
+ blog.tags = tagEntities;
649
+ await repo.save(blog);
650
+ }
651
+ }
652
+ const updated2 = await repo.findOne({
653
+ where: { id: numericId },
654
+ relations: ["tags", "category", "seo"]
655
+ });
656
+ return updated2 ? json(updated2) : json({ message: "Not found" }, { status: 404 });
657
+ }
658
+ const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
659
+ if (Object.keys(updatePayload).length > 0) {
660
+ sanitizeBodyForEntity(repo, updatePayload);
661
+ await repo.update(numericId, updatePayload);
662
+ }
663
+ const updated = await repo.findOne({ where: { id: numericId } });
551
664
  return updated ? json(updated) : json({ message: "Not found" }, { status: 404 });
552
665
  },
553
666
  async DELETE(req, resource, id) {
554
- const authError = await requireAuth(req);
667
+ const authError = await authz(req, resource, "delete");
555
668
  if (authError) return authError;
556
669
  const entity = entityMap[resource];
557
670
  if (!entity) return json({ error: "Invalid resource" }, { status: 400 });
@@ -563,6 +676,16 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
563
676
  };
564
677
  }
565
678
 
679
+ // src/lib/link-contact-to-user.ts
680
+ var import_typeorm2 = require("typeorm");
681
+ async function linkUnclaimedContactToUser(dataSource, contactsEntity, userId, email) {
682
+ const repo = dataSource.getRepository(contactsEntity);
683
+ const found = await repo.findOne({
684
+ where: { email, userId: (0, import_typeorm2.IsNull)(), deleted: false }
685
+ });
686
+ if (found) await repo.update(found.id, { userId });
687
+ }
688
+
566
689
  // src/api/auth-handlers.ts
567
690
  function createForgotPasswordHandler(config) {
568
691
  const { dataSource, entityMap, json, baseUrl, sendEmail, resetExpiryHours = 1, afterCreateToken } = config;
@@ -575,13 +698,20 @@ function createForgotPasswordHandler(config) {
575
698
  const user = await userRepo.findOne({ where: { email }, select: ["email"] });
576
699
  const msg = "If an account exists with this email, you will receive a reset link shortly.";
577
700
  if (!user) return json({ message: msg }, { status: 200 });
578
- const crypto = await import("crypto");
579
- const token = crypto.randomBytes(32).toString("hex");
701
+ const crypto2 = await import("crypto");
702
+ const token = crypto2.randomBytes(32).toString("hex");
580
703
  const expiresAt = new Date(Date.now() + resetExpiryHours * 60 * 60 * 1e3);
581
704
  const tokenRepo = dataSource.getRepository(entityMap.password_reset_tokens);
582
705
  await tokenRepo.save(tokenRepo.create({ email: user.email, token, expiresAt }));
583
706
  const resetLink = `${baseUrl}/admin/reset-password?token=${token}`;
584
- if (sendEmail) await sendEmail({ to: user.email, subject: "Password reset", html: `<a href="${resetLink}">Reset password</a>`, text: resetLink });
707
+ if (sendEmail)
708
+ await sendEmail({
709
+ to: user.email,
710
+ subject: "Password reset",
711
+ html: `<a href="${resetLink}">Reset password</a>`,
712
+ text: resetLink,
713
+ resetLink
714
+ });
585
715
  if (afterCreateToken) await afterCreateToken(user.email, resetLink);
586
716
  return json({ message: msg }, { status: 200 });
587
717
  } catch (err) {
@@ -630,6 +760,9 @@ function createInviteAcceptHandler(config) {
630
760
  const user = await userRepo.findOne({ where: { email }, select: ["id", "blocked"] });
631
761
  if (!user) return json({ error: "User not found" }, { status: 400 });
632
762
  if (!user.blocked) return json({ error: "User is already active" }, { status: 400 });
763
+ if (entityMap.contacts) {
764
+ await linkUnclaimedContactToUser(dataSource, entityMap.contacts, user.id, email);
765
+ }
633
766
  if (beforeActivate) await beforeActivate(email, user.id);
634
767
  const hashedPassword = await hashPassword(password);
635
768
  await userRepo.update(user.id, { password: hashedPassword, blocked: false });
@@ -690,12 +823,17 @@ function createUserAuthApiRouter(config) {
690
823
  }
691
824
 
692
825
  // src/api/cms-handlers.ts
693
- var import_typeorm2 = require("typeorm");
826
+ var import_typeorm3 = require("typeorm");
827
+ init_email_queue();
694
828
  function createDashboardStatsHandler(config) {
695
- const { dataSource, entityMap, json, requireAuth, requirePermission } = config;
829
+ const { dataSource, entityMap, json, requireAuth, requirePermission, requireEntityPermission } = config;
696
830
  return async function GET(req) {
697
831
  const authErr = await requireAuth(req);
698
832
  if (authErr) return authErr;
833
+ if (requireEntityPermission) {
834
+ const pe = await requireEntityPermission(req, "dashboard", "read");
835
+ if (pe) return pe;
836
+ }
699
837
  if (requirePermission) {
700
838
  const permErr = await requirePermission(req, "view_dashboard");
701
839
  if (permErr) return permErr;
@@ -709,8 +847,8 @@ function createDashboardStatsHandler(config) {
709
847
  repo("form_submissions")?.count() ?? 0,
710
848
  repo("users")?.count({ where: { deleted: false } }) ?? 0,
711
849
  repo("blogs")?.count({ where: { deleted: false } }) ?? 0,
712
- repo("contacts")?.count({ where: { createdAt: (0, import_typeorm2.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0,
713
- repo("form_submissions")?.count({ where: { createdAt: (0, import_typeorm2.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0
850
+ repo("contacts")?.count({ where: { createdAt: (0, import_typeorm3.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0,
851
+ repo("form_submissions")?.count({ where: { createdAt: (0, import_typeorm3.MoreThanOrEqual)(sevenDaysAgo) } }) ?? 0
714
852
  ]);
715
853
  return json({
716
854
  contacts: { total: contactsCount, recent: recentContacts },
@@ -754,11 +892,15 @@ function createAnalyticsHandlers(config) {
754
892
  };
755
893
  }
756
894
  function createUploadHandler(config) {
757
- const { json, requireAuth, storage, localUploadDir = "public/uploads", allowedTypes, maxSizeBytes = 10 * 1024 * 1024 } = config;
895
+ const { json, requireAuth, requireEntityPermission, storage, localUploadDir = "public/uploads", allowedTypes, maxSizeBytes = 10 * 1024 * 1024 } = config;
758
896
  const allowed = allowedTypes ?? ["image/jpeg", "image/png", "image/gif", "image/webp", "application/pdf", "text/plain"];
759
897
  return async function POST(req) {
760
898
  const authErr = await requireAuth(req);
761
899
  if (authErr) return authErr;
900
+ if (requireEntityPermission) {
901
+ const pe = await requireEntityPermission(req, "upload", "create");
902
+ if (pe) return pe;
903
+ }
762
904
  try {
763
905
  const formData = await req.formData();
764
906
  const file = formData.get("file");
@@ -839,13 +981,17 @@ function normalizeFieldRow(f, formId) {
839
981
  };
840
982
  }
841
983
  function createFormSaveHandlers(config) {
842
- const { dataSource, entityMap, json, requireAuth } = config;
984
+ const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
843
985
  const formRepo = () => dataSource.getRepository(entityMap.forms);
844
986
  const fieldRepo = () => dataSource.getRepository(entityMap.form_fields);
845
987
  return {
846
988
  async GET(req, id) {
847
989
  const authErr = await requireAuth(req);
848
990
  if (authErr) return authErr;
991
+ if (requireEntityPermission) {
992
+ const pe = await requireEntityPermission(req, "forms", "read");
993
+ if (pe) return pe;
994
+ }
849
995
  try {
850
996
  const formId = Number(id);
851
997
  if (!Number.isInteger(formId) || formId <= 0) return json({ error: "Invalid form id" }, { status: 400 });
@@ -865,6 +1011,10 @@ function createFormSaveHandlers(config) {
865
1011
  async POST(req) {
866
1012
  const authErr = await requireAuth(req);
867
1013
  if (authErr) return authErr;
1014
+ if (requireEntityPermission) {
1015
+ const pe = await requireEntityPermission(req, "forms", "create");
1016
+ if (pe) return pe;
1017
+ }
868
1018
  try {
869
1019
  const body = await req.json();
870
1020
  if (!body || typeof body !== "object") return json({ error: "Invalid request payload" }, { status: 400 });
@@ -885,6 +1035,10 @@ function createFormSaveHandlers(config) {
885
1035
  async PUT(req, id) {
886
1036
  const authErr = await requireAuth(req);
887
1037
  if (authErr) return authErr;
1038
+ if (requireEntityPermission) {
1039
+ const pe = await requireEntityPermission(req, "forms", "update");
1040
+ if (pe) return pe;
1041
+ }
888
1042
  try {
889
1043
  const formId = Number(id);
890
1044
  if (!Number.isInteger(formId) || formId <= 0) return json({ error: "Invalid form id" }, { status: 400 });
@@ -913,10 +1067,14 @@ function createFormSaveHandlers(config) {
913
1067
  };
914
1068
  }
915
1069
  function createFormSubmissionGetByIdHandler(config) {
916
- const { dataSource, entityMap, json, requireAuth } = config;
1070
+ const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
917
1071
  return async function GET(req, id) {
918
1072
  const authErr = await requireAuth(req);
919
1073
  if (authErr) return authErr;
1074
+ if (requireEntityPermission) {
1075
+ const pe = await requireEntityPermission(req, "form_submissions", "read");
1076
+ if (pe) return pe;
1077
+ }
920
1078
  try {
921
1079
  const submissionId = Number(id);
922
1080
  if (!Number.isInteger(submissionId) || submissionId <= 0) return json({ error: "Invalid id" }, { status: 400 });
@@ -943,10 +1101,14 @@ function createFormSubmissionGetByIdHandler(config) {
943
1101
  };
944
1102
  }
945
1103
  function createFormSubmissionListHandler(config) {
946
- const { dataSource, entityMap, json, requireAuth } = config;
1104
+ const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
947
1105
  return async function GET(req) {
948
1106
  const authErr = await requireAuth(req);
949
1107
  if (authErr) return authErr;
1108
+ if (requireEntityPermission) {
1109
+ const pe = await requireEntityPermission(req, "form_submissions", "read");
1110
+ if (pe) return pe;
1111
+ }
950
1112
  try {
951
1113
  const repo = dataSource.getRepository(entityMap.form_submissions);
952
1114
  const { searchParams } = new URL(req.url);
@@ -967,6 +1129,11 @@ function createFormSubmissionListHandler(config) {
967
1129
  }
968
1130
  };
969
1131
  }
1132
+ function formatSubmissionFieldValue(raw) {
1133
+ if (raw == null || raw === "") return "\u2014";
1134
+ if (typeof raw === "object") return JSON.stringify(raw);
1135
+ return String(raw);
1136
+ }
970
1137
  function pickContactFromSubmission(fields, data) {
971
1138
  let email = null;
972
1139
  let name = null;
@@ -1042,6 +1209,50 @@ function createFormSubmissionHandler(config) {
1042
1209
  userAgent: userAgent?.slice(0, 500) ?? null
1043
1210
  })
1044
1211
  );
1212
+ const formWithName = form;
1213
+ const formName = formWithName.name ?? "Form";
1214
+ let contactName = "Unknown";
1215
+ let contactEmail = "";
1216
+ if (Number.isInteger(contactId)) {
1217
+ const contactRepo = dataSource.getRepository(entityMap.contacts);
1218
+ const contact = await contactRepo.findOne({ where: { id: contactId }, select: ["name", "email"] });
1219
+ if (contact) {
1220
+ contactName = contact.name ?? contactName;
1221
+ contactEmail = contact.email ?? contactEmail;
1222
+ }
1223
+ } else {
1224
+ const contactData = pickContactFromSubmission(activeFields, data);
1225
+ if (contactData) {
1226
+ contactName = contactData.name;
1227
+ contactEmail = contactData.email;
1228
+ }
1229
+ }
1230
+ if (config.getCms && config.getCompanyDetails && config.getRecipientForChannel) {
1231
+ try {
1232
+ const cms = await config.getCms();
1233
+ const to = await config.getRecipientForChannel("crm");
1234
+ if (to) {
1235
+ const companyDetails = await config.getCompanyDetails();
1236
+ const formFieldRows = activeFields.map((f) => ({
1237
+ label: f.label && String(f.label).trim() || `Field ${f.id}`,
1238
+ value: formatSubmissionFieldValue(data[String(f.id)])
1239
+ }));
1240
+ await queueEmail(cms, {
1241
+ to,
1242
+ templateName: "formSubmission",
1243
+ ctx: {
1244
+ formName,
1245
+ contactName,
1246
+ contactEmail,
1247
+ formData: data,
1248
+ formFieldRows,
1249
+ companyDetails: companyDetails ?? {}
1250
+ }
1251
+ });
1252
+ }
1253
+ } catch {
1254
+ }
1255
+ }
1045
1256
  return json(created, { status: 201 });
1046
1257
  } catch {
1047
1258
  return json({ error: "Server Error" }, { status: 500 });
@@ -1049,12 +1260,34 @@ function createFormSubmissionHandler(config) {
1049
1260
  };
1050
1261
  }
1051
1262
  function createUsersApiHandlers(config) {
1052
- const { dataSource, entityMap, json, requireAuth, baseUrl } = config;
1263
+ const { dataSource, entityMap, json, requireAuth, requireEntityPermission, baseUrl, getCms, getCompanyDetails } = config;
1264
+ async function trySendInviteEmail(toEmail, inviteLink, inviteeName) {
1265
+ if (!getCms) return;
1266
+ try {
1267
+ const cms = await getCms();
1268
+ const companyDetails = getCompanyDetails ? await getCompanyDetails() : {};
1269
+ await queueEmail(cms, {
1270
+ to: toEmail,
1271
+ templateName: "invite",
1272
+ ctx: {
1273
+ inviteLink,
1274
+ email: toEmail,
1275
+ inviteeName: inviteeName.trim(),
1276
+ companyDetails: companyDetails ?? {}
1277
+ }
1278
+ });
1279
+ } catch {
1280
+ }
1281
+ }
1053
1282
  const userRepo = () => dataSource.getRepository(entityMap.users);
1054
1283
  return {
1055
1284
  async list(req) {
1056
1285
  const authErr = await requireAuth(req);
1057
1286
  if (authErr) return authErr;
1287
+ if (requireEntityPermission) {
1288
+ const pe = await requireEntityPermission(req, "users", "read");
1289
+ if (pe) return pe;
1290
+ }
1058
1291
  try {
1059
1292
  const url = new URL(req.url);
1060
1293
  const page = Math.max(1, parseInt(url.searchParams.get("page") || "1", 10));
@@ -1063,7 +1296,7 @@ function createUsersApiHandlers(config) {
1063
1296
  const sortField = url.searchParams.get("sortField") || "createdAt";
1064
1297
  const sortOrder = url.searchParams.get("sortOrder") === "desc" ? "DESC" : "ASC";
1065
1298
  const search = url.searchParams.get("search");
1066
- const where = search ? [{ name: (0, import_typeorm2.ILike)(`%${search}%`) }, { email: (0, import_typeorm2.ILike)(`%${search}%`) }] : {};
1299
+ const where = search ? [{ name: (0, import_typeorm3.ILike)(`%${search}%`) }, { email: (0, import_typeorm3.ILike)(`%${search}%`) }] : {};
1067
1300
  const [data, total] = await userRepo().findAndCount({
1068
1301
  skip,
1069
1302
  take: limit,
@@ -1080,16 +1313,40 @@ function createUsersApiHandlers(config) {
1080
1313
  async create(req) {
1081
1314
  const authErr = await requireAuth(req);
1082
1315
  if (authErr) return authErr;
1316
+ if (requireEntityPermission) {
1317
+ const pe = await requireEntityPermission(req, "users", "create");
1318
+ if (pe) return pe;
1319
+ }
1083
1320
  try {
1084
1321
  const body = await req.json();
1085
1322
  if (!body?.name || !body?.email) return json({ error: "Name and email are required" }, { status: 400 });
1086
1323
  const existing = await userRepo().findOne({ where: { email: body.email } });
1087
1324
  if (existing) return json({ error: "User with this email already exists" }, { status: 400 });
1325
+ const groupRepo = dataSource.getRepository(entityMap.user_groups);
1326
+ const customerG = await groupRepo.findOne({ where: { name: "Customer", deleted: false } });
1327
+ const gid = body.groupId ?? null;
1328
+ const isCustomer = !!(customerG && gid === customerG.id);
1329
+ const adminAccess = isCustomer ? false : body.adminAccess === false ? false : true;
1088
1330
  const newUser = await userRepo().save(
1089
- userRepo().create({ name: body.name, email: body.email, password: null, blocked: true, groupId: body.groupId ?? null })
1331
+ userRepo().create({
1332
+ name: body.name,
1333
+ email: body.email,
1334
+ password: null,
1335
+ blocked: true,
1336
+ groupId: gid,
1337
+ adminAccess
1338
+ })
1090
1339
  );
1340
+ if (entityMap.contacts) {
1341
+ await linkUnclaimedContactToUser(dataSource, entityMap.contacts, newUser.id, newUser.email);
1342
+ }
1091
1343
  const emailToken = Buffer.from(newUser.email).toString("base64");
1092
1344
  const inviteLink = `${baseUrl}/admin/invite?token=${emailToken}`;
1345
+ await trySendInviteEmail(
1346
+ newUser.email,
1347
+ inviteLink,
1348
+ newUser.name ?? ""
1349
+ );
1093
1350
  return json({ message: "User created successfully (blocked until password is set)", user: newUser, inviteLink }, { status: 201 });
1094
1351
  } catch {
1095
1352
  return json({ error: "Server Error" }, { status: 500 });
@@ -1098,6 +1355,10 @@ function createUsersApiHandlers(config) {
1098
1355
  async getById(_req, id) {
1099
1356
  const authErr = await requireAuth(new Request(_req.url));
1100
1357
  if (authErr) return authErr;
1358
+ if (requireEntityPermission) {
1359
+ const pe = await requireEntityPermission(_req, "users", "read");
1360
+ if (pe) return pe;
1361
+ }
1101
1362
  try {
1102
1363
  const user = await userRepo().findOne({
1103
1364
  where: { id: parseInt(id, 10) },
@@ -1113,6 +1374,10 @@ function createUsersApiHandlers(config) {
1113
1374
  async update(req, id) {
1114
1375
  const authErr = await requireAuth(req);
1115
1376
  if (authErr) return authErr;
1377
+ if (requireEntityPermission) {
1378
+ const pe = await requireEntityPermission(req, "users", "update");
1379
+ if (pe) return pe;
1380
+ }
1116
1381
  try {
1117
1382
  const body = await req.json();
1118
1383
  const { password: _p, ...safe } = body;
@@ -1130,6 +1395,10 @@ function createUsersApiHandlers(config) {
1130
1395
  async delete(_req, id) {
1131
1396
  const authErr = await requireAuth(new Request(_req.url));
1132
1397
  if (authErr) return authErr;
1398
+ if (requireEntityPermission) {
1399
+ const pe = await requireEntityPermission(_req, "users", "delete");
1400
+ if (pe) return pe;
1401
+ }
1133
1402
  try {
1134
1403
  const r = await userRepo().delete(parseInt(id, 10));
1135
1404
  if (r.affected === 0) return json({ error: "User not found" }, { status: 404 });
@@ -1141,11 +1410,16 @@ function createUsersApiHandlers(config) {
1141
1410
  async regenerateInvite(_req, id) {
1142
1411
  const authErr = await requireAuth(new Request(_req.url));
1143
1412
  if (authErr) return authErr;
1413
+ if (requireEntityPermission) {
1414
+ const pe = await requireEntityPermission(_req, "users", "update");
1415
+ if (pe) return pe;
1416
+ }
1144
1417
  try {
1145
- const user = await userRepo().findOne({ where: { id: parseInt(id, 10) }, select: ["email"] });
1418
+ const user = await userRepo().findOne({ where: { id: parseInt(id, 10) }, select: ["email", "name"] });
1146
1419
  if (!user) return json({ error: "User not found" }, { status: 404 });
1147
1420
  const emailToken = Buffer.from(user.email).toString("base64");
1148
1421
  const inviteLink = `${baseUrl}/admin/invite?token=${emailToken}`;
1422
+ await trySendInviteEmail(user.email, inviteLink, user.name ?? "");
1149
1423
  return json({ message: "New invite link generated successfully", inviteLink });
1150
1424
  } catch {
1151
1425
  return json({ error: "Server Error" }, { status: 500 });
@@ -1357,7 +1631,7 @@ function createChatHandlers(config) {
1357
1631
  if (contextParts.length === 0) {
1358
1632
  const terms = getQueryTerms(message);
1359
1633
  if (terms.length > 0) {
1360
- const conditions = terms.map((t) => ({ content: (0, import_typeorm2.ILike)(`%${t}%`) }));
1634
+ const conditions = terms.map((t) => ({ content: (0, import_typeorm3.ILike)(`%${t}%`) }));
1361
1635
  const chunks = await chunkRepo().find({
1362
1636
  where: conditions,
1363
1637
  take: KB_CHUNK_LIMIT,
@@ -1395,8 +1669,207 @@ ${contextParts.join("\n\n")}` : "You are a helpful assistant for the company. If
1395
1669
  };
1396
1670
  }
1397
1671
 
1672
+ // src/auth/permission-entities.ts
1673
+ var PERMISSION_ENTITY_INTERNAL_EXCLUDE = /* @__PURE__ */ new Set([
1674
+ "users",
1675
+ "password_reset_tokens",
1676
+ "user_groups",
1677
+ "permissions",
1678
+ "comments",
1679
+ "form_fields",
1680
+ "configs",
1681
+ "knowledge_base_chunks",
1682
+ "carts",
1683
+ "cart_items",
1684
+ "wishlists",
1685
+ "wishlist_items"
1686
+ ]);
1687
+ var PERMISSION_LOGICAL_ENTITIES = [
1688
+ "users",
1689
+ "forms",
1690
+ "form_submissions",
1691
+ "dashboard",
1692
+ "upload",
1693
+ "settings",
1694
+ "analytics",
1695
+ "chat"
1696
+ ];
1697
+ var ADMIN_GROUP_NAME = "Administrator";
1698
+ function isSuperAdminGroupName(name) {
1699
+ return name === ADMIN_GROUP_NAME;
1700
+ }
1701
+ function getPermissionableEntityKeys(entityMap) {
1702
+ const fromMap = Object.keys(entityMap).filter((k) => !PERMISSION_ENTITY_INTERNAL_EXCLUDE.has(k));
1703
+ const logical = PERMISSION_LOGICAL_ENTITIES.filter((k) => !fromMap.includes(k));
1704
+ return [...fromMap.sort(), ...logical].filter((k, i, a) => a.indexOf(k) === i);
1705
+ }
1706
+
1707
+ // src/auth/helpers.ts
1708
+ function canManageRoles(user) {
1709
+ return !!(user?.email && user.isRBACAdmin);
1710
+ }
1711
+
1712
+ // src/api/admin-roles-handlers.ts
1713
+ function createAdminRolesHandlers(config) {
1714
+ const { dataSource, entityMap, json, getSessionUser } = config;
1715
+ const baseEntities = getPermissionableEntityKeys(entityMap);
1716
+ const allowEntities = /* @__PURE__ */ new Set([...baseEntities, "users"]);
1717
+ const groupRepo = () => dataSource.getRepository(entityMap.user_groups);
1718
+ const permRepo = () => dataSource.getRepository(entityMap.permissions);
1719
+ const userRepo = () => dataSource.getRepository(entityMap.users);
1720
+ async function gate() {
1721
+ const u = await getSessionUser();
1722
+ if (!u?.email) return json({ error: "Unauthorized" }, { status: 401 });
1723
+ if (!canManageRoles(u)) return json({ error: "Forbidden" }, { status: 403 });
1724
+ return null;
1725
+ }
1726
+ return {
1727
+ async list() {
1728
+ const err = await gate();
1729
+ if (err) return err;
1730
+ const groups = await groupRepo().find({
1731
+ where: { deleted: false },
1732
+ order: { id: "ASC" },
1733
+ relations: ["permissions"]
1734
+ });
1735
+ const entities = [...allowEntities].sort();
1736
+ return json({
1737
+ entities,
1738
+ groups: groups.map((g) => ({
1739
+ id: g.id,
1740
+ name: g.name,
1741
+ permissions: (g.permissions ?? []).filter((p) => !p.deleted).map((p) => ({
1742
+ entity: p.entity,
1743
+ canCreate: p.canCreate,
1744
+ canRead: p.canRead,
1745
+ canUpdate: p.canUpdate,
1746
+ canDelete: p.canDelete
1747
+ }))
1748
+ }))
1749
+ });
1750
+ },
1751
+ async createGroup(req) {
1752
+ const err = await gate();
1753
+ if (err) return err;
1754
+ try {
1755
+ const body = await req.json();
1756
+ const name = body?.name?.trim();
1757
+ if (!name) return json({ error: "Name is required" }, { status: 400 });
1758
+ const repo = groupRepo();
1759
+ const existing = await repo.findOne({ where: { name } });
1760
+ if (existing) return json({ error: "Group name already exists" }, { status: 400 });
1761
+ const g = await repo.save(repo.create({ name }));
1762
+ return json({ id: g.id, name: g.name, permissions: [] }, { status: 201 });
1763
+ } catch {
1764
+ return json({ error: "Server error" }, { status: 500 });
1765
+ }
1766
+ },
1767
+ async patchGroup(req, idStr) {
1768
+ const err = await gate();
1769
+ if (err) return err;
1770
+ const id = parseInt(idStr, 10);
1771
+ if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
1772
+ try {
1773
+ const body = await req.json();
1774
+ const name = body?.name?.trim();
1775
+ if (!name) return json({ error: "Name is required" }, { status: 400 });
1776
+ const repo = groupRepo();
1777
+ const g = await repo.findOne({ where: { id, deleted: false } });
1778
+ if (!g) return json({ error: "Not found" }, { status: 404 });
1779
+ if (isSuperAdminGroupName(g.name) && !isSuperAdminGroupName(name)) {
1780
+ return json({ error: "Cannot rename the administrator group" }, { status: 400 });
1781
+ }
1782
+ const dup = await repo.findOne({ where: { name } });
1783
+ if (dup && dup.id !== id) return json({ error: "Name already in use" }, { status: 400 });
1784
+ g.name = name;
1785
+ await repo.save(g);
1786
+ return json({ id: g.id, name: g.name });
1787
+ } catch {
1788
+ return json({ error: "Server error" }, { status: 500 });
1789
+ }
1790
+ },
1791
+ async deleteGroup(idStr) {
1792
+ const err = await gate();
1793
+ if (err) return err;
1794
+ const id = parseInt(idStr, 10);
1795
+ if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
1796
+ const repo = groupRepo();
1797
+ const g = await repo.findOne({ where: { id, deleted: false } });
1798
+ if (!g) return json({ error: "Not found" }, { status: 404 });
1799
+ if (isSuperAdminGroupName(g.name)) return json({ error: "Cannot delete the administrator group" }, { status: 400 });
1800
+ const userCount = await userRepo().count({ where: { groupId: id } });
1801
+ if (userCount > 0) return json({ error: "Reassign users before deleting this group" }, { status: 409 });
1802
+ await permRepo().delete({ groupId: id });
1803
+ await repo.update(id, { deleted: true, deletedAt: /* @__PURE__ */ new Date() });
1804
+ return json({ ok: true });
1805
+ },
1806
+ async putPermissions(req, idStr) {
1807
+ const err = await gate();
1808
+ if (err) return err;
1809
+ const groupId = parseInt(idStr, 10);
1810
+ if (!Number.isFinite(groupId)) return json({ error: "Invalid id" }, { status: 400 });
1811
+ const groupRepository = groupRepo();
1812
+ const g = await groupRepository.findOne({ where: { id: groupId, deleted: false } });
1813
+ if (!g) return json({ error: "Group not found" }, { status: 404 });
1814
+ try {
1815
+ const body = await req.json();
1816
+ const rows = body?.permissions;
1817
+ if (!Array.isArray(rows)) return json({ error: "permissions array required" }, { status: 400 });
1818
+ for (const r of rows) {
1819
+ if (!r?.entity || !allowEntities.has(r.entity)) {
1820
+ return json({ error: `Invalid entity: ${r?.entity ?? ""}` }, { status: 400 });
1821
+ }
1822
+ }
1823
+ await dataSource.transaction(async (em) => {
1824
+ await em.getRepository(entityMap.permissions).delete({ groupId });
1825
+ for (const r of rows) {
1826
+ await em.getRepository(entityMap.permissions).save(
1827
+ em.getRepository(entityMap.permissions).create({
1828
+ groupId,
1829
+ entity: r.entity,
1830
+ canCreate: !!r.canCreate,
1831
+ canRead: !!r.canRead,
1832
+ canUpdate: !!r.canUpdate,
1833
+ canDelete: !!r.canDelete
1834
+ })
1835
+ );
1836
+ }
1837
+ });
1838
+ const updated = await groupRepository.findOne({
1839
+ where: { id: groupId },
1840
+ relations: ["permissions"]
1841
+ });
1842
+ return json({
1843
+ id: groupId,
1844
+ permissions: (updated?.permissions ?? []).map((p) => ({
1845
+ entity: p.entity,
1846
+ canCreate: p.canCreate,
1847
+ canRead: p.canRead,
1848
+ canUpdate: p.canUpdate,
1849
+ canDelete: p.canDelete
1850
+ }))
1851
+ });
1852
+ } catch {
1853
+ return json({ error: "Server error" }, { status: 500 });
1854
+ }
1855
+ }
1856
+ };
1857
+ }
1858
+
1398
1859
  // src/api/cms-api-handler.ts
1399
- var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set(["users", "password_reset_tokens", "user_groups", "permissions", "comments", "form_fields", "configs"]);
1860
+ var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
1861
+ "users",
1862
+ "password_reset_tokens",
1863
+ "user_groups",
1864
+ "permissions",
1865
+ "comments",
1866
+ "form_fields",
1867
+ "configs",
1868
+ "carts",
1869
+ "cart_items",
1870
+ "wishlists",
1871
+ "wishlist_items"
1872
+ ]);
1400
1873
  function createCmsApiHandler(config) {
1401
1874
  const {
1402
1875
  dataSource,
@@ -1417,7 +1890,9 @@ function createCmsApiHandler(config) {
1417
1890
  userAvatar,
1418
1891
  userProfile,
1419
1892
  settings: settingsConfig,
1420
- chat: chatConfig
1893
+ chat: chatConfig,
1894
+ requireEntityPermission: reqEntityPerm,
1895
+ getSessionUser
1421
1896
  } = config;
1422
1897
  const analytics = analyticsConfig ?? (getCms ? {
1423
1898
  json: config.json,
@@ -1438,27 +1913,51 @@ function createCmsApiHandler(config) {
1438
1913
  ...userAuthConfig,
1439
1914
  sendEmail: async (opts) => {
1440
1915
  const cms = await getCms();
1916
+ const queue = cms.getPlugin("queue");
1917
+ const companyDetails = config.getCompanyDetails ? await config.getCompanyDetails() : {};
1918
+ 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] ?? "" : "");
1919
+ const ctx = { resetLink, companyDetails };
1920
+ if (queue) {
1921
+ const { queueEmail: queueEmail2 } = await Promise.resolve().then(() => (init_email_queue(), email_queue_exports));
1922
+ await queueEmail2(cms, { to: opts.to, templateName: "passwordReset", ctx });
1923
+ return;
1924
+ }
1441
1925
  const email = cms.getPlugin("email");
1442
1926
  if (!email?.send) return;
1443
- const { emailTemplates: emailTemplates2 } = await Promise.resolve().then(() => (init_email_service(), email_service_exports));
1444
- const { subject, html, text } = emailTemplates2.passwordReset({ resetLink: opts.text || opts.html });
1445
- await email.send({ subject, html, text, to: opts.to });
1927
+ const rendered = email.renderTemplate("passwordReset", ctx);
1928
+ await email.send({ subject: rendered.subject, html: rendered.html, text: rendered.text, to: opts.to });
1446
1929
  }
1447
1930
  } : userAuthConfig;
1448
- const crudOpts = { requireAuth: config.requireAuth, json: config.json };
1931
+ const crudOpts = {
1932
+ requireAuth: config.requireAuth,
1933
+ json: config.json,
1934
+ requireEntityPermission: reqEntityPerm
1935
+ };
1449
1936
  const crud = createCrudHandler(dataSource, entityMap, crudOpts);
1450
1937
  const crudById = createCrudByIdHandler(dataSource, entityMap, crudOpts);
1938
+ const mergePerm = (c) => !c ? void 0 : reqEntityPerm ? { ...c, requireEntityPermission: reqEntityPerm } : c;
1939
+ const adminRoles = getSessionUser && createAdminRolesHandlers({
1940
+ dataSource,
1941
+ entityMap,
1942
+ json: config.json,
1943
+ getSessionUser
1944
+ });
1451
1945
  const userAuthRouter = userAuth ? createUserAuthApiRouter(userAuth) : null;
1452
- const dashboardGet = dashboard ? createDashboardStatsHandler(dashboard) : null;
1946
+ const dashboardGet = dashboard ? createDashboardStatsHandler(mergePerm(dashboard) ?? dashboard) : null;
1453
1947
  const analyticsHandlers = analytics ? createAnalyticsHandlers(analytics) : null;
1454
- const uploadPost = upload ? createUploadHandler(upload) : null;
1948
+ const uploadPost = upload ? createUploadHandler(mergePerm(upload) ?? upload) : null;
1455
1949
  const blogBySlugGet = blogBySlug ? createBlogBySlugHandler(blogBySlug) : null;
1456
1950
  const formBySlugGet = formBySlug ? createFormBySlugHandler(formBySlug) : null;
1457
- const formSaveHandlers = formSaveConfig ? createFormSaveHandlers(formSaveConfig) : null;
1951
+ const formSaveHandlers = formSaveConfig ? createFormSaveHandlers(mergePerm(formSaveConfig) ?? formSaveConfig) : null;
1458
1952
  const formSubmissionPost = formSubmissionConfig ? createFormSubmissionHandler(formSubmissionConfig) : null;
1459
- const formSubmissionGetById = formSubmissionGetByIdConfig ? createFormSubmissionGetByIdHandler(formSubmissionGetByIdConfig) : null;
1460
- const formSubmissionList = formSubmissionGetByIdConfig ? createFormSubmissionListHandler(formSubmissionGetByIdConfig) : null;
1461
- const usersHandlers = usersApi ? createUsersApiHandlers(usersApi) : null;
1953
+ const formSubmissionGetById = formSubmissionGetByIdConfig ? createFormSubmissionGetByIdHandler(mergePerm(formSubmissionGetByIdConfig) ?? formSubmissionGetByIdConfig) : null;
1954
+ const formSubmissionList = formSubmissionGetByIdConfig ? createFormSubmissionListHandler(mergePerm(formSubmissionGetByIdConfig) ?? formSubmissionGetByIdConfig) : null;
1955
+ const usersApiMerged = usersApi && getCms ? {
1956
+ ...usersApi,
1957
+ getCms: usersApi.getCms ?? getCms,
1958
+ getCompanyDetails: usersApi.getCompanyDetails ?? config.getCompanyDetails
1959
+ } : usersApi;
1960
+ const usersHandlers = usersApiMerged ? createUsersApiHandlers(mergePerm(usersApiMerged) ?? usersApiMerged) : null;
1462
1961
  const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
1463
1962
  const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
1464
1963
  const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
@@ -1469,13 +1968,41 @@ function createCmsApiHandler(config) {
1469
1968
  }
1470
1969
  return {
1471
1970
  async handle(method, path, req) {
1971
+ const perm = reqEntityPerm;
1972
+ async function analyticsGate() {
1973
+ const a = await config.requireAuth(req);
1974
+ if (a) return a;
1975
+ if (perm) return perm(req, "analytics", "read");
1976
+ return null;
1977
+ }
1978
+ if (path[0] === "admin" && path[1] === "roles") {
1979
+ if (!adminRoles) return config.json({ error: "Not found" }, { status: 404 });
1980
+ if (path.length === 2 && method === "GET") return adminRoles.list();
1981
+ if (path.length === 2 && method === "POST") return adminRoles.createGroup(req);
1982
+ if (path.length === 3 && method === "PATCH") return adminRoles.patchGroup(req, path[2]);
1983
+ if (path.length === 3 && method === "DELETE") return adminRoles.deleteGroup(path[2]);
1984
+ if (path.length === 4 && path[3] === "permissions" && method === "PUT") return adminRoles.putPermissions(req, path[2]);
1985
+ return config.json({ error: "Not found" }, { status: 404 });
1986
+ }
1472
1987
  if (path[0] === "dashboard" && path[1] === "stats" && path.length === 2 && method === "GET" && dashboardGet) {
1473
1988
  return dashboardGet(req);
1474
1989
  }
1475
1990
  if (path[0] === "analytics" && analyticsHandlers) {
1476
- if (path.length === 1 && method === "GET") return analyticsHandlers.GET(req);
1477
- if (path.length === 2 && path[1] === "property-id" && method === "GET") return analyticsHandlers.propertyId();
1478
- if (path.length === 2 && path[1] === "permissions" && method === "GET") return analyticsHandlers.permissions();
1991
+ if (path.length === 1 && method === "GET") {
1992
+ const g = await analyticsGate();
1993
+ if (g) return g;
1994
+ return analyticsHandlers.GET(req);
1995
+ }
1996
+ if (path.length === 2 && path[1] === "property-id" && method === "GET") {
1997
+ const g = await analyticsGate();
1998
+ if (g) return g;
1999
+ return analyticsHandlers.propertyId();
2000
+ }
2001
+ if (path.length === 2 && path[1] === "permissions" && method === "GET") {
2002
+ const g = await analyticsGate();
2003
+ if (g) return g;
2004
+ return analyticsHandlers.permissions();
2005
+ }
1479
2006
  }
1480
2007
  if (path[0] === "upload" && path.length === 1 && method === "POST" && uploadPost) return uploadPost(req);
1481
2008
  if (path[0] === "blogs" && path[1] === "slug" && path.length === 3 && method === "GET" && blogBySlugGet) {
@@ -1519,8 +2046,24 @@ function createCmsApiHandler(config) {
1519
2046
  return userAuthRouter.POST(req, path[1]);
1520
2047
  }
1521
2048
  if (path[0] === "settings" && path.length === 2 && settingsHandlers) {
1522
- if (method === "GET") return settingsHandlers.GET(req, path[1]);
1523
- if (method === "PUT") return settingsHandlers.PUT(req, path[1]);
2049
+ const group = path[1];
2050
+ const isPublic = settingsConfig?.publicGetGroups?.includes(group);
2051
+ if (method === "GET") {
2052
+ if (!isPublic && perm) {
2053
+ const a = await config.requireAuth(req);
2054
+ if (a) return a;
2055
+ const pe = await perm(req, "settings", "read");
2056
+ if (pe) return pe;
2057
+ }
2058
+ return settingsHandlers.GET(req, group);
2059
+ }
2060
+ if (method === "PUT") {
2061
+ if (perm) {
2062
+ const pe = await perm(req, "settings", "update");
2063
+ if (pe) return pe;
2064
+ }
2065
+ return settingsHandlers.PUT(req, group);
2066
+ }
1524
2067
  }
1525
2068
  if (path[0] === "chat" && chatHandlers) {
1526
2069
  if (path.length === 2 && path[1] === "identify" && method === "POST") return chatHandlers.identify(req);
@@ -1557,6 +2100,1006 @@ function createCmsApiHandler(config) {
1557
2100
  }
1558
2101
  };
1559
2102
  }
2103
+
2104
+ // src/api/storefront-handlers.ts
2105
+ var import_typeorm4 = require("typeorm");
2106
+
2107
+ // src/lib/is-valid-signup-email.ts
2108
+ var MAX_EMAIL = 254;
2109
+ var MAX_LOCAL = 64;
2110
+ function isValidSignupEmail(email) {
2111
+ if (!email || email.length > MAX_EMAIL) return false;
2112
+ const at = email.indexOf("@");
2113
+ if (at <= 0 || at !== email.lastIndexOf("@")) return false;
2114
+ const local = email.slice(0, at);
2115
+ const domain = email.slice(at + 1);
2116
+ if (!local || local.length > MAX_LOCAL || !domain || domain.length > 253) return false;
2117
+ if (local.startsWith(".") || local.endsWith(".") || local.includes("..")) return false;
2118
+ if (domain.startsWith(".") || domain.endsWith(".") || domain.includes("..")) return false;
2119
+ if (!/^[a-z0-9._%+-]+$/i.test(local)) return false;
2120
+ if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/i.test(domain)) return false;
2121
+ const tld = domain.split(".").pop();
2122
+ return tld.length >= 2;
2123
+ }
2124
+
2125
+ // src/api/storefront-handlers.ts
2126
+ init_email_queue();
2127
+ var GUEST_COOKIE = "guest_id";
2128
+ var ONE_YEAR = 60 * 60 * 24 * 365;
2129
+ function parseCookies(header) {
2130
+ const out = {};
2131
+ if (!header) return out;
2132
+ for (const part of header.split(";")) {
2133
+ const i = part.indexOf("=");
2134
+ if (i === -1) continue;
2135
+ const k = part.slice(0, i).trim();
2136
+ const v = part.slice(i + 1).trim();
2137
+ out[k] = decodeURIComponent(v);
2138
+ }
2139
+ return out;
2140
+ }
2141
+ function guestCookieHeader(name, token) {
2142
+ return `${name}=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ONE_YEAR}`;
2143
+ }
2144
+ function orderNumber() {
2145
+ return `ORD-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
2146
+ }
2147
+ var SIGNUP_VERIFY_EXPIRY_HOURS = 72;
2148
+ function createStorefrontApiHandler(config) {
2149
+ const { dataSource, entityMap, json, getSessionUser, getCms, getCompanyDetails, publicSiteUrl } = config;
2150
+ const cookieName = config.guestCookieName ?? GUEST_COOKIE;
2151
+ const cartRepo = () => dataSource.getRepository(entityMap.carts);
2152
+ const cartItemRepo = () => dataSource.getRepository(entityMap.cart_items);
2153
+ const productRepo = () => dataSource.getRepository(entityMap.products);
2154
+ const contactRepo = () => dataSource.getRepository(entityMap.contacts);
2155
+ const addressRepo = () => dataSource.getRepository(entityMap.addresses);
2156
+ const orderRepo = () => dataSource.getRepository(entityMap.orders);
2157
+ const orderItemRepo = () => dataSource.getRepository(entityMap.order_items);
2158
+ const wishlistRepo = () => dataSource.getRepository(entityMap.wishlists);
2159
+ const wishlistItemRepo = () => dataSource.getRepository(entityMap.wishlist_items);
2160
+ const userRepo = () => dataSource.getRepository(entityMap.users);
2161
+ const tokenRepo = () => dataSource.getRepository(entityMap.password_reset_tokens);
2162
+ const collectionRepo = () => dataSource.getRepository(entityMap.collections);
2163
+ const groupRepo = () => dataSource.getRepository(entityMap.user_groups);
2164
+ async function ensureContactForUser(userId) {
2165
+ let c = await contactRepo().findOne({ where: { userId, deleted: false } });
2166
+ if (c) return c;
2167
+ const u = await userRepo().findOne({ where: { id: userId } });
2168
+ if (!u) return null;
2169
+ const unclaimed = await contactRepo().findOne({
2170
+ where: { email: u.email, userId: (0, import_typeorm4.IsNull)(), deleted: false }
2171
+ });
2172
+ if (unclaimed) {
2173
+ await contactRepo().update(unclaimed.id, { userId });
2174
+ return { id: unclaimed.id };
2175
+ }
2176
+ const created = await contactRepo().save(
2177
+ contactRepo().create({
2178
+ name: u.name,
2179
+ email: u.email,
2180
+ phone: null,
2181
+ userId,
2182
+ deleted: false
2183
+ })
2184
+ );
2185
+ return { id: created.id };
2186
+ }
2187
+ async function getOrCreateCart(req) {
2188
+ const u = await getSessionUser();
2189
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2190
+ if (Number.isFinite(uid)) {
2191
+ const contact = await ensureContactForUser(uid);
2192
+ if (!contact) return { cart: {}, setCookie: null, err: json({ error: "User not found" }, { status: 400 }) };
2193
+ let cart2 = await cartRepo().findOne({
2194
+ where: { contactId: contact.id },
2195
+ relations: ["items", "items.product"]
2196
+ });
2197
+ if (!cart2) {
2198
+ cart2 = await cartRepo().save(
2199
+ cartRepo().create({ contactId: contact.id, guestToken: null, currency: "INR" })
2200
+ );
2201
+ cart2 = await cartRepo().findOne({
2202
+ where: { id: cart2.id },
2203
+ relations: ["items", "items.product"]
2204
+ });
2205
+ }
2206
+ return { cart: cart2, setCookie: null, err: null };
2207
+ }
2208
+ const cookies = parseCookies(req.headers.get("cookie"));
2209
+ let token = cookies[cookieName] || "";
2210
+ if (!token) {
2211
+ token = crypto.randomUUID();
2212
+ let cart2 = await cartRepo().findOne({
2213
+ where: { guestToken: token },
2214
+ relations: ["items", "items.product"]
2215
+ });
2216
+ if (!cart2) {
2217
+ cart2 = await cartRepo().save(
2218
+ cartRepo().create({ guestToken: token, contactId: null, currency: "INR" })
2219
+ );
2220
+ cart2 = await cartRepo().findOne({
2221
+ where: { id: cart2.id },
2222
+ relations: ["items", "items.product"]
2223
+ });
2224
+ }
2225
+ return { cart: cart2, setCookie: guestCookieHeader(cookieName, token), err: null };
2226
+ }
2227
+ let cart = await cartRepo().findOne({
2228
+ where: { guestToken: token },
2229
+ relations: ["items", "items.product"]
2230
+ });
2231
+ if (!cart) {
2232
+ cart = await cartRepo().save(
2233
+ cartRepo().create({ guestToken: token, contactId: null, currency: "INR" })
2234
+ );
2235
+ cart = await cartRepo().findOne({
2236
+ where: { id: cart.id },
2237
+ relations: ["items", "items.product"]
2238
+ });
2239
+ }
2240
+ return { cart, setCookie: null, err: null };
2241
+ }
2242
+ function primaryProductImageUrl(metadata) {
2243
+ const meta = metadata;
2244
+ const images = meta?.images;
2245
+ if (!Array.isArray(images) || !images.length) return null;
2246
+ const sorted = images.filter((i) => i?.url);
2247
+ if (!sorted.length) return null;
2248
+ const di = sorted.findIndex((i) => i.isDefault);
2249
+ if (di > 0) {
2250
+ const [d] = sorted.splice(di, 1);
2251
+ sorted.unshift(d);
2252
+ }
2253
+ return sorted[0].url;
2254
+ }
2255
+ function serializeCart(cart) {
2256
+ const items = cart.items || [];
2257
+ return {
2258
+ id: cart.id,
2259
+ currency: cart.currency,
2260
+ items: items.map((it) => {
2261
+ const p = it.product;
2262
+ return {
2263
+ id: it.id,
2264
+ productId: it.productId,
2265
+ quantity: it.quantity,
2266
+ metadata: it.metadata,
2267
+ product: p ? {
2268
+ id: p.id,
2269
+ name: p.name,
2270
+ slug: p.slug,
2271
+ price: p.price,
2272
+ sku: p.sku,
2273
+ image: primaryProductImageUrl(p.metadata)
2274
+ } : null
2275
+ };
2276
+ })
2277
+ };
2278
+ }
2279
+ function serializeProduct(p) {
2280
+ return {
2281
+ id: p.id,
2282
+ name: p.name,
2283
+ slug: p.slug,
2284
+ sku: p.sku,
2285
+ hsn: p.hsn,
2286
+ price: p.price,
2287
+ compareAtPrice: p.compareAtPrice,
2288
+ status: p.status,
2289
+ collectionId: p.collectionId,
2290
+ metadata: p.metadata
2291
+ };
2292
+ }
2293
+ return {
2294
+ async handle(method, path, req) {
2295
+ try {
2296
+ let serializeAddress2 = function(a) {
2297
+ return {
2298
+ id: a.id,
2299
+ contactId: a.contactId,
2300
+ tag: a.tag,
2301
+ line1: a.line1,
2302
+ line2: a.line2,
2303
+ city: a.city,
2304
+ state: a.state,
2305
+ postalCode: a.postalCode,
2306
+ country: a.country
2307
+ };
2308
+ };
2309
+ var serializeAddress = serializeAddress2;
2310
+ if (path[0] === "products" && path.length === 1 && method === "GET") {
2311
+ const url = new URL(req.url || "", "http://localhost");
2312
+ const collectionSlug = url.searchParams.get("collection")?.trim();
2313
+ const collectionId = url.searchParams.get("collectionId");
2314
+ const limit = Math.min(100, Math.max(1, parseInt(url.searchParams.get("limit") || "20", 10)));
2315
+ const offset = Math.max(0, parseInt(url.searchParams.get("offset") || "0", 10));
2316
+ const where = { status: "available", deleted: false };
2317
+ let collectionFilter = null;
2318
+ if (collectionSlug) {
2319
+ let col = null;
2320
+ if (/^\d+$/.test(collectionSlug)) {
2321
+ col = await collectionRepo().findOne({
2322
+ where: {
2323
+ id: parseInt(collectionSlug, 10),
2324
+ active: true,
2325
+ deleted: false
2326
+ }
2327
+ });
2328
+ } else {
2329
+ 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();
2330
+ }
2331
+ if (!col) {
2332
+ return json({ products: [], total: 0, collection: null });
2333
+ }
2334
+ where.collectionId = col.id;
2335
+ collectionFilter = { name: col.name, slug: col.slug };
2336
+ } else if (collectionId) {
2337
+ const cid = parseInt(collectionId, 10);
2338
+ if (Number.isFinite(cid)) where.collectionId = cid;
2339
+ }
2340
+ const [items, total] = await productRepo().findAndCount({
2341
+ where,
2342
+ order: { id: "ASC" },
2343
+ take: limit,
2344
+ skip: offset
2345
+ });
2346
+ return json({
2347
+ products: items.map(serializeProduct),
2348
+ total,
2349
+ ...collectionFilter && { collection: collectionFilter }
2350
+ });
2351
+ }
2352
+ if (path[0] === "products" && path.length === 2 && method === "GET") {
2353
+ const idOrSlug = path[1];
2354
+ const byId = /^\d+$/.test(idOrSlug);
2355
+ const product = await productRepo().findOne({
2356
+ where: byId ? { id: parseInt(idOrSlug, 10), status: "available", deleted: false } : { slug: idOrSlug, status: "available", deleted: false },
2357
+ relations: ["attributes", "attributes.attribute"]
2358
+ });
2359
+ if (!product) return json({ error: "Not found" }, { status: 404 });
2360
+ const p = product;
2361
+ const attrRows = p.attributes ?? [];
2362
+ const attributeTags = attrRows.map((pa) => ({
2363
+ name: pa.attribute?.name ?? "",
2364
+ value: String(pa.value ?? "")
2365
+ })).filter((t) => t.name || t.value);
2366
+ return json({ ...serializeProduct(p), attributes: attributeTags });
2367
+ }
2368
+ if (path[0] === "collections" && path.length === 1 && method === "GET") {
2369
+ const items = await collectionRepo().find({
2370
+ where: { active: true, deleted: false },
2371
+ order: { sortOrder: "ASC", id: "ASC" }
2372
+ });
2373
+ const ids = items.map((c) => c.id);
2374
+ const countByCollection = {};
2375
+ if (ids.length > 0) {
2376
+ 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();
2377
+ for (const r of rows) {
2378
+ const cid = r.collectionId;
2379
+ if (cid != null) countByCollection[Number(cid)] = parseInt(String(r.cnt), 10);
2380
+ }
2381
+ }
2382
+ return json({
2383
+ collections: items.map((c) => {
2384
+ const col = c;
2385
+ const id = col.id;
2386
+ return {
2387
+ id,
2388
+ name: col.name,
2389
+ slug: col.slug,
2390
+ description: col.description,
2391
+ image: col.image,
2392
+ productCount: countByCollection[id] ?? 0
2393
+ };
2394
+ })
2395
+ });
2396
+ }
2397
+ if (path[0] === "collections" && path.length === 2 && method === "GET") {
2398
+ const idOrSlug = path[1];
2399
+ const byId = /^\d+$/.test(idOrSlug);
2400
+ const collection = await collectionRepo().findOne({
2401
+ where: byId ? { id: parseInt(idOrSlug, 10), active: true, deleted: false } : { slug: idOrSlug, active: true, deleted: false }
2402
+ });
2403
+ if (!collection) return json({ error: "Not found" }, { status: 404 });
2404
+ const col = collection;
2405
+ const products = await productRepo().find({
2406
+ where: { collectionId: col.id, status: "available", deleted: false },
2407
+ order: { id: "ASC" }
2408
+ });
2409
+ return json({
2410
+ id: col.id,
2411
+ name: col.name,
2412
+ slug: col.slug,
2413
+ description: col.description,
2414
+ image: col.image,
2415
+ products: products.map((p) => serializeProduct(p))
2416
+ });
2417
+ }
2418
+ if (path[0] === "profile" && path.length === 1 && method === "GET") {
2419
+ const u = await getSessionUser();
2420
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2421
+ if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
2422
+ const user = await userRepo().findOne({ where: { id: uid }, select: ["id", "name", "email"] });
2423
+ if (!user) return json({ error: "Not found" }, { status: 404 });
2424
+ const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
2425
+ return json({
2426
+ user: { id: user.id, name: user.name, email: user.email },
2427
+ contact: contact ? {
2428
+ id: contact.id,
2429
+ name: contact.name,
2430
+ email: contact.email,
2431
+ phone: contact.phone
2432
+ } : null
2433
+ });
2434
+ }
2435
+ if (path[0] === "profile" && path.length === 1 && method === "PUT") {
2436
+ const u = await getSessionUser();
2437
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2438
+ if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
2439
+ const b = await req.json().catch(() => ({}));
2440
+ const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
2441
+ if (contact) {
2442
+ const updates = {};
2443
+ if (typeof b.name === "string" && b.name.trim()) updates.name = b.name.trim();
2444
+ if (b.phone !== void 0) updates.phone = b.phone === null || b.phone === "" ? null : String(b.phone);
2445
+ if (Object.keys(updates).length) await contactRepo().update(contact.id, updates);
2446
+ }
2447
+ const user = await userRepo().findOne({ where: { id: uid }, select: ["id", "name", "email"] });
2448
+ if (user && typeof b.name === "string" && b.name.trim()) {
2449
+ await userRepo().update(uid, { name: b.name.trim() });
2450
+ }
2451
+ const updatedContact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
2452
+ const updatedUser = await userRepo().findOne({ where: { id: uid }, select: ["id", "name", "email"] });
2453
+ return json({
2454
+ user: updatedUser ? { id: updatedUser.id, name: updatedUser.name, email: updatedUser.email } : null,
2455
+ contact: updatedContact ? {
2456
+ id: updatedContact.id,
2457
+ name: updatedContact.name,
2458
+ email: updatedContact.email,
2459
+ phone: updatedContact.phone
2460
+ } : null
2461
+ });
2462
+ }
2463
+ async function getContactForAddresses() {
2464
+ const u = await getSessionUser();
2465
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2466
+ if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
2467
+ const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
2468
+ if (!contact) return json({ error: "Contact not found" }, { status: 404 });
2469
+ return { contactId: contact.id };
2470
+ }
2471
+ if (path[0] === "addresses" && path.length === 1 && method === "GET") {
2472
+ const contactOrErr = await getContactForAddresses();
2473
+ if (contactOrErr instanceof Response) return contactOrErr;
2474
+ const list = await addressRepo().find({
2475
+ where: { contactId: contactOrErr.contactId },
2476
+ order: { id: "ASC" }
2477
+ });
2478
+ return json({ addresses: list.map((a) => serializeAddress2(a)) });
2479
+ }
2480
+ if (path[0] === "addresses" && path.length === 1 && method === "POST") {
2481
+ const contactOrErr = await getContactForAddresses();
2482
+ if (contactOrErr instanceof Response) return contactOrErr;
2483
+ const b = await req.json().catch(() => ({}));
2484
+ const created = await addressRepo().save(
2485
+ addressRepo().create({
2486
+ contactId: contactOrErr.contactId,
2487
+ tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
2488
+ line1: typeof b.line1 === "string" ? b.line1.trim() || null : null,
2489
+ line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
2490
+ city: typeof b.city === "string" ? b.city.trim() || null : null,
2491
+ state: typeof b.state === "string" ? b.state.trim() || null : null,
2492
+ postalCode: typeof b.postalCode === "string" ? b.postalCode.trim() || null : null,
2493
+ country: typeof b.country === "string" ? b.country.trim() || null : null
2494
+ })
2495
+ );
2496
+ return json(serializeAddress2(created));
2497
+ }
2498
+ if (path[0] === "addresses" && path.length === 2 && (method === "PATCH" || method === "PUT")) {
2499
+ const contactOrErr = await getContactForAddresses();
2500
+ if (contactOrErr instanceof Response) return contactOrErr;
2501
+ const id = parseInt(path[1], 10);
2502
+ if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
2503
+ const existing = await addressRepo().findOne({ where: { id, contactId: contactOrErr.contactId } });
2504
+ if (!existing) return json({ error: "Not found" }, { status: 404 });
2505
+ const b = await req.json().catch(() => ({}));
2506
+ const updates = {};
2507
+ if (b.tag !== void 0) updates.tag = typeof b.tag === "string" ? b.tag.trim() || null : null;
2508
+ if (b.line1 !== void 0) updates.line1 = typeof b.line1 === "string" ? b.line1.trim() || null : null;
2509
+ if (b.line2 !== void 0) updates.line2 = typeof b.line2 === "string" ? b.line2.trim() || null : null;
2510
+ if (b.city !== void 0) updates.city = typeof b.city === "string" ? b.city.trim() || null : null;
2511
+ if (b.state !== void 0) updates.state = typeof b.state === "string" ? b.state.trim() || null : null;
2512
+ if (b.postalCode !== void 0) updates.postalCode = typeof b.postalCode === "string" ? b.postalCode.trim() || null : null;
2513
+ if (b.country !== void 0) updates.country = typeof b.country === "string" ? b.country.trim() || null : null;
2514
+ if (Object.keys(updates).length) await addressRepo().update(id, updates);
2515
+ const updated = await addressRepo().findOne({ where: { id } });
2516
+ return json(serializeAddress2(updated));
2517
+ }
2518
+ if (path[0] === "addresses" && path.length === 2 && method === "DELETE") {
2519
+ const contactOrErr = await getContactForAddresses();
2520
+ if (contactOrErr instanceof Response) return contactOrErr;
2521
+ const id = parseInt(path[1], 10);
2522
+ if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
2523
+ const existing = await addressRepo().findOne({ where: { id, contactId: contactOrErr.contactId } });
2524
+ if (!existing) return json({ error: "Not found" }, { status: 404 });
2525
+ await addressRepo().delete(id);
2526
+ return json({ deleted: true });
2527
+ }
2528
+ if (path[0] === "verify-email" && path.length === 1 && method === "POST") {
2529
+ const b = await req.json().catch(() => ({}));
2530
+ const token = typeof b.token === "string" ? b.token.trim() : "";
2531
+ if (!token) return json({ error: "token is required" }, { status: 400 });
2532
+ const record = await tokenRepo().findOne({ where: { token } });
2533
+ if (!record || record.expiresAt < /* @__PURE__ */ new Date()) {
2534
+ return json({ error: "Invalid or expired link. Please sign up again or contact support." }, { status: 400 });
2535
+ }
2536
+ const email = record.email;
2537
+ const user = await userRepo().findOne({ where: { email }, select: ["id", "blocked"] });
2538
+ if (!user) return json({ error: "User not found" }, { status: 400 });
2539
+ await userRepo().update(user.id, { blocked: false, updatedAt: /* @__PURE__ */ new Date() });
2540
+ await tokenRepo().delete({ email });
2541
+ return json({ success: true, message: "Email verified. You can sign in." });
2542
+ }
2543
+ if (path[0] === "register" && path.length === 1 && method === "POST") {
2544
+ if (!config.hashPassword) return json({ error: "Registration not configured" }, { status: 501 });
2545
+ const b = await req.json().catch(() => ({}));
2546
+ const name = typeof b.name === "string" ? b.name.trim() : "";
2547
+ const email = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
2548
+ const password = typeof b.password === "string" ? b.password : "";
2549
+ if (!name || !email || !password) return json({ error: "name, email and password are required" }, { status: 400 });
2550
+ if (!isValidSignupEmail(email)) return json({ error: "Invalid email address" }, { status: 400 });
2551
+ const existing = await userRepo().findOne({ where: { email } });
2552
+ if (existing) return json({ error: "User with this email already exists" }, { status: 400 });
2553
+ const customerG = await groupRepo().findOne({ where: { name: "Customer", deleted: false } });
2554
+ const groupId = customerG ? customerG.id : null;
2555
+ const hashed = await config.hashPassword(password);
2556
+ const requireEmailVerification = Boolean(getCms);
2557
+ const newUser = await userRepo().save(
2558
+ userRepo().create({
2559
+ name,
2560
+ email,
2561
+ password: hashed,
2562
+ blocked: requireEmailVerification,
2563
+ groupId,
2564
+ adminAccess: false
2565
+ })
2566
+ );
2567
+ const userId = newUser.id;
2568
+ await linkUnclaimedContactToUser(dataSource, entityMap.contacts, userId, email);
2569
+ let emailVerificationSent = false;
2570
+ if (requireEmailVerification && getCms) {
2571
+ try {
2572
+ const crypto2 = await import("crypto");
2573
+ const rawToken = crypto2.randomBytes(32).toString("hex");
2574
+ const expiresAt = new Date(Date.now() + SIGNUP_VERIFY_EXPIRY_HOURS * 60 * 60 * 1e3);
2575
+ await tokenRepo().save(
2576
+ tokenRepo().create({ email, token: rawToken, expiresAt })
2577
+ );
2578
+ const cms = await getCms();
2579
+ const companyDetails = getCompanyDetails ? await getCompanyDetails() : {};
2580
+ const base = (publicSiteUrl || "").replace(/\/$/, "").trim() || "http://localhost:3000";
2581
+ const verifyEmailUrl = `${base}/verify-email?token=${encodeURIComponent(rawToken)}`;
2582
+ await queueEmail(cms, {
2583
+ to: email,
2584
+ templateName: "signup",
2585
+ ctx: { name, verifyEmailUrl, companyDetails: companyDetails ?? {} }
2586
+ });
2587
+ emailVerificationSent = true;
2588
+ } catch {
2589
+ await userRepo().update(userId, { blocked: false, updatedAt: /* @__PURE__ */ new Date() });
2590
+ }
2591
+ }
2592
+ return json({
2593
+ success: true,
2594
+ userId,
2595
+ emailVerificationSent
2596
+ });
2597
+ }
2598
+ if (path[0] === "cart" && path.length === 1 && method === "GET") {
2599
+ const { cart, setCookie, err } = await getOrCreateCart(req);
2600
+ if (err) return err;
2601
+ const body = serializeCart(cart);
2602
+ if (setCookie) return json(body, { headers: { "Set-Cookie": setCookie } });
2603
+ return json(body);
2604
+ }
2605
+ if (path[0] === "cart" && path[1] === "items" && path.length === 2 && method === "POST") {
2606
+ const body = await req.json().catch(() => ({}));
2607
+ const productId = Number(body.productId);
2608
+ const quantity = Math.max(1, Number(body.quantity) || 1);
2609
+ if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
2610
+ const product = await productRepo().findOne({ where: { id: productId, deleted: false } });
2611
+ if (!product) return json({ error: "Product not found" }, { status: 404 });
2612
+ const { cart, setCookie, err } = await getOrCreateCart(req);
2613
+ if (err) return err;
2614
+ const cartId = cart.id;
2615
+ const existing = await cartItemRepo().findOne({ where: { cartId, productId } });
2616
+ if (existing) {
2617
+ await cartItemRepo().update(existing.id, {
2618
+ quantity: existing.quantity + quantity
2619
+ });
2620
+ } else {
2621
+ await cartItemRepo().save(
2622
+ cartItemRepo().create({ cartId, productId, quantity })
2623
+ );
2624
+ }
2625
+ await cartRepo().update(cartId, { updatedAt: /* @__PURE__ */ new Date() });
2626
+ const fresh = await cartRepo().findOne({
2627
+ where: { id: cartId },
2628
+ relations: ["items", "items.product"]
2629
+ });
2630
+ const out = serializeCart(fresh);
2631
+ if (setCookie) return json(out, { headers: { "Set-Cookie": setCookie } });
2632
+ return json(out);
2633
+ }
2634
+ if (path[0] === "cart" && path[1] === "items" && path.length === 3) {
2635
+ const itemId = parseInt(path[2], 10);
2636
+ if (!Number.isFinite(itemId)) return json({ error: "Invalid item id" }, { status: 400 });
2637
+ const { cart, setCookie, err } = await getOrCreateCart(req);
2638
+ if (err) return err;
2639
+ const cartId = cart.id;
2640
+ const item = await cartItemRepo().findOne({ where: { id: itemId, cartId } });
2641
+ if (!item) return json({ error: "Not found" }, { status: 404 });
2642
+ if (method === "DELETE") {
2643
+ await cartItemRepo().delete(itemId);
2644
+ await cartRepo().update(cartId, { updatedAt: /* @__PURE__ */ new Date() });
2645
+ const fresh = await cartRepo().findOne({
2646
+ where: { id: cartId },
2647
+ relations: ["items", "items.product"]
2648
+ });
2649
+ const out = serializeCart(fresh);
2650
+ if (setCookie) return json(out, { headers: { "Set-Cookie": setCookie } });
2651
+ return json(out);
2652
+ }
2653
+ if (method === "PATCH") {
2654
+ const b = await req.json().catch(() => ({}));
2655
+ const q = Math.max(0, Number(b.quantity) || 0);
2656
+ if (q === 0) await cartItemRepo().delete(itemId);
2657
+ else await cartItemRepo().update(itemId, { quantity: q });
2658
+ await cartRepo().update(cartId, { updatedAt: /* @__PURE__ */ new Date() });
2659
+ const fresh = await cartRepo().findOne({
2660
+ where: { id: cartId },
2661
+ relations: ["items", "items.product"]
2662
+ });
2663
+ const out = serializeCart(fresh);
2664
+ if (setCookie) return json(out, { headers: { "Set-Cookie": setCookie } });
2665
+ return json(out);
2666
+ }
2667
+ }
2668
+ if (path[0] === "cart" && path[1] === "merge" && method === "POST") {
2669
+ const u = await getSessionUser();
2670
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2671
+ if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
2672
+ const contact = await ensureContactForUser(uid);
2673
+ if (!contact) return json({ error: "Contact not found" }, { status: 400 });
2674
+ const cookies = parseCookies(req.headers.get("cookie"));
2675
+ const guestToken = cookies[cookieName];
2676
+ if (!guestToken) return json({ merged: false, message: "No guest cart" });
2677
+ const guestCart = await cartRepo().findOne({
2678
+ where: { guestToken },
2679
+ relations: ["items"]
2680
+ });
2681
+ if (!guestCart || !(guestCart.items || []).length) {
2682
+ let uc = await cartRepo().findOne({
2683
+ where: { contactId: contact.id },
2684
+ relations: ["items", "items.product"]
2685
+ });
2686
+ if (!uc) uc = { items: [] };
2687
+ return json(
2688
+ { merged: false, cart: serializeCart(uc) },
2689
+ { headers: { "Set-Cookie": `${cookieName}=; Path=/; Max-Age=0` } }
2690
+ );
2691
+ }
2692
+ let userCart = await cartRepo().findOne({ where: { contactId: contact.id } });
2693
+ if (!userCart) {
2694
+ userCart = await cartRepo().save(
2695
+ cartRepo().create({ contactId: contact.id, guestToken: null, currency: guestCart.currency })
2696
+ );
2697
+ }
2698
+ const uidCart = userCart.id;
2699
+ const gItems = guestCart.items || [];
2700
+ for (const gi of gItems) {
2701
+ const existing = await cartItemRepo().findOne({
2702
+ where: { cartId: uidCart, productId: gi.productId }
2703
+ });
2704
+ if (existing) {
2705
+ await cartItemRepo().update(existing.id, {
2706
+ quantity: existing.quantity + gi.quantity
2707
+ });
2708
+ } else {
2709
+ await cartItemRepo().save(
2710
+ cartItemRepo().create({
2711
+ cartId: uidCart,
2712
+ productId: gi.productId,
2713
+ quantity: gi.quantity,
2714
+ metadata: gi.metadata
2715
+ })
2716
+ );
2717
+ }
2718
+ }
2719
+ await cartRepo().delete(guestCart.id);
2720
+ await cartRepo().update(uidCart, { updatedAt: /* @__PURE__ */ new Date() });
2721
+ const fresh = await cartRepo().findOne({
2722
+ where: { id: uidCart },
2723
+ relations: ["items", "items.product"]
2724
+ });
2725
+ const guestWishlist = await wishlistRepo().findOne({
2726
+ where: { guestId: guestToken },
2727
+ relations: ["items"]
2728
+ });
2729
+ if (guestWishlist && (guestWishlist.items || []).length > 0) {
2730
+ const userWishlist = await getDefaultWishlist(contact.id);
2731
+ const gItems2 = guestWishlist.items || [];
2732
+ for (const gi of gItems2) {
2733
+ const pid = gi.productId;
2734
+ const ex = await wishlistItemRepo().findOne({ where: { wishlistId: userWishlist.id, productId: pid } });
2735
+ if (!ex) await wishlistItemRepo().save(wishlistItemRepo().create({ wishlistId: userWishlist.id, productId: pid }));
2736
+ }
2737
+ await wishlistRepo().delete(guestWishlist.id);
2738
+ }
2739
+ return json({ merged: true, cart: serializeCart(fresh) }, { headers: { "Set-Cookie": `${cookieName}=; Path=/; Max-Age=0` } });
2740
+ }
2741
+ async function getDefaultWishlist(contactId) {
2742
+ let w = await wishlistRepo().findOne({ where: { contactId, name: "default" } });
2743
+ if (!w) {
2744
+ w = await wishlistRepo().save(wishlistRepo().create({ contactId, guestId: null, name: "default" }));
2745
+ }
2746
+ return w;
2747
+ }
2748
+ async function getOrCreateWishlist(req2) {
2749
+ const u = await getSessionUser();
2750
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2751
+ if (Number.isFinite(uid)) {
2752
+ const contact = await ensureContactForUser(uid);
2753
+ if (!contact) return { wishlist: {}, setCookie: null, err: json({ error: "User not found" }, { status: 400 }) };
2754
+ const w2 = await getDefaultWishlist(contact.id);
2755
+ const wishlist = await wishlistRepo().findOne({ where: { id: w2.id } });
2756
+ return { wishlist, setCookie: null, err: null };
2757
+ }
2758
+ const cookies = parseCookies(req2.headers.get("cookie"));
2759
+ let token = cookies[cookieName] || "";
2760
+ if (!token) {
2761
+ token = crypto.randomUUID();
2762
+ let w2 = await wishlistRepo().findOne({ where: { guestId: token } });
2763
+ if (!w2) {
2764
+ w2 = await wishlistRepo().save(wishlistRepo().create({ guestId: token, contactId: null, name: "default" }));
2765
+ }
2766
+ return { wishlist: w2, setCookie: guestCookieHeader(cookieName, token), err: null };
2767
+ }
2768
+ let w = await wishlistRepo().findOne({ where: { guestId: token } });
2769
+ if (!w) {
2770
+ w = await wishlistRepo().save(wishlistRepo().create({ guestId: token, contactId: null, name: "default" }));
2771
+ }
2772
+ return { wishlist: w, setCookie: null, err: null };
2773
+ }
2774
+ if (path[0] === "wishlist" && path.length === 1 && method === "GET") {
2775
+ const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
2776
+ if (err) return err;
2777
+ const items = await wishlistItemRepo().find({
2778
+ where: { wishlistId: wishlist.id },
2779
+ relations: ["product"]
2780
+ });
2781
+ const body = {
2782
+ wishlistId: wishlist.id,
2783
+ items: items.map((it) => {
2784
+ const p = it.product;
2785
+ return {
2786
+ id: it.id,
2787
+ productId: it.productId,
2788
+ product: p ? {
2789
+ id: p.id,
2790
+ name: p.name,
2791
+ slug: p.slug,
2792
+ price: p.price,
2793
+ sku: p.sku,
2794
+ image: primaryProductImageUrl(p.metadata)
2795
+ } : null
2796
+ };
2797
+ })
2798
+ };
2799
+ if (setCookie) return json(body, { headers: { "Set-Cookie": setCookie } });
2800
+ return json(body);
2801
+ }
2802
+ if (path[0] === "wishlist" && path[1] === "items" && path.length === 2 && method === "POST") {
2803
+ const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
2804
+ if (err) return err;
2805
+ const b = await req.json().catch(() => ({}));
2806
+ const productId = Number(b.productId);
2807
+ if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
2808
+ const wid = wishlist.id;
2809
+ const ex = await wishlistItemRepo().findOne({ where: { wishlistId: wid, productId } });
2810
+ if (!ex) await wishlistItemRepo().save(wishlistItemRepo().create({ wishlistId: wid, productId }));
2811
+ if (setCookie) return json({ ok: true }, { headers: { "Set-Cookie": setCookie } });
2812
+ return json({ ok: true });
2813
+ }
2814
+ if (path[0] === "wishlist" && path[1] === "items" && path.length === 3 && method === "DELETE") {
2815
+ const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
2816
+ if (err) return err;
2817
+ const productId = parseInt(path[2], 10);
2818
+ await wishlistItemRepo().delete({ wishlistId: wishlist.id, productId });
2819
+ if (setCookie) return json({ ok: true }, { headers: { "Set-Cookie": setCookie } });
2820
+ return json({ ok: true });
2821
+ }
2822
+ if (path[0] === "checkout" && path[1] === "order" && path.length === 2 && method === "POST") {
2823
+ const b = await req.json().catch(() => ({}));
2824
+ const u = await getSessionUser();
2825
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2826
+ let contactId;
2827
+ let cart;
2828
+ if (Number.isFinite(uid)) {
2829
+ const contact = await ensureContactForUser(uid);
2830
+ if (!contact) return json({ error: "Contact required" }, { status: 400 });
2831
+ contactId = contact.id;
2832
+ cart = await cartRepo().findOne({
2833
+ where: { contactId },
2834
+ relations: ["items", "items.product"]
2835
+ });
2836
+ } else {
2837
+ const email = (b.email || "").trim();
2838
+ const name = (b.name || "").trim();
2839
+ if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
2840
+ let contact = await contactRepo().findOne({ where: { email, deleted: false } });
2841
+ if (contact && contact.userId != null) {
2842
+ return json({ error: "Please sign in to complete checkout" }, { status: 400 });
2843
+ }
2844
+ if (!contact) {
2845
+ contact = await contactRepo().save(
2846
+ contactRepo().create({
2847
+ name,
2848
+ email,
2849
+ phone: b.phone || null,
2850
+ userId: null,
2851
+ deleted: false
2852
+ })
2853
+ );
2854
+ } else if (name) await contactRepo().update(contact.id, { name, phone: b.phone || contact.phone });
2855
+ contactId = contact.id;
2856
+ const cookies = parseCookies(req.headers.get("cookie"));
2857
+ const guestToken = cookies[cookieName];
2858
+ if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
2859
+ cart = await cartRepo().findOne({
2860
+ where: { guestToken },
2861
+ relations: ["items", "items.product"]
2862
+ });
2863
+ }
2864
+ if (!cart || !(cart.items || []).length) {
2865
+ return json({ error: "Cart is empty" }, { status: 400 });
2866
+ }
2867
+ let subtotal = 0;
2868
+ const lines = [];
2869
+ for (const it of cart.items || []) {
2870
+ const p = it.product;
2871
+ if (!p || p.deleted || p.status !== "available") continue;
2872
+ const unit = Number(p.price);
2873
+ const qty = it.quantity || 1;
2874
+ const lineTotal = unit * qty;
2875
+ subtotal += lineTotal;
2876
+ lines.push({ productId: p.id, quantity: qty, unitPrice: unit, tax: 0, total: lineTotal });
2877
+ }
2878
+ if (!lines.length) return json({ error: "No available items in cart" }, { status: 400 });
2879
+ const total = subtotal;
2880
+ const cartId = cart.id;
2881
+ const ord = await orderRepo().save(
2882
+ orderRepo().create({
2883
+ orderNumber: orderNumber(),
2884
+ contactId,
2885
+ billingAddressId: b.billingAddressId ?? null,
2886
+ shippingAddressId: b.shippingAddressId ?? null,
2887
+ status: "pending",
2888
+ subtotal,
2889
+ tax: 0,
2890
+ discount: 0,
2891
+ total,
2892
+ currency: cart.currency || "INR",
2893
+ metadata: { cartId }
2894
+ })
2895
+ );
2896
+ const oid = ord.id;
2897
+ for (const line of lines) {
2898
+ await orderItemRepo().save(
2899
+ orderItemRepo().create({
2900
+ orderId: oid,
2901
+ productId: line.productId,
2902
+ quantity: line.quantity,
2903
+ unitPrice: line.unitPrice,
2904
+ tax: line.tax,
2905
+ total: line.total
2906
+ })
2907
+ );
2908
+ }
2909
+ return json({
2910
+ orderId: oid,
2911
+ orderNumber: ord.orderNumber,
2912
+ total,
2913
+ currency: cart.currency || "INR"
2914
+ });
2915
+ }
2916
+ if (path[0] === "checkout" && path.length === 1 && method === "POST") {
2917
+ const b = await req.json().catch(() => ({}));
2918
+ const u = await getSessionUser();
2919
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2920
+ let contactId;
2921
+ let cart;
2922
+ if (Number.isFinite(uid)) {
2923
+ const contact = await ensureContactForUser(uid);
2924
+ if (!contact) return json({ error: "Contact required" }, { status: 400 });
2925
+ contactId = contact.id;
2926
+ cart = await cartRepo().findOne({
2927
+ where: { contactId },
2928
+ relations: ["items", "items.product"]
2929
+ });
2930
+ } else {
2931
+ const email = (b.email || "").trim();
2932
+ const name = (b.name || "").trim();
2933
+ if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
2934
+ let contact = await contactRepo().findOne({ where: { email, deleted: false } });
2935
+ if (contact && contact.userId != null) {
2936
+ return json({ error: "Please sign in to complete checkout" }, { status: 400 });
2937
+ }
2938
+ if (!contact) {
2939
+ contact = await contactRepo().save(
2940
+ contactRepo().create({
2941
+ name,
2942
+ email,
2943
+ phone: b.phone || null,
2944
+ userId: null,
2945
+ deleted: false
2946
+ })
2947
+ );
2948
+ } else if (name) await contactRepo().update(contact.id, { name, phone: b.phone || contact.phone });
2949
+ contactId = contact.id;
2950
+ const cookies = parseCookies(req.headers.get("cookie"));
2951
+ const guestToken = cookies[cookieName];
2952
+ if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
2953
+ cart = await cartRepo().findOne({
2954
+ where: { guestToken },
2955
+ relations: ["items", "items.product"]
2956
+ });
2957
+ }
2958
+ if (!cart || !(cart.items || []).length) {
2959
+ return json({ error: "Cart is empty" }, { status: 400 });
2960
+ }
2961
+ let subtotal = 0;
2962
+ const lines = [];
2963
+ for (const it of cart.items || []) {
2964
+ const p = it.product;
2965
+ if (!p || p.deleted || p.status !== "available") continue;
2966
+ const unit = Number(p.price);
2967
+ const qty = it.quantity || 1;
2968
+ const lineTotal = unit * qty;
2969
+ subtotal += lineTotal;
2970
+ lines.push({ productId: p.id, quantity: qty, unitPrice: unit, tax: 0, total: lineTotal });
2971
+ }
2972
+ if (!lines.length) return json({ error: "No available items in cart" }, { status: 400 });
2973
+ const total = subtotal;
2974
+ const ord = await orderRepo().save(
2975
+ orderRepo().create({
2976
+ orderNumber: orderNumber(),
2977
+ contactId,
2978
+ billingAddressId: b.billingAddressId ?? null,
2979
+ shippingAddressId: b.shippingAddressId ?? null,
2980
+ status: "pending",
2981
+ subtotal,
2982
+ tax: 0,
2983
+ discount: 0,
2984
+ total,
2985
+ currency: cart.currency || "INR"
2986
+ })
2987
+ );
2988
+ const oid = ord.id;
2989
+ for (const line of lines) {
2990
+ await orderItemRepo().save(
2991
+ orderItemRepo().create({
2992
+ orderId: oid,
2993
+ productId: line.productId,
2994
+ quantity: line.quantity,
2995
+ unitPrice: line.unitPrice,
2996
+ tax: line.tax,
2997
+ total: line.total
2998
+ })
2999
+ );
3000
+ }
3001
+ await cartItemRepo().delete({ cartId: cart.id });
3002
+ await cartRepo().delete(cart.id);
3003
+ return json({
3004
+ orderId: oid,
3005
+ orderNumber: ord.orderNumber,
3006
+ total
3007
+ });
3008
+ }
3009
+ if (path[0] === "orders" && path.length === 1 && method === "GET") {
3010
+ const u = await getSessionUser();
3011
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
3012
+ if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
3013
+ const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
3014
+ if (!contact) return json({ orders: [] });
3015
+ const orders = await orderRepo().find({
3016
+ where: { contactId: contact.id, deleted: false },
3017
+ order: { createdAt: "DESC" },
3018
+ take: 50
3019
+ });
3020
+ const orderIds = orders.map((o) => o.id);
3021
+ const previewByOrder = {};
3022
+ if (orderIds.length) {
3023
+ const oItems = await orderItemRepo().find({
3024
+ where: { orderId: (0, import_typeorm4.In)(orderIds) },
3025
+ relations: ["product"],
3026
+ order: { id: "ASC" }
3027
+ });
3028
+ for (const oi of oItems) {
3029
+ const oid = oi.orderId;
3030
+ if (!previewByOrder[oid]) previewByOrder[oid] = [];
3031
+ if (previewByOrder[oid].length >= 4) continue;
3032
+ const url = primaryProductImageUrl(oi.product?.metadata);
3033
+ if (url && !previewByOrder[oid].includes(url)) previewByOrder[oid].push(url);
3034
+ }
3035
+ }
3036
+ return json({
3037
+ orders: orders.map((o) => {
3038
+ const ol = o;
3039
+ return {
3040
+ id: ol.id,
3041
+ orderNumber: ol.orderNumber,
3042
+ status: ol.status,
3043
+ total: ol.total,
3044
+ currency: ol.currency,
3045
+ createdAt: ol.createdAt,
3046
+ previewImages: previewByOrder[ol.id] ?? []
3047
+ };
3048
+ })
3049
+ });
3050
+ }
3051
+ if (path[0] === "orders" && path.length === 2 && method === "GET") {
3052
+ const u = await getSessionUser();
3053
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
3054
+ if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
3055
+ const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
3056
+ if (!contact) return json({ error: "Not found" }, { status: 404 });
3057
+ const orderId = parseInt(path[1], 10);
3058
+ const order = await orderRepo().findOne({
3059
+ where: { id: orderId, contactId: contact.id, deleted: false },
3060
+ relations: ["items", "items.product"]
3061
+ });
3062
+ if (!order) return json({ error: "Not found" }, { status: 404 });
3063
+ const o = order;
3064
+ const lines = (o.items || []).map((line) => {
3065
+ const p = line.product;
3066
+ return {
3067
+ id: line.id,
3068
+ productId: line.productId,
3069
+ quantity: line.quantity,
3070
+ unitPrice: line.unitPrice,
3071
+ tax: line.tax,
3072
+ total: line.total,
3073
+ product: p ? {
3074
+ name: p.name,
3075
+ slug: p.slug,
3076
+ sku: p.sku,
3077
+ image: primaryProductImageUrl(p.metadata)
3078
+ } : null
3079
+ };
3080
+ });
3081
+ return json({
3082
+ order: {
3083
+ id: o.id,
3084
+ orderNumber: o.orderNumber,
3085
+ status: o.status,
3086
+ subtotal: o.subtotal,
3087
+ tax: o.tax,
3088
+ discount: o.discount,
3089
+ total: o.total,
3090
+ currency: o.currency,
3091
+ createdAt: o.createdAt,
3092
+ items: lines
3093
+ }
3094
+ });
3095
+ }
3096
+ return json({ error: "Not found" }, { status: 404 });
3097
+ } catch {
3098
+ return json({ error: "Server error" }, { status: 500 });
3099
+ }
3100
+ }
3101
+ };
3102
+ }
1560
3103
  // Annotate the CommonJS export names for ESM import in node:
1561
3104
  0 && (module.exports = {
1562
3105
  createAnalyticsHandlers,
@@ -1571,6 +3114,7 @@ function createCmsApiHandler(config) {
1571
3114
  createInviteAcceptHandler,
1572
3115
  createSetPasswordHandler,
1573
3116
  createSettingsApiHandlers,
3117
+ createStorefrontApiHandler,
1574
3118
  createUploadHandler,
1575
3119
  createUserAuthApiRouter,
1576
3120
  createUserAvatarHandler,