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