@infuro/cms-core 1.0.9 → 1.0.11

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 +2568 -1184
  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 +2594 -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.js CHANGED
@@ -8,105 +8,91 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
- // src/plugins/email/email-service.ts
12
- var email_service_exports = {};
13
- __export(email_service_exports, {
14
- EmailService: () => EmailService,
15
- emailTemplates: () => emailTemplates
11
+ // src/plugins/email/email-queue.ts
12
+ var email_queue_exports = {};
13
+ __export(email_queue_exports, {
14
+ queueEmail: () => queueEmail,
15
+ queueOrderPlacedEmails: () => queueOrderPlacedEmails,
16
+ registerEmailQueueProcessor: () => registerEmailQueueProcessor
16
17
  });
17
- import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";
18
- import nodemailer from "nodemailer";
19
- var EmailService, emailTemplates;
20
- var init_email_service = __esm({
21
- "src/plugins/email/email-service.ts"() {
22
- "use strict";
23
- EmailService = class {
24
- config;
25
- sesClient;
26
- transporter;
27
- constructor(config) {
28
- this.config = config;
29
- if (config.type === "AWS") {
30
- if (!config.region || !config.accessKeyId || !config.secretAccessKey) {
31
- throw new Error("AWS SES configuration incomplete");
32
- }
33
- this.sesClient = new SESClient({
34
- region: config.region,
35
- credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey }
36
- });
37
- } else if (config.type === "SMTP" || config.type === "GMAIL") {
38
- if (!config.user || !config.password) throw new Error("SMTP configuration incomplete");
39
- this.transporter = nodemailer.createTransport({
40
- host: config.type === "GMAIL" ? "smtp.gmail.com" : void 0,
41
- port: 587,
42
- secure: false,
43
- auth: { user: config.user, pass: config.password }
44
- });
45
- } else {
46
- throw new Error(`Unsupported email type: ${config.type}`);
47
- }
48
- }
49
- async send(emailData) {
50
- try {
51
- if (this.config.type === "AWS" && this.sesClient) {
52
- await this.sesClient.send(
53
- new SendEmailCommand({
54
- Source: emailData.from || this.config.from,
55
- Destination: { ToAddresses: [emailData.to || this.config.to] },
56
- Message: {
57
- Subject: { Data: emailData.subject, Charset: "UTF-8" },
58
- Body: {
59
- Html: { Data: emailData.html, Charset: "UTF-8" },
60
- ...emailData.text && { Text: { Data: emailData.text, Charset: "UTF-8" } }
61
- }
62
- }
63
- })
64
- );
65
- return true;
66
- }
67
- if ((this.config.type === "SMTP" || this.config.type === "GMAIL") && this.transporter) {
68
- await this.transporter.sendMail({
69
- from: emailData.from || this.config.from,
70
- to: emailData.to || this.config.to,
71
- subject: emailData.subject,
72
- html: emailData.html,
73
- text: emailData.text
74
- });
75
- return true;
76
- }
77
- return false;
78
- } catch (error) {
79
- console.error("Email sending failed:", error);
80
- return false;
18
+ function registerEmailQueueProcessor(cms) {
19
+ const queue = cms.getPlugin("queue");
20
+ const email = cms.getPlugin("email");
21
+ if (!queue || !email) return;
22
+ queue.registerProcessor(EMAIL_QUEUE_NAME, async (data) => {
23
+ const payload = data;
24
+ const { to, templateName, ctx, subject, html, text } = payload;
25
+ if (!to) return;
26
+ if (templateName && ctx) {
27
+ const rendered = email.renderTemplate(templateName, ctx);
28
+ await email.send({ to, subject: rendered.subject, html: rendered.html, text: rendered.text });
29
+ } else if (subject != null && html != null) {
30
+ await email.send({ to, subject, html, text });
31
+ }
32
+ });
33
+ }
34
+ async function queueEmail(cms, payload) {
35
+ const queue = cms.getPlugin("queue");
36
+ if (queue) {
37
+ await queue.add(EMAIL_QUEUE_NAME, payload);
38
+ return;
39
+ }
40
+ const email = cms.getPlugin("email");
41
+ if (email && payload.templateName && payload.ctx) {
42
+ const rendered = email.renderTemplate(payload.templateName, payload.ctx);
43
+ await email.send({ to: payload.to, subject: rendered.subject, html: rendered.html, text: rendered.text });
44
+ } else if (email && payload.subject != null && payload.html != null) {
45
+ await email.send({ to: payload.to, subject: payload.subject, html: payload.html, text: payload.text });
46
+ }
47
+ }
48
+ async function queueOrderPlacedEmails(cms, payload) {
49
+ const { orderNumber: orderNumber2, total, currency, customerName, customerEmail, salesTeamEmails, companyDetails, lineItems } = payload;
50
+ const base = {
51
+ orderNumber: orderNumber2,
52
+ total: total != null ? String(total) : void 0,
53
+ currency,
54
+ customerName,
55
+ companyDetails: companyDetails ?? {},
56
+ lineItems: lineItems ?? []
57
+ };
58
+ const customerLower = customerEmail?.trim().toLowerCase() ?? "";
59
+ const jobs = [];
60
+ if (customerEmail?.trim()) {
61
+ jobs.push(
62
+ queueEmail(cms, {
63
+ to: customerEmail.trim(),
64
+ templateName: "orderPlaced",
65
+ ctx: { ...base, audience: "customer" }
66
+ })
67
+ );
68
+ }
69
+ const seen = /* @__PURE__ */ new Set();
70
+ for (const raw of salesTeamEmails) {
71
+ const to = raw.trim();
72
+ if (!to) continue;
73
+ const key = to.toLowerCase();
74
+ if (seen.has(key)) continue;
75
+ seen.add(key);
76
+ if (customerLower && key === customerLower) continue;
77
+ jobs.push(
78
+ queueEmail(cms, {
79
+ to,
80
+ templateName: "orderPlaced",
81
+ ctx: {
82
+ ...base,
83
+ audience: "sales",
84
+ internalCustomerEmail: customerEmail?.trim() || void 0
81
85
  }
82
- }
83
- };
84
- emailTemplates = {
85
- formSubmission: (data) => ({
86
- subject: `New Form Submission: ${data.formName}`,
87
- 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>`,
88
- text: `New Form Submission
89
- Form: ${data.formName}
90
- Contact: ${data.contactName} (${data.contactEmail})
91
- ${JSON.stringify(data.formData, null, 2)}`
92
- }),
93
- contactSubmission: (data) => ({
94
- subject: `New Contact Form Submission from ${data.name}`,
95
- 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>` : ""}`,
96
- text: `New Contact Form Submission
97
- Name: ${data.name}
98
- Email: ${data.email}
99
- ${data.phone ? `Phone: ${data.phone}
100
- ` : ""}${data.message ? `Message: ${data.message}` : ""}`
101
- }),
102
- passwordReset: (data) => ({
103
- subject: "Reset your password",
104
- 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>`,
105
- text: `Reset your password: ${data.resetLink}
106
-
107
- This link expires in 1 hour.`
108
86
  })
109
- };
87
+ );
88
+ }
89
+ await Promise.all(jobs);
90
+ }
91
+ var EMAIL_QUEUE_NAME;
92
+ var init_email_queue = __esm({
93
+ "src/plugins/email/email-queue.ts"() {
94
+ "use strict";
95
+ EMAIL_QUEUE_NAME = "email";
110
96
  }
111
97
  });
112
98
 
@@ -146,11 +132,38 @@ function sanitizeBodyForEntity(repo, body) {
146
132
  }
147
133
  }
148
134
  }
135
+ function pickColumnUpdates(repo, body) {
136
+ const cols = new Set(repo.metadata.columns.map((c) => c.propertyName));
137
+ const out = {};
138
+ for (const k of Object.keys(body)) {
139
+ if (cols.has(k)) out[k] = body[k];
140
+ }
141
+ return out;
142
+ }
143
+ function buildSearchWhereClause(repo, search) {
144
+ const cols = new Set(repo.metadata.columns.map((c) => c.propertyName));
145
+ const term = ILike(`%${search}%`);
146
+ const ors = [];
147
+ for (const field of ["name", "title", "slug", "email", "filename"]) {
148
+ if (cols.has(field)) ors.push({ [field]: term });
149
+ }
150
+ if (ors.length === 0) return {};
151
+ return ors.length === 1 ? ors[0] : ors;
152
+ }
149
153
  function createCrudHandler(dataSource, entityMap, options) {
150
- const { requireAuth, json } = options;
154
+ const { requireAuth, json, requireEntityPermission: reqPerm } = options;
155
+ async function authz(req, resource, action) {
156
+ const authError = await requireAuth(req);
157
+ if (authError) return authError;
158
+ if (reqPerm) {
159
+ const pe = await reqPerm(req, resource, action);
160
+ if (pe) return pe;
161
+ }
162
+ return null;
163
+ }
151
164
  return {
152
165
  async GET(req, resource) {
153
- const authError = await requireAuth(req);
166
+ const authError = await authz(req, resource, "read");
154
167
  if (authError) return authError;
155
168
  const entity = entityMap[resource];
156
169
  if (!resource || !entity) {
@@ -166,7 +179,7 @@ function createCrudHandler(dataSource, entityMap, options) {
166
179
  if (resource === "orders") {
167
180
  const repo2 = dataSource.getRepository(entity);
168
181
  const allowedSort = ["id", "orderNumber", "contactId", "status", "total", "currency", "createdAt", "updatedAt"];
169
- const sortField = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
182
+ const sortField2 = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
170
183
  const sortOrderOrders = searchParams.get("sortOrder") === "asc" ? "ASC" : "DESC";
171
184
  const statusFilter = searchParams.get("status")?.trim();
172
185
  const dateFrom = searchParams.get("dateFrom")?.trim();
@@ -181,7 +194,7 @@ function createCrudHandler(dataSource, entityMap, options) {
181
194
  return json({ total: 0, page, limit, totalPages: 0, data: [] });
182
195
  }
183
196
  }
184
- 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);
197
+ 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);
185
198
  if (search && typeof search === "string" && search.trim()) {
186
199
  const term = `%${search.trim()}%`;
187
200
  qb.andWhere(
@@ -212,14 +225,14 @@ function createCrudHandler(dataSource, entityMap, options) {
212
225
  if (resource === "payments") {
213
226
  const repo2 = dataSource.getRepository(entity);
214
227
  const allowedSort = ["id", "orderId", "amount", "currency", "status", "method", "paidAt", "createdAt", "updatedAt"];
215
- const sortField = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
228
+ const sortField2 = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
216
229
  const sortOrderPayments = searchParams.get("sortOrder") === "asc" ? "ASC" : "DESC";
217
230
  const statusFilter = searchParams.get("status")?.trim();
218
231
  const dateFrom = searchParams.get("dateFrom")?.trim();
219
232
  const dateTo = searchParams.get("dateTo")?.trim();
220
233
  const methodFilter = searchParams.get("method")?.trim();
221
234
  const orderNumberParam = searchParams.get("orderNumber")?.trim();
222
- 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);
235
+ 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);
223
236
  if (search && typeof search === "string" && search.trim()) {
224
237
  const term = `%${search.trim()}%`;
225
238
  qb.andWhere(
@@ -268,12 +281,12 @@ function createCrudHandler(dataSource, entityMap, options) {
268
281
  if (resource === "contacts") {
269
282
  const repo2 = dataSource.getRepository(entity);
270
283
  const allowedSort = ["id", "name", "email", "createdAt", "type"];
271
- const sortField = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
284
+ const sortField2 = allowedSort.includes(sortFieldRaw) ? sortFieldRaw : "createdAt";
272
285
  const sortOrderContacts = searchParams.get("sortOrder") === "asc" ? "ASC" : "DESC";
273
286
  const typeFilter2 = searchParams.get("type")?.trim();
274
287
  const orderIdParam = searchParams.get("orderId")?.trim();
275
288
  const includeSummary = searchParams.get("includeSummary") === "1";
276
- const qb = repo2.createQueryBuilder("contact").orderBy(`contact.${sortField}`, sortOrderContacts).skip(skip).take(limit);
289
+ const qb = repo2.createQueryBuilder("contact").orderBy(`contact.${sortField2}`, sortOrderContacts).skip(skip).take(limit);
277
290
  if (search && typeof search === "string" && search.trim()) {
278
291
  const term = `%${search.trim()}%`;
279
292
  qb.andWhere("(contact.name ILIKE :term OR contact.email ILIKE :term OR contact.phone ILIKE :term)", { term });
@@ -307,6 +320,8 @@ function createCrudHandler(dataSource, entityMap, options) {
307
320
  }
308
321
  const repo = dataSource.getRepository(entity);
309
322
  const typeFilter = searchParams.get("type");
323
+ const columnNames = new Set(repo.metadata.columns.map((c) => c.propertyName));
324
+ const sortField = columnNames.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
310
325
  let where = {};
311
326
  if (resource === "media") {
312
327
  const mediaWhere = {};
@@ -314,18 +329,18 @@ function createCrudHandler(dataSource, entityMap, options) {
314
329
  if (typeFilter) mediaWhere.mimeType = Like(`${typeFilter}/%`);
315
330
  where = Object.keys(mediaWhere).length > 0 ? mediaWhere : {};
316
331
  } else if (search) {
317
- where = [{ name: ILike(`%${search}%`) }, { title: ILike(`%${search}%`) }];
332
+ where = buildSearchWhereClause(repo, search);
318
333
  }
319
334
  const [data, total] = await repo.findAndCount({
320
335
  skip,
321
336
  take: limit,
322
- order: { [sortFieldRaw]: sortOrder },
337
+ order: { [sortField]: sortOrder },
323
338
  where
324
339
  });
325
340
  return json({ total, page, limit, totalPages: Math.ceil(total / limit), data });
326
341
  },
327
342
  async POST(req, resource) {
328
- const authError = await requireAuth(req);
343
+ const authError = await authz(req, resource, "create");
329
344
  if (authError) return authError;
330
345
  const entity = entityMap[resource];
331
346
  if (!resource || !entity) {
@@ -341,7 +356,7 @@ function createCrudHandler(dataSource, entityMap, options) {
341
356
  return json(created, { status: 201 });
342
357
  },
343
358
  async GET_METADATA(req, resource) {
344
- const authError = await requireAuth(req);
359
+ const authError = await authz(req, resource, "read");
345
360
  if (authError) return authError;
346
361
  const entity = entityMap[resource];
347
362
  if (!resource || !entity) {
@@ -372,7 +387,7 @@ function createCrudHandler(dataSource, entityMap, options) {
372
387
  return json({ columns, uniqueColumns });
373
388
  },
374
389
  async BULK_POST(req, resource) {
375
- const authError = await requireAuth(req);
390
+ const authError = await authz(req, resource, "update");
376
391
  if (authError) return authError;
377
392
  const entity = entityMap[resource];
378
393
  if (!resource || !entity) {
@@ -403,7 +418,7 @@ function createCrudHandler(dataSource, entityMap, options) {
403
418
  }
404
419
  },
405
420
  async GET_EXPORT(req, resource) {
406
- const authError = await requireAuth(req);
421
+ const authError = await authz(req, resource, "read");
407
422
  if (authError) return authError;
408
423
  const entity = entityMap[resource];
409
424
  if (!resource || !entity) {
@@ -444,10 +459,19 @@ function createCrudHandler(dataSource, entityMap, options) {
444
459
  };
445
460
  }
446
461
  function createCrudByIdHandler(dataSource, entityMap, options) {
447
- const { requireAuth, json } = options;
462
+ const { requireAuth, json, requireEntityPermission: reqPerm } = options;
463
+ async function authz(req, resource, action) {
464
+ const authError = await requireAuth(req);
465
+ if (authError) return authError;
466
+ if (reqPerm) {
467
+ const pe = await reqPerm(req, resource, action);
468
+ if (pe) return pe;
469
+ }
470
+ return null;
471
+ }
448
472
  return {
449
473
  async GET(req, resource, id) {
450
- const authError = await requireAuth(req);
474
+ const authError = await authz(req, resource, "read");
451
475
  if (authError) return authError;
452
476
  const entity = entityMap[resource];
453
477
  if (!entity) return json({ error: "Invalid resource" }, { status: 400 });
@@ -490,23 +514,111 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
490
514
  if (!payment) return json({ message: "Not found" }, { status: 404 });
491
515
  return json(payment);
492
516
  }
517
+ if (resource === "blogs") {
518
+ const blog = await repo.findOne({
519
+ where: { id: Number(id) },
520
+ relations: ["category", "seo", "tags"]
521
+ });
522
+ return blog ? json(blog) : json({ message: "Not found" }, { status: 404 });
523
+ }
493
524
  const item = await repo.findOne({ where: { id: Number(id) } });
494
525
  return item ? json(item) : json({ message: "Not found" }, { status: 404 });
495
526
  },
496
527
  async PUT(req, resource, id) {
497
- const authError = await requireAuth(req);
528
+ const authError = await authz(req, resource, "update");
498
529
  if (authError) return authError;
499
530
  const entity = entityMap[resource];
500
531
  if (!entity) return json({ error: "Invalid resource" }, { status: 400 });
501
- const body = await req.json();
532
+ const rawBody = await req.json();
502
533
  const repo = dataSource.getRepository(entity);
503
- if (body && typeof body === "object") sanitizeBodyForEntity(repo, body);
504
- await repo.update(Number(id), body);
505
- const updated = await repo.findOne({ where: { id: Number(id) } });
534
+ const numericId = Number(id);
535
+ if (resource === "blogs" && rawBody && typeof rawBody === "object" && entityMap.categories && entityMap.seos && entityMap.tags) {
536
+ const existing = await repo.findOne({ where: { id: numericId } });
537
+ if (!existing) return json({ message: "Not found" }, { status: 404 });
538
+ const updatePayload2 = pickColumnUpdates(repo, rawBody);
539
+ if ("category" in rawBody) {
540
+ const c = rawBody.category;
541
+ if (typeof c === "string" && c.trim()) {
542
+ const cat = await dataSource.getRepository(entityMap.categories).findOne({ where: { name: c.trim() } });
543
+ updatePayload2.categoryId = cat?.id ?? null;
544
+ } else {
545
+ updatePayload2.categoryId = null;
546
+ }
547
+ }
548
+ const blogSlug = typeof updatePayload2.slug === "string" && updatePayload2.slug || existing.slug;
549
+ const seoRepo = dataSource.getRepository(entityMap.seos);
550
+ const seoField = (k) => {
551
+ if (!(k in rawBody)) return void 0;
552
+ const v = rawBody[k];
553
+ if (v == null || v === "") return null;
554
+ return String(v);
555
+ };
556
+ if ("metaTitle" in rawBody || "metaDescription" in rawBody || "metaKeywords" in rawBody || "ogImage" in rawBody) {
557
+ const title = seoField("metaTitle");
558
+ const description = seoField("metaDescription");
559
+ const keywords = seoField("metaKeywords");
560
+ const ogImage = seoField("ogImage");
561
+ const exSeoId = existing.seoId;
562
+ if (exSeoId) {
563
+ const seo = await seoRepo.findOne({ where: { id: exSeoId } });
564
+ if (seo) {
565
+ const s = seo;
566
+ if (title !== void 0) s.title = title;
567
+ if (description !== void 0) s.description = description;
568
+ if (keywords !== void 0) s.keywords = keywords;
569
+ if (ogImage !== void 0) s.ogImage = ogImage;
570
+ s.slug = blogSlug;
571
+ await seoRepo.save(seo);
572
+ }
573
+ } else {
574
+ let seoSlug = blogSlug;
575
+ const taken = await seoRepo.findOne({ where: { slug: seoSlug } });
576
+ if (taken) seoSlug = `blog-${numericId}-${blogSlug}`;
577
+ const seo = await seoRepo.save(
578
+ seoRepo.create({
579
+ slug: seoSlug,
580
+ title: title ?? null,
581
+ description: description ?? null,
582
+ keywords: keywords ?? null,
583
+ ogImage: ogImage ?? null
584
+ })
585
+ );
586
+ updatePayload2.seoId = seo.id;
587
+ }
588
+ }
589
+ sanitizeBodyForEntity(repo, updatePayload2);
590
+ await repo.update(numericId, updatePayload2);
591
+ if (Array.isArray(rawBody.tags)) {
592
+ const tagNames = rawBody.tags.map((t) => String(t).trim()).filter(Boolean);
593
+ const tagRepo = dataSource.getRepository(entityMap.tags);
594
+ const tagEntities = [];
595
+ for (const name of tagNames) {
596
+ let tag = await tagRepo.findOne({ where: { name } });
597
+ if (!tag) tag = await tagRepo.save(tagRepo.create({ name }));
598
+ tagEntities.push(tag);
599
+ }
600
+ const blog = await repo.findOne({ where: { id: numericId }, relations: ["tags"] });
601
+ if (blog) {
602
+ blog.tags = tagEntities;
603
+ await repo.save(blog);
604
+ }
605
+ }
606
+ const updated2 = await repo.findOne({
607
+ where: { id: numericId },
608
+ relations: ["tags", "category", "seo"]
609
+ });
610
+ return updated2 ? json(updated2) : json({ message: "Not found" }, { status: 404 });
611
+ }
612
+ const updatePayload = rawBody && typeof rawBody === "object" ? pickColumnUpdates(repo, rawBody) : {};
613
+ if (Object.keys(updatePayload).length > 0) {
614
+ sanitizeBodyForEntity(repo, updatePayload);
615
+ await repo.update(numericId, updatePayload);
616
+ }
617
+ const updated = await repo.findOne({ where: { id: numericId } });
506
618
  return updated ? json(updated) : json({ message: "Not found" }, { status: 404 });
507
619
  },
508
620
  async DELETE(req, resource, id) {
509
- const authError = await requireAuth(req);
621
+ const authError = await authz(req, resource, "delete");
510
622
  if (authError) return authError;
511
623
  const entity = entityMap[resource];
512
624
  if (!entity) return json({ error: "Invalid resource" }, { status: 400 });
@@ -518,6 +630,16 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
518
630
  };
519
631
  }
520
632
 
633
+ // src/lib/link-contact-to-user.ts
634
+ import { IsNull } from "typeorm";
635
+ async function linkUnclaimedContactToUser(dataSource, contactsEntity, userId, email) {
636
+ const repo = dataSource.getRepository(contactsEntity);
637
+ const found = await repo.findOne({
638
+ where: { email, userId: IsNull(), deleted: false }
639
+ });
640
+ if (found) await repo.update(found.id, { userId });
641
+ }
642
+
521
643
  // src/api/auth-handlers.ts
522
644
  function createForgotPasswordHandler(config) {
523
645
  const { dataSource, entityMap, json, baseUrl, sendEmail, resetExpiryHours = 1, afterCreateToken } = config;
@@ -530,13 +652,20 @@ function createForgotPasswordHandler(config) {
530
652
  const user = await userRepo.findOne({ where: { email }, select: ["email"] });
531
653
  const msg = "If an account exists with this email, you will receive a reset link shortly.";
532
654
  if (!user) return json({ message: msg }, { status: 200 });
533
- const crypto = await import("crypto");
534
- const token = crypto.randomBytes(32).toString("hex");
655
+ const crypto2 = await import("crypto");
656
+ const token = crypto2.randomBytes(32).toString("hex");
535
657
  const expiresAt = new Date(Date.now() + resetExpiryHours * 60 * 60 * 1e3);
536
658
  const tokenRepo = dataSource.getRepository(entityMap.password_reset_tokens);
537
659
  await tokenRepo.save(tokenRepo.create({ email: user.email, token, expiresAt }));
538
660
  const resetLink = `${baseUrl}/admin/reset-password?token=${token}`;
539
- if (sendEmail) await sendEmail({ to: user.email, subject: "Password reset", html: `<a href="${resetLink}">Reset password</a>`, text: resetLink });
661
+ if (sendEmail)
662
+ await sendEmail({
663
+ to: user.email,
664
+ subject: "Password reset",
665
+ html: `<a href="${resetLink}">Reset password</a>`,
666
+ text: resetLink,
667
+ resetLink
668
+ });
540
669
  if (afterCreateToken) await afterCreateToken(user.email, resetLink);
541
670
  return json({ message: msg }, { status: 200 });
542
671
  } catch (err) {
@@ -585,6 +714,9 @@ function createInviteAcceptHandler(config) {
585
714
  const user = await userRepo.findOne({ where: { email }, select: ["id", "blocked"] });
586
715
  if (!user) return json({ error: "User not found" }, { status: 400 });
587
716
  if (!user.blocked) return json({ error: "User is already active" }, { status: 400 });
717
+ if (entityMap.contacts) {
718
+ await linkUnclaimedContactToUser(dataSource, entityMap.contacts, user.id, email);
719
+ }
588
720
  if (beforeActivate) await beforeActivate(email, user.id);
589
721
  const hashedPassword = await hashPassword(password);
590
722
  await userRepo.update(user.id, { password: hashedPassword, blocked: false });
@@ -645,12 +777,17 @@ function createUserAuthApiRouter(config) {
645
777
  }
646
778
 
647
779
  // src/api/cms-handlers.ts
780
+ init_email_queue();
648
781
  import { MoreThanOrEqual, ILike as ILike2 } from "typeorm";
649
782
  function createDashboardStatsHandler(config) {
650
- const { dataSource, entityMap, json, requireAuth, requirePermission } = config;
783
+ const { dataSource, entityMap, json, requireAuth, requirePermission, requireEntityPermission } = config;
651
784
  return async function GET(req) {
652
785
  const authErr = await requireAuth(req);
653
786
  if (authErr) return authErr;
787
+ if (requireEntityPermission) {
788
+ const pe = await requireEntityPermission(req, "dashboard", "read");
789
+ if (pe) return pe;
790
+ }
654
791
  if (requirePermission) {
655
792
  const permErr = await requirePermission(req, "view_dashboard");
656
793
  if (permErr) return permErr;
@@ -709,11 +846,15 @@ function createAnalyticsHandlers(config) {
709
846
  };
710
847
  }
711
848
  function createUploadHandler(config) {
712
- const { json, requireAuth, storage, localUploadDir = "public/uploads", allowedTypes, maxSizeBytes = 10 * 1024 * 1024 } = config;
849
+ const { json, requireAuth, requireEntityPermission, storage, localUploadDir = "public/uploads", allowedTypes, maxSizeBytes = 10 * 1024 * 1024 } = config;
713
850
  const allowed = allowedTypes ?? ["image/jpeg", "image/png", "image/gif", "image/webp", "application/pdf", "text/plain"];
714
851
  return async function POST(req) {
715
852
  const authErr = await requireAuth(req);
716
853
  if (authErr) return authErr;
854
+ if (requireEntityPermission) {
855
+ const pe = await requireEntityPermission(req, "upload", "create");
856
+ if (pe) return pe;
857
+ }
717
858
  try {
718
859
  const formData = await req.formData();
719
860
  const file = formData.get("file");
@@ -794,13 +935,17 @@ function normalizeFieldRow(f, formId) {
794
935
  };
795
936
  }
796
937
  function createFormSaveHandlers(config) {
797
- const { dataSource, entityMap, json, requireAuth } = config;
938
+ const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
798
939
  const formRepo = () => dataSource.getRepository(entityMap.forms);
799
940
  const fieldRepo = () => dataSource.getRepository(entityMap.form_fields);
800
941
  return {
801
942
  async GET(req, id) {
802
943
  const authErr = await requireAuth(req);
803
944
  if (authErr) return authErr;
945
+ if (requireEntityPermission) {
946
+ const pe = await requireEntityPermission(req, "forms", "read");
947
+ if (pe) return pe;
948
+ }
804
949
  try {
805
950
  const formId = Number(id);
806
951
  if (!Number.isInteger(formId) || formId <= 0) return json({ error: "Invalid form id" }, { status: 400 });
@@ -820,6 +965,10 @@ function createFormSaveHandlers(config) {
820
965
  async POST(req) {
821
966
  const authErr = await requireAuth(req);
822
967
  if (authErr) return authErr;
968
+ if (requireEntityPermission) {
969
+ const pe = await requireEntityPermission(req, "forms", "create");
970
+ if (pe) return pe;
971
+ }
823
972
  try {
824
973
  const body = await req.json();
825
974
  if (!body || typeof body !== "object") return json({ error: "Invalid request payload" }, { status: 400 });
@@ -840,6 +989,10 @@ function createFormSaveHandlers(config) {
840
989
  async PUT(req, id) {
841
990
  const authErr = await requireAuth(req);
842
991
  if (authErr) return authErr;
992
+ if (requireEntityPermission) {
993
+ const pe = await requireEntityPermission(req, "forms", "update");
994
+ if (pe) return pe;
995
+ }
843
996
  try {
844
997
  const formId = Number(id);
845
998
  if (!Number.isInteger(formId) || formId <= 0) return json({ error: "Invalid form id" }, { status: 400 });
@@ -868,10 +1021,14 @@ function createFormSaveHandlers(config) {
868
1021
  };
869
1022
  }
870
1023
  function createFormSubmissionGetByIdHandler(config) {
871
- const { dataSource, entityMap, json, requireAuth } = config;
1024
+ const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
872
1025
  return async function GET(req, id) {
873
1026
  const authErr = await requireAuth(req);
874
1027
  if (authErr) return authErr;
1028
+ if (requireEntityPermission) {
1029
+ const pe = await requireEntityPermission(req, "form_submissions", "read");
1030
+ if (pe) return pe;
1031
+ }
875
1032
  try {
876
1033
  const submissionId = Number(id);
877
1034
  if (!Number.isInteger(submissionId) || submissionId <= 0) return json({ error: "Invalid id" }, { status: 400 });
@@ -898,10 +1055,14 @@ function createFormSubmissionGetByIdHandler(config) {
898
1055
  };
899
1056
  }
900
1057
  function createFormSubmissionListHandler(config) {
901
- const { dataSource, entityMap, json, requireAuth } = config;
1058
+ const { dataSource, entityMap, json, requireAuth, requireEntityPermission } = config;
902
1059
  return async function GET(req) {
903
1060
  const authErr = await requireAuth(req);
904
1061
  if (authErr) return authErr;
1062
+ if (requireEntityPermission) {
1063
+ const pe = await requireEntityPermission(req, "form_submissions", "read");
1064
+ if (pe) return pe;
1065
+ }
905
1066
  try {
906
1067
  const repo = dataSource.getRepository(entityMap.form_submissions);
907
1068
  const { searchParams } = new URL(req.url);
@@ -922,6 +1083,11 @@ function createFormSubmissionListHandler(config) {
922
1083
  }
923
1084
  };
924
1085
  }
1086
+ function formatSubmissionFieldValue(raw) {
1087
+ if (raw == null || raw === "") return "\u2014";
1088
+ if (typeof raw === "object") return JSON.stringify(raw);
1089
+ return String(raw);
1090
+ }
925
1091
  function pickContactFromSubmission(fields, data) {
926
1092
  let email = null;
927
1093
  let name = null;
@@ -997,6 +1163,50 @@ function createFormSubmissionHandler(config) {
997
1163
  userAgent: userAgent?.slice(0, 500) ?? null
998
1164
  })
999
1165
  );
1166
+ const formWithName = form;
1167
+ const formName = formWithName.name ?? "Form";
1168
+ let contactName = "Unknown";
1169
+ let contactEmail = "";
1170
+ if (Number.isInteger(contactId)) {
1171
+ const contactRepo = dataSource.getRepository(entityMap.contacts);
1172
+ const contact = await contactRepo.findOne({ where: { id: contactId }, select: ["name", "email"] });
1173
+ if (contact) {
1174
+ contactName = contact.name ?? contactName;
1175
+ contactEmail = contact.email ?? contactEmail;
1176
+ }
1177
+ } else {
1178
+ const contactData = pickContactFromSubmission(activeFields, data);
1179
+ if (contactData) {
1180
+ contactName = contactData.name;
1181
+ contactEmail = contactData.email;
1182
+ }
1183
+ }
1184
+ if (config.getCms && config.getCompanyDetails && config.getRecipientForChannel) {
1185
+ try {
1186
+ const cms = await config.getCms();
1187
+ const to = await config.getRecipientForChannel("crm");
1188
+ if (to) {
1189
+ const companyDetails = await config.getCompanyDetails();
1190
+ const formFieldRows = activeFields.map((f) => ({
1191
+ label: f.label && String(f.label).trim() || `Field ${f.id}`,
1192
+ value: formatSubmissionFieldValue(data[String(f.id)])
1193
+ }));
1194
+ await queueEmail(cms, {
1195
+ to,
1196
+ templateName: "formSubmission",
1197
+ ctx: {
1198
+ formName,
1199
+ contactName,
1200
+ contactEmail,
1201
+ formData: data,
1202
+ formFieldRows,
1203
+ companyDetails: companyDetails ?? {}
1204
+ }
1205
+ });
1206
+ }
1207
+ } catch {
1208
+ }
1209
+ }
1000
1210
  return json(created, { status: 201 });
1001
1211
  } catch {
1002
1212
  return json({ error: "Server Error" }, { status: 500 });
@@ -1004,12 +1214,34 @@ function createFormSubmissionHandler(config) {
1004
1214
  };
1005
1215
  }
1006
1216
  function createUsersApiHandlers(config) {
1007
- const { dataSource, entityMap, json, requireAuth, baseUrl } = config;
1217
+ const { dataSource, entityMap, json, requireAuth, requireEntityPermission, baseUrl, getCms, getCompanyDetails } = config;
1218
+ async function trySendInviteEmail(toEmail, inviteLink, inviteeName) {
1219
+ if (!getCms) return;
1220
+ try {
1221
+ const cms = await getCms();
1222
+ const companyDetails = getCompanyDetails ? await getCompanyDetails() : {};
1223
+ await queueEmail(cms, {
1224
+ to: toEmail,
1225
+ templateName: "invite",
1226
+ ctx: {
1227
+ inviteLink,
1228
+ email: toEmail,
1229
+ inviteeName: inviteeName.trim(),
1230
+ companyDetails: companyDetails ?? {}
1231
+ }
1232
+ });
1233
+ } catch {
1234
+ }
1235
+ }
1008
1236
  const userRepo = () => dataSource.getRepository(entityMap.users);
1009
1237
  return {
1010
1238
  async list(req) {
1011
1239
  const authErr = await requireAuth(req);
1012
1240
  if (authErr) return authErr;
1241
+ if (requireEntityPermission) {
1242
+ const pe = await requireEntityPermission(req, "users", "read");
1243
+ if (pe) return pe;
1244
+ }
1013
1245
  try {
1014
1246
  const url = new URL(req.url);
1015
1247
  const page = Math.max(1, parseInt(url.searchParams.get("page") || "1", 10));
@@ -1035,16 +1267,40 @@ function createUsersApiHandlers(config) {
1035
1267
  async create(req) {
1036
1268
  const authErr = await requireAuth(req);
1037
1269
  if (authErr) return authErr;
1270
+ if (requireEntityPermission) {
1271
+ const pe = await requireEntityPermission(req, "users", "create");
1272
+ if (pe) return pe;
1273
+ }
1038
1274
  try {
1039
1275
  const body = await req.json();
1040
1276
  if (!body?.name || !body?.email) return json({ error: "Name and email are required" }, { status: 400 });
1041
1277
  const existing = await userRepo().findOne({ where: { email: body.email } });
1042
1278
  if (existing) return json({ error: "User with this email already exists" }, { status: 400 });
1279
+ const groupRepo = dataSource.getRepository(entityMap.user_groups);
1280
+ const customerG = await groupRepo.findOne({ where: { name: "Customer", deleted: false } });
1281
+ const gid = body.groupId ?? null;
1282
+ const isCustomer = !!(customerG && gid === customerG.id);
1283
+ const adminAccess = isCustomer ? false : body.adminAccess === false ? false : true;
1043
1284
  const newUser = await userRepo().save(
1044
- userRepo().create({ name: body.name, email: body.email, password: null, blocked: true, groupId: body.groupId ?? null })
1285
+ userRepo().create({
1286
+ name: body.name,
1287
+ email: body.email,
1288
+ password: null,
1289
+ blocked: true,
1290
+ groupId: gid,
1291
+ adminAccess
1292
+ })
1045
1293
  );
1294
+ if (entityMap.contacts) {
1295
+ await linkUnclaimedContactToUser(dataSource, entityMap.contacts, newUser.id, newUser.email);
1296
+ }
1046
1297
  const emailToken = Buffer.from(newUser.email).toString("base64");
1047
1298
  const inviteLink = `${baseUrl}/admin/invite?token=${emailToken}`;
1299
+ await trySendInviteEmail(
1300
+ newUser.email,
1301
+ inviteLink,
1302
+ newUser.name ?? ""
1303
+ );
1048
1304
  return json({ message: "User created successfully (blocked until password is set)", user: newUser, inviteLink }, { status: 201 });
1049
1305
  } catch {
1050
1306
  return json({ error: "Server Error" }, { status: 500 });
@@ -1053,6 +1309,10 @@ function createUsersApiHandlers(config) {
1053
1309
  async getById(_req, id) {
1054
1310
  const authErr = await requireAuth(new Request(_req.url));
1055
1311
  if (authErr) return authErr;
1312
+ if (requireEntityPermission) {
1313
+ const pe = await requireEntityPermission(_req, "users", "read");
1314
+ if (pe) return pe;
1315
+ }
1056
1316
  try {
1057
1317
  const user = await userRepo().findOne({
1058
1318
  where: { id: parseInt(id, 10) },
@@ -1068,6 +1328,10 @@ function createUsersApiHandlers(config) {
1068
1328
  async update(req, id) {
1069
1329
  const authErr = await requireAuth(req);
1070
1330
  if (authErr) return authErr;
1331
+ if (requireEntityPermission) {
1332
+ const pe = await requireEntityPermission(req, "users", "update");
1333
+ if (pe) return pe;
1334
+ }
1071
1335
  try {
1072
1336
  const body = await req.json();
1073
1337
  const { password: _p, ...safe } = body;
@@ -1085,6 +1349,10 @@ function createUsersApiHandlers(config) {
1085
1349
  async delete(_req, id) {
1086
1350
  const authErr = await requireAuth(new Request(_req.url));
1087
1351
  if (authErr) return authErr;
1352
+ if (requireEntityPermission) {
1353
+ const pe = await requireEntityPermission(_req, "users", "delete");
1354
+ if (pe) return pe;
1355
+ }
1088
1356
  try {
1089
1357
  const r = await userRepo().delete(parseInt(id, 10));
1090
1358
  if (r.affected === 0) return json({ error: "User not found" }, { status: 404 });
@@ -1096,11 +1364,16 @@ function createUsersApiHandlers(config) {
1096
1364
  async regenerateInvite(_req, id) {
1097
1365
  const authErr = await requireAuth(new Request(_req.url));
1098
1366
  if (authErr) return authErr;
1367
+ if (requireEntityPermission) {
1368
+ const pe = await requireEntityPermission(_req, "users", "update");
1369
+ if (pe) return pe;
1370
+ }
1099
1371
  try {
1100
- const user = await userRepo().findOne({ where: { id: parseInt(id, 10) }, select: ["email"] });
1372
+ const user = await userRepo().findOne({ where: { id: parseInt(id, 10) }, select: ["email", "name"] });
1101
1373
  if (!user) return json({ error: "User not found" }, { status: 404 });
1102
1374
  const emailToken = Buffer.from(user.email).toString("base64");
1103
1375
  const inviteLink = `${baseUrl}/admin/invite?token=${emailToken}`;
1376
+ await trySendInviteEmail(user.email, inviteLink, user.name ?? "");
1104
1377
  return json({ message: "New invite link generated successfully", inviteLink });
1105
1378
  } catch {
1106
1379
  return json({ error: "Server Error" }, { status: 500 });
@@ -1350,8 +1623,207 @@ ${contextParts.join("\n\n")}` : "You are a helpful assistant for the company. If
1350
1623
  };
1351
1624
  }
1352
1625
 
1626
+ // src/auth/permission-entities.ts
1627
+ var PERMISSION_ENTITY_INTERNAL_EXCLUDE = /* @__PURE__ */ new Set([
1628
+ "users",
1629
+ "password_reset_tokens",
1630
+ "user_groups",
1631
+ "permissions",
1632
+ "comments",
1633
+ "form_fields",
1634
+ "configs",
1635
+ "knowledge_base_chunks",
1636
+ "carts",
1637
+ "cart_items",
1638
+ "wishlists",
1639
+ "wishlist_items"
1640
+ ]);
1641
+ var PERMISSION_LOGICAL_ENTITIES = [
1642
+ "users",
1643
+ "forms",
1644
+ "form_submissions",
1645
+ "dashboard",
1646
+ "upload",
1647
+ "settings",
1648
+ "analytics",
1649
+ "chat"
1650
+ ];
1651
+ var ADMIN_GROUP_NAME = "Administrator";
1652
+ function isSuperAdminGroupName(name) {
1653
+ return name === ADMIN_GROUP_NAME;
1654
+ }
1655
+ function getPermissionableEntityKeys(entityMap) {
1656
+ const fromMap = Object.keys(entityMap).filter((k) => !PERMISSION_ENTITY_INTERNAL_EXCLUDE.has(k));
1657
+ const logical = PERMISSION_LOGICAL_ENTITIES.filter((k) => !fromMap.includes(k));
1658
+ return [...fromMap.sort(), ...logical].filter((k, i, a) => a.indexOf(k) === i);
1659
+ }
1660
+
1661
+ // src/auth/helpers.ts
1662
+ function canManageRoles(user) {
1663
+ return !!(user?.email && user.isRBACAdmin);
1664
+ }
1665
+
1666
+ // src/api/admin-roles-handlers.ts
1667
+ function createAdminRolesHandlers(config) {
1668
+ const { dataSource, entityMap, json, getSessionUser } = config;
1669
+ const baseEntities = getPermissionableEntityKeys(entityMap);
1670
+ const allowEntities = /* @__PURE__ */ new Set([...baseEntities, "users"]);
1671
+ const groupRepo = () => dataSource.getRepository(entityMap.user_groups);
1672
+ const permRepo = () => dataSource.getRepository(entityMap.permissions);
1673
+ const userRepo = () => dataSource.getRepository(entityMap.users);
1674
+ async function gate() {
1675
+ const u = await getSessionUser();
1676
+ if (!u?.email) return json({ error: "Unauthorized" }, { status: 401 });
1677
+ if (!canManageRoles(u)) return json({ error: "Forbidden" }, { status: 403 });
1678
+ return null;
1679
+ }
1680
+ return {
1681
+ async list() {
1682
+ const err = await gate();
1683
+ if (err) return err;
1684
+ const groups = await groupRepo().find({
1685
+ where: { deleted: false },
1686
+ order: { id: "ASC" },
1687
+ relations: ["permissions"]
1688
+ });
1689
+ const entities = [...allowEntities].sort();
1690
+ return json({
1691
+ entities,
1692
+ groups: groups.map((g) => ({
1693
+ id: g.id,
1694
+ name: g.name,
1695
+ permissions: (g.permissions ?? []).filter((p) => !p.deleted).map((p) => ({
1696
+ entity: p.entity,
1697
+ canCreate: p.canCreate,
1698
+ canRead: p.canRead,
1699
+ canUpdate: p.canUpdate,
1700
+ canDelete: p.canDelete
1701
+ }))
1702
+ }))
1703
+ });
1704
+ },
1705
+ async createGroup(req) {
1706
+ const err = await gate();
1707
+ if (err) return err;
1708
+ try {
1709
+ const body = await req.json();
1710
+ const name = body?.name?.trim();
1711
+ if (!name) return json({ error: "Name is required" }, { status: 400 });
1712
+ const repo = groupRepo();
1713
+ const existing = await repo.findOne({ where: { name } });
1714
+ if (existing) return json({ error: "Group name already exists" }, { status: 400 });
1715
+ const g = await repo.save(repo.create({ name }));
1716
+ return json({ id: g.id, name: g.name, permissions: [] }, { status: 201 });
1717
+ } catch {
1718
+ return json({ error: "Server error" }, { status: 500 });
1719
+ }
1720
+ },
1721
+ async patchGroup(req, idStr) {
1722
+ const err = await gate();
1723
+ if (err) return err;
1724
+ const id = parseInt(idStr, 10);
1725
+ if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
1726
+ try {
1727
+ const body = await req.json();
1728
+ const name = body?.name?.trim();
1729
+ if (!name) return json({ error: "Name is required" }, { status: 400 });
1730
+ const repo = groupRepo();
1731
+ const g = await repo.findOne({ where: { id, deleted: false } });
1732
+ if (!g) return json({ error: "Not found" }, { status: 404 });
1733
+ if (isSuperAdminGroupName(g.name) && !isSuperAdminGroupName(name)) {
1734
+ return json({ error: "Cannot rename the administrator group" }, { status: 400 });
1735
+ }
1736
+ const dup = await repo.findOne({ where: { name } });
1737
+ if (dup && dup.id !== id) return json({ error: "Name already in use" }, { status: 400 });
1738
+ g.name = name;
1739
+ await repo.save(g);
1740
+ return json({ id: g.id, name: g.name });
1741
+ } catch {
1742
+ return json({ error: "Server error" }, { status: 500 });
1743
+ }
1744
+ },
1745
+ async deleteGroup(idStr) {
1746
+ const err = await gate();
1747
+ if (err) return err;
1748
+ const id = parseInt(idStr, 10);
1749
+ if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
1750
+ const repo = groupRepo();
1751
+ const g = await repo.findOne({ where: { id, deleted: false } });
1752
+ if (!g) return json({ error: "Not found" }, { status: 404 });
1753
+ if (isSuperAdminGroupName(g.name)) return json({ error: "Cannot delete the administrator group" }, { status: 400 });
1754
+ const userCount = await userRepo().count({ where: { groupId: id } });
1755
+ if (userCount > 0) return json({ error: "Reassign users before deleting this group" }, { status: 409 });
1756
+ await permRepo().delete({ groupId: id });
1757
+ await repo.update(id, { deleted: true, deletedAt: /* @__PURE__ */ new Date() });
1758
+ return json({ ok: true });
1759
+ },
1760
+ async putPermissions(req, idStr) {
1761
+ const err = await gate();
1762
+ if (err) return err;
1763
+ const groupId = parseInt(idStr, 10);
1764
+ if (!Number.isFinite(groupId)) return json({ error: "Invalid id" }, { status: 400 });
1765
+ const groupRepository = groupRepo();
1766
+ const g = await groupRepository.findOne({ where: { id: groupId, deleted: false } });
1767
+ if (!g) return json({ error: "Group not found" }, { status: 404 });
1768
+ try {
1769
+ const body = await req.json();
1770
+ const rows = body?.permissions;
1771
+ if (!Array.isArray(rows)) return json({ error: "permissions array required" }, { status: 400 });
1772
+ for (const r of rows) {
1773
+ if (!r?.entity || !allowEntities.has(r.entity)) {
1774
+ return json({ error: `Invalid entity: ${r?.entity ?? ""}` }, { status: 400 });
1775
+ }
1776
+ }
1777
+ await dataSource.transaction(async (em) => {
1778
+ await em.getRepository(entityMap.permissions).delete({ groupId });
1779
+ for (const r of rows) {
1780
+ await em.getRepository(entityMap.permissions).save(
1781
+ em.getRepository(entityMap.permissions).create({
1782
+ groupId,
1783
+ entity: r.entity,
1784
+ canCreate: !!r.canCreate,
1785
+ canRead: !!r.canRead,
1786
+ canUpdate: !!r.canUpdate,
1787
+ canDelete: !!r.canDelete
1788
+ })
1789
+ );
1790
+ }
1791
+ });
1792
+ const updated = await groupRepository.findOne({
1793
+ where: { id: groupId },
1794
+ relations: ["permissions"]
1795
+ });
1796
+ return json({
1797
+ id: groupId,
1798
+ permissions: (updated?.permissions ?? []).map((p) => ({
1799
+ entity: p.entity,
1800
+ canCreate: p.canCreate,
1801
+ canRead: p.canRead,
1802
+ canUpdate: p.canUpdate,
1803
+ canDelete: p.canDelete
1804
+ }))
1805
+ });
1806
+ } catch {
1807
+ return json({ error: "Server error" }, { status: 500 });
1808
+ }
1809
+ }
1810
+ };
1811
+ }
1812
+
1353
1813
  // src/api/cms-api-handler.ts
1354
- var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set(["users", "password_reset_tokens", "user_groups", "permissions", "comments", "form_fields", "configs"]);
1814
+ var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
1815
+ "users",
1816
+ "password_reset_tokens",
1817
+ "user_groups",
1818
+ "permissions",
1819
+ "comments",
1820
+ "form_fields",
1821
+ "configs",
1822
+ "carts",
1823
+ "cart_items",
1824
+ "wishlists",
1825
+ "wishlist_items"
1826
+ ]);
1355
1827
  function createCmsApiHandler(config) {
1356
1828
  const {
1357
1829
  dataSource,
@@ -1372,7 +1844,9 @@ function createCmsApiHandler(config) {
1372
1844
  userAvatar,
1373
1845
  userProfile,
1374
1846
  settings: settingsConfig,
1375
- chat: chatConfig
1847
+ chat: chatConfig,
1848
+ requireEntityPermission: reqEntityPerm,
1849
+ getSessionUser
1376
1850
  } = config;
1377
1851
  const analytics = analyticsConfig ?? (getCms ? {
1378
1852
  json: config.json,
@@ -1393,27 +1867,51 @@ function createCmsApiHandler(config) {
1393
1867
  ...userAuthConfig,
1394
1868
  sendEmail: async (opts) => {
1395
1869
  const cms = await getCms();
1870
+ const queue = cms.getPlugin("queue");
1871
+ const companyDetails = config.getCompanyDetails ? await config.getCompanyDetails() : {};
1872
+ 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] ?? "" : "");
1873
+ const ctx = { resetLink, companyDetails };
1874
+ if (queue) {
1875
+ const { queueEmail: queueEmail2 } = await Promise.resolve().then(() => (init_email_queue(), email_queue_exports));
1876
+ await queueEmail2(cms, { to: opts.to, templateName: "passwordReset", ctx });
1877
+ return;
1878
+ }
1396
1879
  const email = cms.getPlugin("email");
1397
1880
  if (!email?.send) return;
1398
- const { emailTemplates: emailTemplates2 } = await Promise.resolve().then(() => (init_email_service(), email_service_exports));
1399
- const { subject, html, text } = emailTemplates2.passwordReset({ resetLink: opts.text || opts.html });
1400
- await email.send({ subject, html, text, to: opts.to });
1881
+ const rendered = email.renderTemplate("passwordReset", ctx);
1882
+ await email.send({ subject: rendered.subject, html: rendered.html, text: rendered.text, to: opts.to });
1401
1883
  }
1402
1884
  } : userAuthConfig;
1403
- const crudOpts = { requireAuth: config.requireAuth, json: config.json };
1885
+ const crudOpts = {
1886
+ requireAuth: config.requireAuth,
1887
+ json: config.json,
1888
+ requireEntityPermission: reqEntityPerm
1889
+ };
1404
1890
  const crud = createCrudHandler(dataSource, entityMap, crudOpts);
1405
1891
  const crudById = createCrudByIdHandler(dataSource, entityMap, crudOpts);
1892
+ const mergePerm = (c) => !c ? void 0 : reqEntityPerm ? { ...c, requireEntityPermission: reqEntityPerm } : c;
1893
+ const adminRoles = getSessionUser && createAdminRolesHandlers({
1894
+ dataSource,
1895
+ entityMap,
1896
+ json: config.json,
1897
+ getSessionUser
1898
+ });
1406
1899
  const userAuthRouter = userAuth ? createUserAuthApiRouter(userAuth) : null;
1407
- const dashboardGet = dashboard ? createDashboardStatsHandler(dashboard) : null;
1900
+ const dashboardGet = dashboard ? createDashboardStatsHandler(mergePerm(dashboard) ?? dashboard) : null;
1408
1901
  const analyticsHandlers = analytics ? createAnalyticsHandlers(analytics) : null;
1409
- const uploadPost = upload ? createUploadHandler(upload) : null;
1902
+ const uploadPost = upload ? createUploadHandler(mergePerm(upload) ?? upload) : null;
1410
1903
  const blogBySlugGet = blogBySlug ? createBlogBySlugHandler(blogBySlug) : null;
1411
1904
  const formBySlugGet = formBySlug ? createFormBySlugHandler(formBySlug) : null;
1412
- const formSaveHandlers = formSaveConfig ? createFormSaveHandlers(formSaveConfig) : null;
1905
+ const formSaveHandlers = formSaveConfig ? createFormSaveHandlers(mergePerm(formSaveConfig) ?? formSaveConfig) : null;
1413
1906
  const formSubmissionPost = formSubmissionConfig ? createFormSubmissionHandler(formSubmissionConfig) : null;
1414
- const formSubmissionGetById = formSubmissionGetByIdConfig ? createFormSubmissionGetByIdHandler(formSubmissionGetByIdConfig) : null;
1415
- const formSubmissionList = formSubmissionGetByIdConfig ? createFormSubmissionListHandler(formSubmissionGetByIdConfig) : null;
1416
- const usersHandlers = usersApi ? createUsersApiHandlers(usersApi) : null;
1907
+ const formSubmissionGetById = formSubmissionGetByIdConfig ? createFormSubmissionGetByIdHandler(mergePerm(formSubmissionGetByIdConfig) ?? formSubmissionGetByIdConfig) : null;
1908
+ const formSubmissionList = formSubmissionGetByIdConfig ? createFormSubmissionListHandler(mergePerm(formSubmissionGetByIdConfig) ?? formSubmissionGetByIdConfig) : null;
1909
+ const usersApiMerged = usersApi && getCms ? {
1910
+ ...usersApi,
1911
+ getCms: usersApi.getCms ?? getCms,
1912
+ getCompanyDetails: usersApi.getCompanyDetails ?? config.getCompanyDetails
1913
+ } : usersApi;
1914
+ const usersHandlers = usersApiMerged ? createUsersApiHandlers(mergePerm(usersApiMerged) ?? usersApiMerged) : null;
1417
1915
  const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
1418
1916
  const profilePut = userProfile ? createUserProfileHandler(userProfile) : null;
1419
1917
  const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
@@ -1424,13 +1922,41 @@ function createCmsApiHandler(config) {
1424
1922
  }
1425
1923
  return {
1426
1924
  async handle(method, path, req) {
1925
+ const perm = reqEntityPerm;
1926
+ async function analyticsGate() {
1927
+ const a = await config.requireAuth(req);
1928
+ if (a) return a;
1929
+ if (perm) return perm(req, "analytics", "read");
1930
+ return null;
1931
+ }
1932
+ if (path[0] === "admin" && path[1] === "roles") {
1933
+ if (!adminRoles) return config.json({ error: "Not found" }, { status: 404 });
1934
+ if (path.length === 2 && method === "GET") return adminRoles.list();
1935
+ if (path.length === 2 && method === "POST") return adminRoles.createGroup(req);
1936
+ if (path.length === 3 && method === "PATCH") return adminRoles.patchGroup(req, path[2]);
1937
+ if (path.length === 3 && method === "DELETE") return adminRoles.deleteGroup(path[2]);
1938
+ if (path.length === 4 && path[3] === "permissions" && method === "PUT") return adminRoles.putPermissions(req, path[2]);
1939
+ return config.json({ error: "Not found" }, { status: 404 });
1940
+ }
1427
1941
  if (path[0] === "dashboard" && path[1] === "stats" && path.length === 2 && method === "GET" && dashboardGet) {
1428
1942
  return dashboardGet(req);
1429
1943
  }
1430
1944
  if (path[0] === "analytics" && analyticsHandlers) {
1431
- if (path.length === 1 && method === "GET") return analyticsHandlers.GET(req);
1432
- if (path.length === 2 && path[1] === "property-id" && method === "GET") return analyticsHandlers.propertyId();
1433
- if (path.length === 2 && path[1] === "permissions" && method === "GET") return analyticsHandlers.permissions();
1945
+ if (path.length === 1 && method === "GET") {
1946
+ const g = await analyticsGate();
1947
+ if (g) return g;
1948
+ return analyticsHandlers.GET(req);
1949
+ }
1950
+ if (path.length === 2 && path[1] === "property-id" && method === "GET") {
1951
+ const g = await analyticsGate();
1952
+ if (g) return g;
1953
+ return analyticsHandlers.propertyId();
1954
+ }
1955
+ if (path.length === 2 && path[1] === "permissions" && method === "GET") {
1956
+ const g = await analyticsGate();
1957
+ if (g) return g;
1958
+ return analyticsHandlers.permissions();
1959
+ }
1434
1960
  }
1435
1961
  if (path[0] === "upload" && path.length === 1 && method === "POST" && uploadPost) return uploadPost(req);
1436
1962
  if (path[0] === "blogs" && path[1] === "slug" && path.length === 3 && method === "GET" && blogBySlugGet) {
@@ -1474,8 +2000,24 @@ function createCmsApiHandler(config) {
1474
2000
  return userAuthRouter.POST(req, path[1]);
1475
2001
  }
1476
2002
  if (path[0] === "settings" && path.length === 2 && settingsHandlers) {
1477
- if (method === "GET") return settingsHandlers.GET(req, path[1]);
1478
- if (method === "PUT") return settingsHandlers.PUT(req, path[1]);
2003
+ const group = path[1];
2004
+ const isPublic = settingsConfig?.publicGetGroups?.includes(group);
2005
+ if (method === "GET") {
2006
+ if (!isPublic && perm) {
2007
+ const a = await config.requireAuth(req);
2008
+ if (a) return a;
2009
+ const pe = await perm(req, "settings", "read");
2010
+ if (pe) return pe;
2011
+ }
2012
+ return settingsHandlers.GET(req, group);
2013
+ }
2014
+ if (method === "PUT") {
2015
+ if (perm) {
2016
+ const pe = await perm(req, "settings", "update");
2017
+ if (pe) return pe;
2018
+ }
2019
+ return settingsHandlers.PUT(req, group);
2020
+ }
1479
2021
  }
1480
2022
  if (path[0] === "chat" && chatHandlers) {
1481
2023
  if (path.length === 2 && path[1] === "identify" && method === "POST") return chatHandlers.identify(req);
@@ -1512,6 +2054,1006 @@ function createCmsApiHandler(config) {
1512
2054
  }
1513
2055
  };
1514
2056
  }
2057
+
2058
+ // src/api/storefront-handlers.ts
2059
+ import { In, IsNull as IsNull2 } from "typeorm";
2060
+
2061
+ // src/lib/is-valid-signup-email.ts
2062
+ var MAX_EMAIL = 254;
2063
+ var MAX_LOCAL = 64;
2064
+ function isValidSignupEmail(email) {
2065
+ if (!email || email.length > MAX_EMAIL) return false;
2066
+ const at = email.indexOf("@");
2067
+ if (at <= 0 || at !== email.lastIndexOf("@")) return false;
2068
+ const local = email.slice(0, at);
2069
+ const domain = email.slice(at + 1);
2070
+ if (!local || local.length > MAX_LOCAL || !domain || domain.length > 253) return false;
2071
+ if (local.startsWith(".") || local.endsWith(".") || local.includes("..")) return false;
2072
+ if (domain.startsWith(".") || domain.endsWith(".") || domain.includes("..")) return false;
2073
+ if (!/^[a-z0-9._%+-]+$/i.test(local)) return false;
2074
+ if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/i.test(domain)) return false;
2075
+ const tld = domain.split(".").pop();
2076
+ return tld.length >= 2;
2077
+ }
2078
+
2079
+ // src/api/storefront-handlers.ts
2080
+ init_email_queue();
2081
+ var GUEST_COOKIE = "guest_id";
2082
+ var ONE_YEAR = 60 * 60 * 24 * 365;
2083
+ function parseCookies(header) {
2084
+ const out = {};
2085
+ if (!header) return out;
2086
+ for (const part of header.split(";")) {
2087
+ const i = part.indexOf("=");
2088
+ if (i === -1) continue;
2089
+ const k = part.slice(0, i).trim();
2090
+ const v = part.slice(i + 1).trim();
2091
+ out[k] = decodeURIComponent(v);
2092
+ }
2093
+ return out;
2094
+ }
2095
+ function guestCookieHeader(name, token) {
2096
+ return `${name}=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ONE_YEAR}`;
2097
+ }
2098
+ function orderNumber() {
2099
+ return `ORD-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
2100
+ }
2101
+ var SIGNUP_VERIFY_EXPIRY_HOURS = 72;
2102
+ function createStorefrontApiHandler(config) {
2103
+ const { dataSource, entityMap, json, getSessionUser, getCms, getCompanyDetails, publicSiteUrl } = config;
2104
+ const cookieName = config.guestCookieName ?? GUEST_COOKIE;
2105
+ const cartRepo = () => dataSource.getRepository(entityMap.carts);
2106
+ const cartItemRepo = () => dataSource.getRepository(entityMap.cart_items);
2107
+ const productRepo = () => dataSource.getRepository(entityMap.products);
2108
+ const contactRepo = () => dataSource.getRepository(entityMap.contacts);
2109
+ const addressRepo = () => dataSource.getRepository(entityMap.addresses);
2110
+ const orderRepo = () => dataSource.getRepository(entityMap.orders);
2111
+ const orderItemRepo = () => dataSource.getRepository(entityMap.order_items);
2112
+ const wishlistRepo = () => dataSource.getRepository(entityMap.wishlists);
2113
+ const wishlistItemRepo = () => dataSource.getRepository(entityMap.wishlist_items);
2114
+ const userRepo = () => dataSource.getRepository(entityMap.users);
2115
+ const tokenRepo = () => dataSource.getRepository(entityMap.password_reset_tokens);
2116
+ const collectionRepo = () => dataSource.getRepository(entityMap.collections);
2117
+ const groupRepo = () => dataSource.getRepository(entityMap.user_groups);
2118
+ async function ensureContactForUser(userId) {
2119
+ let c = await contactRepo().findOne({ where: { userId, deleted: false } });
2120
+ if (c) return c;
2121
+ const u = await userRepo().findOne({ where: { id: userId } });
2122
+ if (!u) return null;
2123
+ const unclaimed = await contactRepo().findOne({
2124
+ where: { email: u.email, userId: IsNull2(), deleted: false }
2125
+ });
2126
+ if (unclaimed) {
2127
+ await contactRepo().update(unclaimed.id, { userId });
2128
+ return { id: unclaimed.id };
2129
+ }
2130
+ const created = await contactRepo().save(
2131
+ contactRepo().create({
2132
+ name: u.name,
2133
+ email: u.email,
2134
+ phone: null,
2135
+ userId,
2136
+ deleted: false
2137
+ })
2138
+ );
2139
+ return { id: created.id };
2140
+ }
2141
+ async function getOrCreateCart(req) {
2142
+ const u = await getSessionUser();
2143
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2144
+ if (Number.isFinite(uid)) {
2145
+ const contact = await ensureContactForUser(uid);
2146
+ if (!contact) return { cart: {}, setCookie: null, err: json({ error: "User not found" }, { status: 400 }) };
2147
+ let cart2 = await cartRepo().findOne({
2148
+ where: { contactId: contact.id },
2149
+ relations: ["items", "items.product"]
2150
+ });
2151
+ if (!cart2) {
2152
+ cart2 = await cartRepo().save(
2153
+ cartRepo().create({ contactId: contact.id, guestToken: null, currency: "INR" })
2154
+ );
2155
+ cart2 = await cartRepo().findOne({
2156
+ where: { id: cart2.id },
2157
+ relations: ["items", "items.product"]
2158
+ });
2159
+ }
2160
+ return { cart: cart2, setCookie: null, err: null };
2161
+ }
2162
+ const cookies = parseCookies(req.headers.get("cookie"));
2163
+ let token = cookies[cookieName] || "";
2164
+ if (!token) {
2165
+ token = crypto.randomUUID();
2166
+ let cart2 = await cartRepo().findOne({
2167
+ where: { guestToken: token },
2168
+ relations: ["items", "items.product"]
2169
+ });
2170
+ if (!cart2) {
2171
+ cart2 = await cartRepo().save(
2172
+ cartRepo().create({ guestToken: token, contactId: null, currency: "INR" })
2173
+ );
2174
+ cart2 = await cartRepo().findOne({
2175
+ where: { id: cart2.id },
2176
+ relations: ["items", "items.product"]
2177
+ });
2178
+ }
2179
+ return { cart: cart2, setCookie: guestCookieHeader(cookieName, token), err: null };
2180
+ }
2181
+ let cart = await cartRepo().findOne({
2182
+ where: { guestToken: token },
2183
+ relations: ["items", "items.product"]
2184
+ });
2185
+ if (!cart) {
2186
+ cart = await cartRepo().save(
2187
+ cartRepo().create({ guestToken: token, contactId: null, currency: "INR" })
2188
+ );
2189
+ cart = await cartRepo().findOne({
2190
+ where: { id: cart.id },
2191
+ relations: ["items", "items.product"]
2192
+ });
2193
+ }
2194
+ return { cart, setCookie: null, err: null };
2195
+ }
2196
+ function primaryProductImageUrl(metadata) {
2197
+ const meta = metadata;
2198
+ const images = meta?.images;
2199
+ if (!Array.isArray(images) || !images.length) return null;
2200
+ const sorted = images.filter((i) => i?.url);
2201
+ if (!sorted.length) return null;
2202
+ const di = sorted.findIndex((i) => i.isDefault);
2203
+ if (di > 0) {
2204
+ const [d] = sorted.splice(di, 1);
2205
+ sorted.unshift(d);
2206
+ }
2207
+ return sorted[0].url;
2208
+ }
2209
+ function serializeCart(cart) {
2210
+ const items = cart.items || [];
2211
+ return {
2212
+ id: cart.id,
2213
+ currency: cart.currency,
2214
+ items: items.map((it) => {
2215
+ const p = it.product;
2216
+ return {
2217
+ id: it.id,
2218
+ productId: it.productId,
2219
+ quantity: it.quantity,
2220
+ metadata: it.metadata,
2221
+ product: p ? {
2222
+ id: p.id,
2223
+ name: p.name,
2224
+ slug: p.slug,
2225
+ price: p.price,
2226
+ sku: p.sku,
2227
+ image: primaryProductImageUrl(p.metadata)
2228
+ } : null
2229
+ };
2230
+ })
2231
+ };
2232
+ }
2233
+ function serializeProduct(p) {
2234
+ return {
2235
+ id: p.id,
2236
+ name: p.name,
2237
+ slug: p.slug,
2238
+ sku: p.sku,
2239
+ hsn: p.hsn,
2240
+ price: p.price,
2241
+ compareAtPrice: p.compareAtPrice,
2242
+ status: p.status,
2243
+ collectionId: p.collectionId,
2244
+ metadata: p.metadata
2245
+ };
2246
+ }
2247
+ return {
2248
+ async handle(method, path, req) {
2249
+ try {
2250
+ let serializeAddress2 = function(a) {
2251
+ return {
2252
+ id: a.id,
2253
+ contactId: a.contactId,
2254
+ tag: a.tag,
2255
+ line1: a.line1,
2256
+ line2: a.line2,
2257
+ city: a.city,
2258
+ state: a.state,
2259
+ postalCode: a.postalCode,
2260
+ country: a.country
2261
+ };
2262
+ };
2263
+ var serializeAddress = serializeAddress2;
2264
+ if (path[0] === "products" && path.length === 1 && method === "GET") {
2265
+ const url = new URL(req.url || "", "http://localhost");
2266
+ const collectionSlug = url.searchParams.get("collection")?.trim();
2267
+ const collectionId = url.searchParams.get("collectionId");
2268
+ const limit = Math.min(100, Math.max(1, parseInt(url.searchParams.get("limit") || "20", 10)));
2269
+ const offset = Math.max(0, parseInt(url.searchParams.get("offset") || "0", 10));
2270
+ const where = { status: "available", deleted: false };
2271
+ let collectionFilter = null;
2272
+ if (collectionSlug) {
2273
+ let col = null;
2274
+ if (/^\d+$/.test(collectionSlug)) {
2275
+ col = await collectionRepo().findOne({
2276
+ where: {
2277
+ id: parseInt(collectionSlug, 10),
2278
+ active: true,
2279
+ deleted: false
2280
+ }
2281
+ });
2282
+ } else {
2283
+ 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();
2284
+ }
2285
+ if (!col) {
2286
+ return json({ products: [], total: 0, collection: null });
2287
+ }
2288
+ where.collectionId = col.id;
2289
+ collectionFilter = { name: col.name, slug: col.slug };
2290
+ } else if (collectionId) {
2291
+ const cid = parseInt(collectionId, 10);
2292
+ if (Number.isFinite(cid)) where.collectionId = cid;
2293
+ }
2294
+ const [items, total] = await productRepo().findAndCount({
2295
+ where,
2296
+ order: { id: "ASC" },
2297
+ take: limit,
2298
+ skip: offset
2299
+ });
2300
+ return json({
2301
+ products: items.map(serializeProduct),
2302
+ total,
2303
+ ...collectionFilter && { collection: collectionFilter }
2304
+ });
2305
+ }
2306
+ if (path[0] === "products" && path.length === 2 && method === "GET") {
2307
+ const idOrSlug = path[1];
2308
+ const byId = /^\d+$/.test(idOrSlug);
2309
+ const product = await productRepo().findOne({
2310
+ where: byId ? { id: parseInt(idOrSlug, 10), status: "available", deleted: false } : { slug: idOrSlug, status: "available", deleted: false },
2311
+ relations: ["attributes", "attributes.attribute"]
2312
+ });
2313
+ if (!product) return json({ error: "Not found" }, { status: 404 });
2314
+ const p = product;
2315
+ const attrRows = p.attributes ?? [];
2316
+ const attributeTags = attrRows.map((pa) => ({
2317
+ name: pa.attribute?.name ?? "",
2318
+ value: String(pa.value ?? "")
2319
+ })).filter((t) => t.name || t.value);
2320
+ return json({ ...serializeProduct(p), attributes: attributeTags });
2321
+ }
2322
+ if (path[0] === "collections" && path.length === 1 && method === "GET") {
2323
+ const items = await collectionRepo().find({
2324
+ where: { active: true, deleted: false },
2325
+ order: { sortOrder: "ASC", id: "ASC" }
2326
+ });
2327
+ const ids = items.map((c) => c.id);
2328
+ const countByCollection = {};
2329
+ if (ids.length > 0) {
2330
+ 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();
2331
+ for (const r of rows) {
2332
+ const cid = r.collectionId;
2333
+ if (cid != null) countByCollection[Number(cid)] = parseInt(String(r.cnt), 10);
2334
+ }
2335
+ }
2336
+ return json({
2337
+ collections: items.map((c) => {
2338
+ const col = c;
2339
+ const id = col.id;
2340
+ return {
2341
+ id,
2342
+ name: col.name,
2343
+ slug: col.slug,
2344
+ description: col.description,
2345
+ image: col.image,
2346
+ productCount: countByCollection[id] ?? 0
2347
+ };
2348
+ })
2349
+ });
2350
+ }
2351
+ if (path[0] === "collections" && path.length === 2 && method === "GET") {
2352
+ const idOrSlug = path[1];
2353
+ const byId = /^\d+$/.test(idOrSlug);
2354
+ const collection = await collectionRepo().findOne({
2355
+ where: byId ? { id: parseInt(idOrSlug, 10), active: true, deleted: false } : { slug: idOrSlug, active: true, deleted: false }
2356
+ });
2357
+ if (!collection) return json({ error: "Not found" }, { status: 404 });
2358
+ const col = collection;
2359
+ const products = await productRepo().find({
2360
+ where: { collectionId: col.id, status: "available", deleted: false },
2361
+ order: { id: "ASC" }
2362
+ });
2363
+ return json({
2364
+ id: col.id,
2365
+ name: col.name,
2366
+ slug: col.slug,
2367
+ description: col.description,
2368
+ image: col.image,
2369
+ products: products.map((p) => serializeProduct(p))
2370
+ });
2371
+ }
2372
+ if (path[0] === "profile" && path.length === 1 && method === "GET") {
2373
+ const u = await getSessionUser();
2374
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2375
+ if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
2376
+ const user = await userRepo().findOne({ where: { id: uid }, select: ["id", "name", "email"] });
2377
+ if (!user) return json({ error: "Not found" }, { status: 404 });
2378
+ const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
2379
+ return json({
2380
+ user: { id: user.id, name: user.name, email: user.email },
2381
+ contact: contact ? {
2382
+ id: contact.id,
2383
+ name: contact.name,
2384
+ email: contact.email,
2385
+ phone: contact.phone
2386
+ } : null
2387
+ });
2388
+ }
2389
+ if (path[0] === "profile" && path.length === 1 && method === "PUT") {
2390
+ const u = await getSessionUser();
2391
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2392
+ if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
2393
+ const b = await req.json().catch(() => ({}));
2394
+ const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
2395
+ if (contact) {
2396
+ const updates = {};
2397
+ if (typeof b.name === "string" && b.name.trim()) updates.name = b.name.trim();
2398
+ if (b.phone !== void 0) updates.phone = b.phone === null || b.phone === "" ? null : String(b.phone);
2399
+ if (Object.keys(updates).length) await contactRepo().update(contact.id, updates);
2400
+ }
2401
+ const user = await userRepo().findOne({ where: { id: uid }, select: ["id", "name", "email"] });
2402
+ if (user && typeof b.name === "string" && b.name.trim()) {
2403
+ await userRepo().update(uid, { name: b.name.trim() });
2404
+ }
2405
+ const updatedContact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
2406
+ const updatedUser = await userRepo().findOne({ where: { id: uid }, select: ["id", "name", "email"] });
2407
+ return json({
2408
+ user: updatedUser ? { id: updatedUser.id, name: updatedUser.name, email: updatedUser.email } : null,
2409
+ contact: updatedContact ? {
2410
+ id: updatedContact.id,
2411
+ name: updatedContact.name,
2412
+ email: updatedContact.email,
2413
+ phone: updatedContact.phone
2414
+ } : null
2415
+ });
2416
+ }
2417
+ async function getContactForAddresses() {
2418
+ const u = await getSessionUser();
2419
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2420
+ if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
2421
+ const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
2422
+ if (!contact) return json({ error: "Contact not found" }, { status: 404 });
2423
+ return { contactId: contact.id };
2424
+ }
2425
+ if (path[0] === "addresses" && path.length === 1 && method === "GET") {
2426
+ const contactOrErr = await getContactForAddresses();
2427
+ if (contactOrErr instanceof Response) return contactOrErr;
2428
+ const list = await addressRepo().find({
2429
+ where: { contactId: contactOrErr.contactId },
2430
+ order: { id: "ASC" }
2431
+ });
2432
+ return json({ addresses: list.map((a) => serializeAddress2(a)) });
2433
+ }
2434
+ if (path[0] === "addresses" && path.length === 1 && method === "POST") {
2435
+ const contactOrErr = await getContactForAddresses();
2436
+ if (contactOrErr instanceof Response) return contactOrErr;
2437
+ const b = await req.json().catch(() => ({}));
2438
+ const created = await addressRepo().save(
2439
+ addressRepo().create({
2440
+ contactId: contactOrErr.contactId,
2441
+ tag: typeof b.tag === "string" ? b.tag.trim() || null : null,
2442
+ line1: typeof b.line1 === "string" ? b.line1.trim() || null : null,
2443
+ line2: typeof b.line2 === "string" ? b.line2.trim() || null : null,
2444
+ city: typeof b.city === "string" ? b.city.trim() || null : null,
2445
+ state: typeof b.state === "string" ? b.state.trim() || null : null,
2446
+ postalCode: typeof b.postalCode === "string" ? b.postalCode.trim() || null : null,
2447
+ country: typeof b.country === "string" ? b.country.trim() || null : null
2448
+ })
2449
+ );
2450
+ return json(serializeAddress2(created));
2451
+ }
2452
+ if (path[0] === "addresses" && path.length === 2 && (method === "PATCH" || method === "PUT")) {
2453
+ const contactOrErr = await getContactForAddresses();
2454
+ if (contactOrErr instanceof Response) return contactOrErr;
2455
+ const id = parseInt(path[1], 10);
2456
+ if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
2457
+ const existing = await addressRepo().findOne({ where: { id, contactId: contactOrErr.contactId } });
2458
+ if (!existing) return json({ error: "Not found" }, { status: 404 });
2459
+ const b = await req.json().catch(() => ({}));
2460
+ const updates = {};
2461
+ if (b.tag !== void 0) updates.tag = typeof b.tag === "string" ? b.tag.trim() || null : null;
2462
+ if (b.line1 !== void 0) updates.line1 = typeof b.line1 === "string" ? b.line1.trim() || null : null;
2463
+ if (b.line2 !== void 0) updates.line2 = typeof b.line2 === "string" ? b.line2.trim() || null : null;
2464
+ if (b.city !== void 0) updates.city = typeof b.city === "string" ? b.city.trim() || null : null;
2465
+ if (b.state !== void 0) updates.state = typeof b.state === "string" ? b.state.trim() || null : null;
2466
+ if (b.postalCode !== void 0) updates.postalCode = typeof b.postalCode === "string" ? b.postalCode.trim() || null : null;
2467
+ if (b.country !== void 0) updates.country = typeof b.country === "string" ? b.country.trim() || null : null;
2468
+ if (Object.keys(updates).length) await addressRepo().update(id, updates);
2469
+ const updated = await addressRepo().findOne({ where: { id } });
2470
+ return json(serializeAddress2(updated));
2471
+ }
2472
+ if (path[0] === "addresses" && path.length === 2 && method === "DELETE") {
2473
+ const contactOrErr = await getContactForAddresses();
2474
+ if (contactOrErr instanceof Response) return contactOrErr;
2475
+ const id = parseInt(path[1], 10);
2476
+ if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
2477
+ const existing = await addressRepo().findOne({ where: { id, contactId: contactOrErr.contactId } });
2478
+ if (!existing) return json({ error: "Not found" }, { status: 404 });
2479
+ await addressRepo().delete(id);
2480
+ return json({ deleted: true });
2481
+ }
2482
+ if (path[0] === "verify-email" && path.length === 1 && method === "POST") {
2483
+ const b = await req.json().catch(() => ({}));
2484
+ const token = typeof b.token === "string" ? b.token.trim() : "";
2485
+ if (!token) return json({ error: "token is required" }, { status: 400 });
2486
+ const record = await tokenRepo().findOne({ where: { token } });
2487
+ if (!record || record.expiresAt < /* @__PURE__ */ new Date()) {
2488
+ return json({ error: "Invalid or expired link. Please sign up again or contact support." }, { status: 400 });
2489
+ }
2490
+ const email = record.email;
2491
+ const user = await userRepo().findOne({ where: { email }, select: ["id", "blocked"] });
2492
+ if (!user) return json({ error: "User not found" }, { status: 400 });
2493
+ await userRepo().update(user.id, { blocked: false, updatedAt: /* @__PURE__ */ new Date() });
2494
+ await tokenRepo().delete({ email });
2495
+ return json({ success: true, message: "Email verified. You can sign in." });
2496
+ }
2497
+ if (path[0] === "register" && path.length === 1 && method === "POST") {
2498
+ if (!config.hashPassword) return json({ error: "Registration not configured" }, { status: 501 });
2499
+ const b = await req.json().catch(() => ({}));
2500
+ const name = typeof b.name === "string" ? b.name.trim() : "";
2501
+ const email = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
2502
+ const password = typeof b.password === "string" ? b.password : "";
2503
+ if (!name || !email || !password) return json({ error: "name, email and password are required" }, { status: 400 });
2504
+ if (!isValidSignupEmail(email)) return json({ error: "Invalid email address" }, { status: 400 });
2505
+ const existing = await userRepo().findOne({ where: { email } });
2506
+ if (existing) return json({ error: "User with this email already exists" }, { status: 400 });
2507
+ const customerG = await groupRepo().findOne({ where: { name: "Customer", deleted: false } });
2508
+ const groupId = customerG ? customerG.id : null;
2509
+ const hashed = await config.hashPassword(password);
2510
+ const requireEmailVerification = Boolean(getCms);
2511
+ const newUser = await userRepo().save(
2512
+ userRepo().create({
2513
+ name,
2514
+ email,
2515
+ password: hashed,
2516
+ blocked: requireEmailVerification,
2517
+ groupId,
2518
+ adminAccess: false
2519
+ })
2520
+ );
2521
+ const userId = newUser.id;
2522
+ await linkUnclaimedContactToUser(dataSource, entityMap.contacts, userId, email);
2523
+ let emailVerificationSent = false;
2524
+ if (requireEmailVerification && getCms) {
2525
+ try {
2526
+ const crypto2 = await import("crypto");
2527
+ const rawToken = crypto2.randomBytes(32).toString("hex");
2528
+ const expiresAt = new Date(Date.now() + SIGNUP_VERIFY_EXPIRY_HOURS * 60 * 60 * 1e3);
2529
+ await tokenRepo().save(
2530
+ tokenRepo().create({ email, token: rawToken, expiresAt })
2531
+ );
2532
+ const cms = await getCms();
2533
+ const companyDetails = getCompanyDetails ? await getCompanyDetails() : {};
2534
+ const base = (publicSiteUrl || "").replace(/\/$/, "").trim() || "http://localhost:3000";
2535
+ const verifyEmailUrl = `${base}/verify-email?token=${encodeURIComponent(rawToken)}`;
2536
+ await queueEmail(cms, {
2537
+ to: email,
2538
+ templateName: "signup",
2539
+ ctx: { name, verifyEmailUrl, companyDetails: companyDetails ?? {} }
2540
+ });
2541
+ emailVerificationSent = true;
2542
+ } catch {
2543
+ await userRepo().update(userId, { blocked: false, updatedAt: /* @__PURE__ */ new Date() });
2544
+ }
2545
+ }
2546
+ return json({
2547
+ success: true,
2548
+ userId,
2549
+ emailVerificationSent
2550
+ });
2551
+ }
2552
+ if (path[0] === "cart" && path.length === 1 && method === "GET") {
2553
+ const { cart, setCookie, err } = await getOrCreateCart(req);
2554
+ if (err) return err;
2555
+ const body = serializeCart(cart);
2556
+ if (setCookie) return json(body, { headers: { "Set-Cookie": setCookie } });
2557
+ return json(body);
2558
+ }
2559
+ if (path[0] === "cart" && path[1] === "items" && path.length === 2 && method === "POST") {
2560
+ const body = await req.json().catch(() => ({}));
2561
+ const productId = Number(body.productId);
2562
+ const quantity = Math.max(1, Number(body.quantity) || 1);
2563
+ if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
2564
+ const product = await productRepo().findOne({ where: { id: productId, deleted: false } });
2565
+ if (!product) return json({ error: "Product not found" }, { status: 404 });
2566
+ const { cart, setCookie, err } = await getOrCreateCart(req);
2567
+ if (err) return err;
2568
+ const cartId = cart.id;
2569
+ const existing = await cartItemRepo().findOne({ where: { cartId, productId } });
2570
+ if (existing) {
2571
+ await cartItemRepo().update(existing.id, {
2572
+ quantity: existing.quantity + quantity
2573
+ });
2574
+ } else {
2575
+ await cartItemRepo().save(
2576
+ cartItemRepo().create({ cartId, productId, quantity })
2577
+ );
2578
+ }
2579
+ await cartRepo().update(cartId, { updatedAt: /* @__PURE__ */ new Date() });
2580
+ const fresh = await cartRepo().findOne({
2581
+ where: { id: cartId },
2582
+ relations: ["items", "items.product"]
2583
+ });
2584
+ const out = serializeCart(fresh);
2585
+ if (setCookie) return json(out, { headers: { "Set-Cookie": setCookie } });
2586
+ return json(out);
2587
+ }
2588
+ if (path[0] === "cart" && path[1] === "items" && path.length === 3) {
2589
+ const itemId = parseInt(path[2], 10);
2590
+ if (!Number.isFinite(itemId)) return json({ error: "Invalid item id" }, { status: 400 });
2591
+ const { cart, setCookie, err } = await getOrCreateCart(req);
2592
+ if (err) return err;
2593
+ const cartId = cart.id;
2594
+ const item = await cartItemRepo().findOne({ where: { id: itemId, cartId } });
2595
+ if (!item) return json({ error: "Not found" }, { status: 404 });
2596
+ if (method === "DELETE") {
2597
+ await cartItemRepo().delete(itemId);
2598
+ await cartRepo().update(cartId, { updatedAt: /* @__PURE__ */ new Date() });
2599
+ const fresh = await cartRepo().findOne({
2600
+ where: { id: cartId },
2601
+ relations: ["items", "items.product"]
2602
+ });
2603
+ const out = serializeCart(fresh);
2604
+ if (setCookie) return json(out, { headers: { "Set-Cookie": setCookie } });
2605
+ return json(out);
2606
+ }
2607
+ if (method === "PATCH") {
2608
+ const b = await req.json().catch(() => ({}));
2609
+ const q = Math.max(0, Number(b.quantity) || 0);
2610
+ if (q === 0) await cartItemRepo().delete(itemId);
2611
+ else await cartItemRepo().update(itemId, { quantity: q });
2612
+ await cartRepo().update(cartId, { updatedAt: /* @__PURE__ */ new Date() });
2613
+ const fresh = await cartRepo().findOne({
2614
+ where: { id: cartId },
2615
+ relations: ["items", "items.product"]
2616
+ });
2617
+ const out = serializeCart(fresh);
2618
+ if (setCookie) return json(out, { headers: { "Set-Cookie": setCookie } });
2619
+ return json(out);
2620
+ }
2621
+ }
2622
+ if (path[0] === "cart" && path[1] === "merge" && method === "POST") {
2623
+ const u = await getSessionUser();
2624
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2625
+ if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
2626
+ const contact = await ensureContactForUser(uid);
2627
+ if (!contact) return json({ error: "Contact not found" }, { status: 400 });
2628
+ const cookies = parseCookies(req.headers.get("cookie"));
2629
+ const guestToken = cookies[cookieName];
2630
+ if (!guestToken) return json({ merged: false, message: "No guest cart" });
2631
+ const guestCart = await cartRepo().findOne({
2632
+ where: { guestToken },
2633
+ relations: ["items"]
2634
+ });
2635
+ if (!guestCart || !(guestCart.items || []).length) {
2636
+ let uc = await cartRepo().findOne({
2637
+ where: { contactId: contact.id },
2638
+ relations: ["items", "items.product"]
2639
+ });
2640
+ if (!uc) uc = { items: [] };
2641
+ return json(
2642
+ { merged: false, cart: serializeCart(uc) },
2643
+ { headers: { "Set-Cookie": `${cookieName}=; Path=/; Max-Age=0` } }
2644
+ );
2645
+ }
2646
+ let userCart = await cartRepo().findOne({ where: { contactId: contact.id } });
2647
+ if (!userCart) {
2648
+ userCart = await cartRepo().save(
2649
+ cartRepo().create({ contactId: contact.id, guestToken: null, currency: guestCart.currency })
2650
+ );
2651
+ }
2652
+ const uidCart = userCart.id;
2653
+ const gItems = guestCart.items || [];
2654
+ for (const gi of gItems) {
2655
+ const existing = await cartItemRepo().findOne({
2656
+ where: { cartId: uidCart, productId: gi.productId }
2657
+ });
2658
+ if (existing) {
2659
+ await cartItemRepo().update(existing.id, {
2660
+ quantity: existing.quantity + gi.quantity
2661
+ });
2662
+ } else {
2663
+ await cartItemRepo().save(
2664
+ cartItemRepo().create({
2665
+ cartId: uidCart,
2666
+ productId: gi.productId,
2667
+ quantity: gi.quantity,
2668
+ metadata: gi.metadata
2669
+ })
2670
+ );
2671
+ }
2672
+ }
2673
+ await cartRepo().delete(guestCart.id);
2674
+ await cartRepo().update(uidCart, { updatedAt: /* @__PURE__ */ new Date() });
2675
+ const fresh = await cartRepo().findOne({
2676
+ where: { id: uidCart },
2677
+ relations: ["items", "items.product"]
2678
+ });
2679
+ const guestWishlist = await wishlistRepo().findOne({
2680
+ where: { guestId: guestToken },
2681
+ relations: ["items"]
2682
+ });
2683
+ if (guestWishlist && (guestWishlist.items || []).length > 0) {
2684
+ const userWishlist = await getDefaultWishlist(contact.id);
2685
+ const gItems2 = guestWishlist.items || [];
2686
+ for (const gi of gItems2) {
2687
+ const pid = gi.productId;
2688
+ const ex = await wishlistItemRepo().findOne({ where: { wishlistId: userWishlist.id, productId: pid } });
2689
+ if (!ex) await wishlistItemRepo().save(wishlistItemRepo().create({ wishlistId: userWishlist.id, productId: pid }));
2690
+ }
2691
+ await wishlistRepo().delete(guestWishlist.id);
2692
+ }
2693
+ return json({ merged: true, cart: serializeCart(fresh) }, { headers: { "Set-Cookie": `${cookieName}=; Path=/; Max-Age=0` } });
2694
+ }
2695
+ async function getDefaultWishlist(contactId) {
2696
+ let w = await wishlistRepo().findOne({ where: { contactId, name: "default" } });
2697
+ if (!w) {
2698
+ w = await wishlistRepo().save(wishlistRepo().create({ contactId, guestId: null, name: "default" }));
2699
+ }
2700
+ return w;
2701
+ }
2702
+ async function getOrCreateWishlist(req2) {
2703
+ const u = await getSessionUser();
2704
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2705
+ if (Number.isFinite(uid)) {
2706
+ const contact = await ensureContactForUser(uid);
2707
+ if (!contact) return { wishlist: {}, setCookie: null, err: json({ error: "User not found" }, { status: 400 }) };
2708
+ const w2 = await getDefaultWishlist(contact.id);
2709
+ const wishlist = await wishlistRepo().findOne({ where: { id: w2.id } });
2710
+ return { wishlist, setCookie: null, err: null };
2711
+ }
2712
+ const cookies = parseCookies(req2.headers.get("cookie"));
2713
+ let token = cookies[cookieName] || "";
2714
+ if (!token) {
2715
+ token = crypto.randomUUID();
2716
+ let w2 = await wishlistRepo().findOne({ where: { guestId: token } });
2717
+ if (!w2) {
2718
+ w2 = await wishlistRepo().save(wishlistRepo().create({ guestId: token, contactId: null, name: "default" }));
2719
+ }
2720
+ return { wishlist: w2, setCookie: guestCookieHeader(cookieName, token), err: null };
2721
+ }
2722
+ let w = await wishlistRepo().findOne({ where: { guestId: token } });
2723
+ if (!w) {
2724
+ w = await wishlistRepo().save(wishlistRepo().create({ guestId: token, contactId: null, name: "default" }));
2725
+ }
2726
+ return { wishlist: w, setCookie: null, err: null };
2727
+ }
2728
+ if (path[0] === "wishlist" && path.length === 1 && method === "GET") {
2729
+ const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
2730
+ if (err) return err;
2731
+ const items = await wishlistItemRepo().find({
2732
+ where: { wishlistId: wishlist.id },
2733
+ relations: ["product"]
2734
+ });
2735
+ const body = {
2736
+ wishlistId: wishlist.id,
2737
+ items: items.map((it) => {
2738
+ const p = it.product;
2739
+ return {
2740
+ id: it.id,
2741
+ productId: it.productId,
2742
+ product: p ? {
2743
+ id: p.id,
2744
+ name: p.name,
2745
+ slug: p.slug,
2746
+ price: p.price,
2747
+ sku: p.sku,
2748
+ image: primaryProductImageUrl(p.metadata)
2749
+ } : null
2750
+ };
2751
+ })
2752
+ };
2753
+ if (setCookie) return json(body, { headers: { "Set-Cookie": setCookie } });
2754
+ return json(body);
2755
+ }
2756
+ if (path[0] === "wishlist" && path[1] === "items" && path.length === 2 && method === "POST") {
2757
+ const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
2758
+ if (err) return err;
2759
+ const b = await req.json().catch(() => ({}));
2760
+ const productId = Number(b.productId);
2761
+ if (!Number.isFinite(productId)) return json({ error: "productId required" }, { status: 400 });
2762
+ const wid = wishlist.id;
2763
+ const ex = await wishlistItemRepo().findOne({ where: { wishlistId: wid, productId } });
2764
+ if (!ex) await wishlistItemRepo().save(wishlistItemRepo().create({ wishlistId: wid, productId }));
2765
+ if (setCookie) return json({ ok: true }, { headers: { "Set-Cookie": setCookie } });
2766
+ return json({ ok: true });
2767
+ }
2768
+ if (path[0] === "wishlist" && path[1] === "items" && path.length === 3 && method === "DELETE") {
2769
+ const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
2770
+ if (err) return err;
2771
+ const productId = parseInt(path[2], 10);
2772
+ await wishlistItemRepo().delete({ wishlistId: wishlist.id, productId });
2773
+ if (setCookie) return json({ ok: true }, { headers: { "Set-Cookie": setCookie } });
2774
+ return json({ ok: true });
2775
+ }
2776
+ if (path[0] === "checkout" && path[1] === "order" && path.length === 2 && method === "POST") {
2777
+ const b = await req.json().catch(() => ({}));
2778
+ const u = await getSessionUser();
2779
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2780
+ let contactId;
2781
+ let cart;
2782
+ if (Number.isFinite(uid)) {
2783
+ const contact = await ensureContactForUser(uid);
2784
+ if (!contact) return json({ error: "Contact required" }, { status: 400 });
2785
+ contactId = contact.id;
2786
+ cart = await cartRepo().findOne({
2787
+ where: { contactId },
2788
+ relations: ["items", "items.product"]
2789
+ });
2790
+ } else {
2791
+ const email = (b.email || "").trim();
2792
+ const name = (b.name || "").trim();
2793
+ if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
2794
+ let contact = await contactRepo().findOne({ where: { email, deleted: false } });
2795
+ if (contact && contact.userId != null) {
2796
+ return json({ error: "Please sign in to complete checkout" }, { status: 400 });
2797
+ }
2798
+ if (!contact) {
2799
+ contact = await contactRepo().save(
2800
+ contactRepo().create({
2801
+ name,
2802
+ email,
2803
+ phone: b.phone || null,
2804
+ userId: null,
2805
+ deleted: false
2806
+ })
2807
+ );
2808
+ } else if (name) await contactRepo().update(contact.id, { name, phone: b.phone || contact.phone });
2809
+ contactId = contact.id;
2810
+ const cookies = parseCookies(req.headers.get("cookie"));
2811
+ const guestToken = cookies[cookieName];
2812
+ if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
2813
+ cart = await cartRepo().findOne({
2814
+ where: { guestToken },
2815
+ relations: ["items", "items.product"]
2816
+ });
2817
+ }
2818
+ if (!cart || !(cart.items || []).length) {
2819
+ return json({ error: "Cart is empty" }, { status: 400 });
2820
+ }
2821
+ let subtotal = 0;
2822
+ const lines = [];
2823
+ for (const it of cart.items || []) {
2824
+ const p = it.product;
2825
+ if (!p || p.deleted || p.status !== "available") continue;
2826
+ const unit = Number(p.price);
2827
+ const qty = it.quantity || 1;
2828
+ const lineTotal = unit * qty;
2829
+ subtotal += lineTotal;
2830
+ lines.push({ productId: p.id, quantity: qty, unitPrice: unit, tax: 0, total: lineTotal });
2831
+ }
2832
+ if (!lines.length) return json({ error: "No available items in cart" }, { status: 400 });
2833
+ const total = subtotal;
2834
+ const cartId = cart.id;
2835
+ const ord = await orderRepo().save(
2836
+ orderRepo().create({
2837
+ orderNumber: orderNumber(),
2838
+ contactId,
2839
+ billingAddressId: b.billingAddressId ?? null,
2840
+ shippingAddressId: b.shippingAddressId ?? null,
2841
+ status: "pending",
2842
+ subtotal,
2843
+ tax: 0,
2844
+ discount: 0,
2845
+ total,
2846
+ currency: cart.currency || "INR",
2847
+ metadata: { cartId }
2848
+ })
2849
+ );
2850
+ const oid = ord.id;
2851
+ for (const line of lines) {
2852
+ await orderItemRepo().save(
2853
+ orderItemRepo().create({
2854
+ orderId: oid,
2855
+ productId: line.productId,
2856
+ quantity: line.quantity,
2857
+ unitPrice: line.unitPrice,
2858
+ tax: line.tax,
2859
+ total: line.total
2860
+ })
2861
+ );
2862
+ }
2863
+ return json({
2864
+ orderId: oid,
2865
+ orderNumber: ord.orderNumber,
2866
+ total,
2867
+ currency: cart.currency || "INR"
2868
+ });
2869
+ }
2870
+ if (path[0] === "checkout" && path.length === 1 && method === "POST") {
2871
+ const b = await req.json().catch(() => ({}));
2872
+ const u = await getSessionUser();
2873
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2874
+ let contactId;
2875
+ let cart;
2876
+ if (Number.isFinite(uid)) {
2877
+ const contact = await ensureContactForUser(uid);
2878
+ if (!contact) return json({ error: "Contact required" }, { status: 400 });
2879
+ contactId = contact.id;
2880
+ cart = await cartRepo().findOne({
2881
+ where: { contactId },
2882
+ relations: ["items", "items.product"]
2883
+ });
2884
+ } else {
2885
+ const email = (b.email || "").trim();
2886
+ const name = (b.name || "").trim();
2887
+ if (!email || !name) return json({ error: "name and email required for guest checkout" }, { status: 400 });
2888
+ let contact = await contactRepo().findOne({ where: { email, deleted: false } });
2889
+ if (contact && contact.userId != null) {
2890
+ return json({ error: "Please sign in to complete checkout" }, { status: 400 });
2891
+ }
2892
+ if (!contact) {
2893
+ contact = await contactRepo().save(
2894
+ contactRepo().create({
2895
+ name,
2896
+ email,
2897
+ phone: b.phone || null,
2898
+ userId: null,
2899
+ deleted: false
2900
+ })
2901
+ );
2902
+ } else if (name) await contactRepo().update(contact.id, { name, phone: b.phone || contact.phone });
2903
+ contactId = contact.id;
2904
+ const cookies = parseCookies(req.headers.get("cookie"));
2905
+ const guestToken = cookies[cookieName];
2906
+ if (!guestToken) return json({ error: "Cart not found" }, { status: 400 });
2907
+ cart = await cartRepo().findOne({
2908
+ where: { guestToken },
2909
+ relations: ["items", "items.product"]
2910
+ });
2911
+ }
2912
+ if (!cart || !(cart.items || []).length) {
2913
+ return json({ error: "Cart is empty" }, { status: 400 });
2914
+ }
2915
+ let subtotal = 0;
2916
+ const lines = [];
2917
+ for (const it of cart.items || []) {
2918
+ const p = it.product;
2919
+ if (!p || p.deleted || p.status !== "available") continue;
2920
+ const unit = Number(p.price);
2921
+ const qty = it.quantity || 1;
2922
+ const lineTotal = unit * qty;
2923
+ subtotal += lineTotal;
2924
+ lines.push({ productId: p.id, quantity: qty, unitPrice: unit, tax: 0, total: lineTotal });
2925
+ }
2926
+ if (!lines.length) return json({ error: "No available items in cart" }, { status: 400 });
2927
+ const total = subtotal;
2928
+ const ord = await orderRepo().save(
2929
+ orderRepo().create({
2930
+ orderNumber: orderNumber(),
2931
+ contactId,
2932
+ billingAddressId: b.billingAddressId ?? null,
2933
+ shippingAddressId: b.shippingAddressId ?? null,
2934
+ status: "pending",
2935
+ subtotal,
2936
+ tax: 0,
2937
+ discount: 0,
2938
+ total,
2939
+ currency: cart.currency || "INR"
2940
+ })
2941
+ );
2942
+ const oid = ord.id;
2943
+ for (const line of lines) {
2944
+ await orderItemRepo().save(
2945
+ orderItemRepo().create({
2946
+ orderId: oid,
2947
+ productId: line.productId,
2948
+ quantity: line.quantity,
2949
+ unitPrice: line.unitPrice,
2950
+ tax: line.tax,
2951
+ total: line.total
2952
+ })
2953
+ );
2954
+ }
2955
+ await cartItemRepo().delete({ cartId: cart.id });
2956
+ await cartRepo().delete(cart.id);
2957
+ return json({
2958
+ orderId: oid,
2959
+ orderNumber: ord.orderNumber,
2960
+ total
2961
+ });
2962
+ }
2963
+ if (path[0] === "orders" && path.length === 1 && method === "GET") {
2964
+ const u = await getSessionUser();
2965
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
2966
+ if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
2967
+ const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
2968
+ if (!contact) return json({ orders: [] });
2969
+ const orders = await orderRepo().find({
2970
+ where: { contactId: contact.id, deleted: false },
2971
+ order: { createdAt: "DESC" },
2972
+ take: 50
2973
+ });
2974
+ const orderIds = orders.map((o) => o.id);
2975
+ const previewByOrder = {};
2976
+ if (orderIds.length) {
2977
+ const oItems = await orderItemRepo().find({
2978
+ where: { orderId: In(orderIds) },
2979
+ relations: ["product"],
2980
+ order: { id: "ASC" }
2981
+ });
2982
+ for (const oi of oItems) {
2983
+ const oid = oi.orderId;
2984
+ if (!previewByOrder[oid]) previewByOrder[oid] = [];
2985
+ if (previewByOrder[oid].length >= 4) continue;
2986
+ const url = primaryProductImageUrl(oi.product?.metadata);
2987
+ if (url && !previewByOrder[oid].includes(url)) previewByOrder[oid].push(url);
2988
+ }
2989
+ }
2990
+ return json({
2991
+ orders: orders.map((o) => {
2992
+ const ol = o;
2993
+ return {
2994
+ id: ol.id,
2995
+ orderNumber: ol.orderNumber,
2996
+ status: ol.status,
2997
+ total: ol.total,
2998
+ currency: ol.currency,
2999
+ createdAt: ol.createdAt,
3000
+ previewImages: previewByOrder[ol.id] ?? []
3001
+ };
3002
+ })
3003
+ });
3004
+ }
3005
+ if (path[0] === "orders" && path.length === 2 && method === "GET") {
3006
+ const u = await getSessionUser();
3007
+ const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
3008
+ if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
3009
+ const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
3010
+ if (!contact) return json({ error: "Not found" }, { status: 404 });
3011
+ const orderId = parseInt(path[1], 10);
3012
+ const order = await orderRepo().findOne({
3013
+ where: { id: orderId, contactId: contact.id, deleted: false },
3014
+ relations: ["items", "items.product"]
3015
+ });
3016
+ if (!order) return json({ error: "Not found" }, { status: 404 });
3017
+ const o = order;
3018
+ const lines = (o.items || []).map((line) => {
3019
+ const p = line.product;
3020
+ return {
3021
+ id: line.id,
3022
+ productId: line.productId,
3023
+ quantity: line.quantity,
3024
+ unitPrice: line.unitPrice,
3025
+ tax: line.tax,
3026
+ total: line.total,
3027
+ product: p ? {
3028
+ name: p.name,
3029
+ slug: p.slug,
3030
+ sku: p.sku,
3031
+ image: primaryProductImageUrl(p.metadata)
3032
+ } : null
3033
+ };
3034
+ });
3035
+ return json({
3036
+ order: {
3037
+ id: o.id,
3038
+ orderNumber: o.orderNumber,
3039
+ status: o.status,
3040
+ subtotal: o.subtotal,
3041
+ tax: o.tax,
3042
+ discount: o.discount,
3043
+ total: o.total,
3044
+ currency: o.currency,
3045
+ createdAt: o.createdAt,
3046
+ items: lines
3047
+ }
3048
+ });
3049
+ }
3050
+ return json({ error: "Not found" }, { status: 404 });
3051
+ } catch {
3052
+ return json({ error: "Server error" }, { status: 500 });
3053
+ }
3054
+ }
3055
+ };
3056
+ }
1515
3057
  export {
1516
3058
  createAnalyticsHandlers,
1517
3059
  createBlogBySlugHandler,
@@ -1525,6 +3067,7 @@ export {
1525
3067
  createInviteAcceptHandler,
1526
3068
  createSetPasswordHandler,
1527
3069
  createSettingsApiHandlers,
3070
+ createStorefrontApiHandler,
1528
3071
  createUploadHandler,
1529
3072
  createUserAuthApiRouter,
1530
3073
  createUserAvatarHandler,