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