@hackthedev/dsync-shop 1.0.1
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/.gitattributes +2 -0
- package/.github/workflows/publish.yml +43 -0
- package/README.md +296 -0
- package/index.mjs +530 -0
- package/package.json +19 -0
- package/web/index.html +133 -0
- package/web/script.js +605 -0
- package/web/style.css +928 -0
package/index.mjs
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
|
|
4
|
+
export default class dSyncShop {
|
|
5
|
+
constructor({
|
|
6
|
+
app = null,
|
|
7
|
+
express = null,
|
|
8
|
+
payments = null,
|
|
9
|
+
db = null,
|
|
10
|
+
basePath = '/shop',
|
|
11
|
+
isAdmin = null,
|
|
12
|
+
enrichMetadata = null,
|
|
13
|
+
productActions = {},
|
|
14
|
+
} = {}) {
|
|
15
|
+
|
|
16
|
+
if(!app) throw new Error("missing express app instance");
|
|
17
|
+
if(!express) throw new Error("missing express");
|
|
18
|
+
if(!payments) throw new Error("missing payments");
|
|
19
|
+
if(!db) throw new Error("missing db");
|
|
20
|
+
|
|
21
|
+
this.app = app;
|
|
22
|
+
this.express = express;
|
|
23
|
+
this.payments = payments;
|
|
24
|
+
this.basePath = basePath;
|
|
25
|
+
this.db = db;
|
|
26
|
+
this.isAdmin = isAdmin;
|
|
27
|
+
this.enrichMetadata = enrichMetadata;
|
|
28
|
+
this.productActions = productActions;
|
|
29
|
+
|
|
30
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
31
|
+
const __dirname = path.dirname(__filename);
|
|
32
|
+
|
|
33
|
+
const staticDir = path.join(__dirname, "web");
|
|
34
|
+
|
|
35
|
+
if (this.payments) {
|
|
36
|
+
const originalCompleted = this.payments.callbacks.onPaymentCompleted;
|
|
37
|
+
const originalFailed = this.payments.callbacks.onPaymentFailed;
|
|
38
|
+
const originalCancelled = this.payments.callbacks.onPaymentCancelled;
|
|
39
|
+
|
|
40
|
+
this.payments.callbacks.onPaymentCompleted = async (data) => {
|
|
41
|
+
console.log('[dSyncShop] payment completed:', data);
|
|
42
|
+
if (originalCompleted) await originalCompleted(data);
|
|
43
|
+
await this.createOrder({ ...data, status: 'COMPLETED' });
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
this.payments.callbacks.onPaymentFailed = async (data) => {
|
|
47
|
+
console.log('[dSyncShop] payment failed:', data);
|
|
48
|
+
if (originalFailed) await originalFailed(data);
|
|
49
|
+
await this.createOrder({ ...data, status: 'FAILED' });
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
this.payments.callbacks.onPaymentCancelled = async (data) => {
|
|
53
|
+
console.log('[dSyncShop] payment cancelled:', data);
|
|
54
|
+
if (originalCancelled) await originalCancelled(data);
|
|
55
|
+
await this.createOrder({ ...data, status: 'CANCELLED' });
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
app.use(basePath, express.static(staticDir));
|
|
60
|
+
|
|
61
|
+
this.registerRoutes();
|
|
62
|
+
this.initDB();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
adminMiddleware() {
|
|
66
|
+
return async (req, res, next) => {
|
|
67
|
+
if (!this.isAdmin) return next();
|
|
68
|
+
const result = await this.isAdmin(req);
|
|
69
|
+
if (!result) return res.status(403).json({ error: 'forbidden' });
|
|
70
|
+
next();
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// normalize action - supports both shorthand function and full object definition
|
|
75
|
+
resolveAction(key) {
|
|
76
|
+
const action = this.productActions[key];
|
|
77
|
+
if (!action) return null;
|
|
78
|
+
if (typeof action === 'function') {
|
|
79
|
+
return { label: key, params: [], handler: action };
|
|
80
|
+
}
|
|
81
|
+
return action;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async createOrder(paymentData) {
|
|
85
|
+
const { metadata, pricing, amount, paymentId, status, provider, chargeId, orderId } = paymentData;
|
|
86
|
+
|
|
87
|
+
const finalAmount = amount || (pricing?.local?.amount ? parseFloat(pricing.local.amount) : 0);
|
|
88
|
+
|
|
89
|
+
let orderStatus = 'pending';
|
|
90
|
+
if (status === 'COMPLETED' || status === 'confirmed') {
|
|
91
|
+
orderStatus = 'completed';
|
|
92
|
+
} else if (status === 'failed' || status === 'FAILED') {
|
|
93
|
+
orderStatus = 'failed';
|
|
94
|
+
} else if (status === 'cancelled' || status === 'CANCELLED') {
|
|
95
|
+
orderStatus = 'cancelled';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!metadata || !metadata.product_id) {
|
|
99
|
+
console.error('[dSyncShop] no product_id in metadata:', paymentData);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const finalPaymentId = paymentId || orderId || chargeId || null;
|
|
104
|
+
|
|
105
|
+
const result = await this.db.queryDatabase(
|
|
106
|
+
"insert into orders (total_amount, status, payment_method, payment_id, customId) values (?, ?, ?, ?, ?)",
|
|
107
|
+
[finalAmount, orderStatus, provider || null, finalPaymentId, metadata?.userId || null]
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
await this.db.queryDatabase(
|
|
111
|
+
"insert into order_items (order_id, product_id, quantity, price) values (?, ?, ?, ?)",
|
|
112
|
+
[result.insertId, metadata.product_id, 1, finalAmount]
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (orderStatus === 'completed') {
|
|
116
|
+
try {
|
|
117
|
+
const products = await this.db.queryDatabase(
|
|
118
|
+
"select * from products where id = ?",
|
|
119
|
+
[metadata.product_id]
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const product = products[0];
|
|
123
|
+
|
|
124
|
+
if (product?.action) {
|
|
125
|
+
const action = this.resolveAction(product.action);
|
|
126
|
+
if (action) {
|
|
127
|
+
const params = product.action_params ? JSON.parse(product.action_params) : {};
|
|
128
|
+
await action.handler(metadata, product, params);
|
|
129
|
+
} else {
|
|
130
|
+
console.warn(`[dSyncShop] action '${product.action}' not found in productActions`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error('[dSyncShop] error executing product action:', err);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async initDB() {
|
|
142
|
+
const shopTables = [
|
|
143
|
+
{
|
|
144
|
+
name: "categories",
|
|
145
|
+
columns: [
|
|
146
|
+
{name: "id", type: "int(12) NOT NULL AUTO_INCREMENT PRIMARY KEY"},
|
|
147
|
+
{name: "name", type: "varchar(255) NOT NULL"},
|
|
148
|
+
{name: "description", type: "text"},
|
|
149
|
+
{name: "parent_id", type: "int(12)"},
|
|
150
|
+
{name: "created_at", type: "bigint NOT NULL DEFAULT (UNIX_TIMESTAMP() * 1000)"}
|
|
151
|
+
],
|
|
152
|
+
keys: [
|
|
153
|
+
{name: "UNIQUE KEY", type: "name (name)"},
|
|
154
|
+
{name: "FOREIGN KEY", type: "(parent_id) REFERENCES categories(id) ON DELETE SET NULL"}
|
|
155
|
+
]
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "products",
|
|
159
|
+
columns: [
|
|
160
|
+
{name: "id", type: "int(12) NOT NULL AUTO_INCREMENT PRIMARY KEY"},
|
|
161
|
+
{name: "name", type: "varchar(255) NOT NULL"},
|
|
162
|
+
{name: "description", type: "text"},
|
|
163
|
+
{name: "price", type: "decimal(10,2) NOT NULL"},
|
|
164
|
+
{name: "category_id", type: "int(12)"},
|
|
165
|
+
{name: "image_url", type: "varchar(512)"},
|
|
166
|
+
{name: "stock", type: "int(12) NOT NULL DEFAULT 0"},
|
|
167
|
+
{name: "active", type: "tinyint(1) NOT NULL DEFAULT 1"},
|
|
168
|
+
{name: "action", type: "varchar(100) DEFAULT NULL"},
|
|
169
|
+
{name: "action_params", type: "text DEFAULT NULL"},
|
|
170
|
+
{name: "created_at", type: "bigint NOT NULL DEFAULT (UNIX_TIMESTAMP() * 1000)"}
|
|
171
|
+
],
|
|
172
|
+
keys: [
|
|
173
|
+
{name: "INDEX", type: "category_id (category_id)"},
|
|
174
|
+
{name: "INDEX", type: "active (active)"},
|
|
175
|
+
{name: "FOREIGN KEY", type: "(category_id) REFERENCES categories(id) ON DELETE SET NULL"}
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "orders",
|
|
180
|
+
columns: [
|
|
181
|
+
{name: "id", type: "int(12) NOT NULL AUTO_INCREMENT PRIMARY KEY"},
|
|
182
|
+
{name: "customer_email", type: "varchar(255) DEFAULT NULL"},
|
|
183
|
+
{name: "customer_name", type: "varchar(255) DEFAULT NULL"},
|
|
184
|
+
{name: "customId", type: "varchar(255) DEFAULT NULL"},
|
|
185
|
+
{name: "total_amount", type: "decimal(10,2) NOT NULL"},
|
|
186
|
+
{name: "status", type: "varchar(50) NOT NULL DEFAULT 'pending'"},
|
|
187
|
+
{name: "payment_method", type: "varchar(50)"},
|
|
188
|
+
{name: "payment_id", type: "varchar(255)"},
|
|
189
|
+
{name: "created_at", type: "bigint NOT NULL DEFAULT (UNIX_TIMESTAMP() * 1000)"}
|
|
190
|
+
],
|
|
191
|
+
keys: [
|
|
192
|
+
{name: "INDEX", type: "customer_email (customer_email)"},
|
|
193
|
+
{name: "INDEX", type: "status (status)"},
|
|
194
|
+
{name: "INDEX", type: "payment_id (payment_id)"}
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "order_items",
|
|
199
|
+
columns: [
|
|
200
|
+
{name: "id", type: "int(12) NOT NULL AUTO_INCREMENT PRIMARY KEY"},
|
|
201
|
+
{name: "order_id", type: "int(12) NOT NULL"},
|
|
202
|
+
{name: "product_id", type: "int(12) NOT NULL"},
|
|
203
|
+
{name: "quantity", type: "int(12) NOT NULL"},
|
|
204
|
+
{name: "price", type: "decimal(10,2) NOT NULL"},
|
|
205
|
+
{name: "created_at", type: "bigint NOT NULL DEFAULT (UNIX_TIMESTAMP() * 1000)"}
|
|
206
|
+
],
|
|
207
|
+
keys: [
|
|
208
|
+
{name: "INDEX", type: "order_id (order_id)"},
|
|
209
|
+
{name: "INDEX", type: "product_id (product_id)"},
|
|
210
|
+
{name: "FOREIGN KEY", type: "(order_id) REFERENCES orders(id) ON DELETE CASCADE"},
|
|
211
|
+
{name: "FOREIGN KEY", type: "(product_id) REFERENCES products(id) ON DELETE CASCADE"}
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
for (const table of shopTables) {
|
|
217
|
+
await this.db.checkAndCreateTable(table);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
registerRoutes() {
|
|
222
|
+
this.createProductRoutes();
|
|
223
|
+
this.createCategoryRoutes();
|
|
224
|
+
this.createPaymentRoute();
|
|
225
|
+
this.createActionRoutes();
|
|
226
|
+
|
|
227
|
+
this.app.get(`${this.basePath}/admin/check`, async (req, res) => {
|
|
228
|
+
if (!this.isAdmin) return res.status(200).json({ isAdmin: false });
|
|
229
|
+
const result = await this.isAdmin(req);
|
|
230
|
+
return res.status(200).json({ isAdmin: !!result });
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
createActionRoutes() {
|
|
235
|
+
this.app.get(`${this.basePath}/actions/list`, this.adminMiddleware(), (req, res) => {
|
|
236
|
+
const actions = Object.keys(this.productActions).map(key => {
|
|
237
|
+
const action = this.resolveAction(key);
|
|
238
|
+
return {
|
|
239
|
+
key,
|
|
240
|
+
label: action.label || key,
|
|
241
|
+
params: action.params || []
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
return res.status(200).json({ error: null, actions });
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
createPaymentRoute() {
|
|
249
|
+
this.app.post(`${this.basePath}/payment/create`, this.express.json(), async (req, res) => {
|
|
250
|
+
try {
|
|
251
|
+
const { product_id, payment_method } = req.body;
|
|
252
|
+
const extra = this.enrichMetadata ? await this.enrichMetadata(req) : {};
|
|
253
|
+
if (extra === null) return res.status(401).json({ error: 'unauthorized' });
|
|
254
|
+
|
|
255
|
+
const products = await this.db.queryDatabase(
|
|
256
|
+
"select * from products where id = ?",
|
|
257
|
+
[product_id]
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
if (products.length === 0) {
|
|
261
|
+
return res.status(404).json({ error: "product not found" });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const product = products[0];
|
|
265
|
+
|
|
266
|
+
if (payment_method === 'paypal') {
|
|
267
|
+
const order = await this.payments.paypal.createOrder({
|
|
268
|
+
title: product.name,
|
|
269
|
+
price: parseFloat(product.price),
|
|
270
|
+
metadata: { product_id: product.id, ...extra }
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return res.status(200).json({
|
|
274
|
+
error: null,
|
|
275
|
+
approvalUrl: order.approvalUrl,
|
|
276
|
+
orderId: order.orderId
|
|
277
|
+
});
|
|
278
|
+
} else if (payment_method === 'crypto') {
|
|
279
|
+
const charge = await this.payments.coinbase.createCharge({
|
|
280
|
+
title: product.name,
|
|
281
|
+
price: parseFloat(product.price),
|
|
282
|
+
metadata: { product_id: product.id, ...extra }
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
return res.status(200).json({
|
|
286
|
+
error: null,
|
|
287
|
+
hostedUrl: charge.hostedUrl,
|
|
288
|
+
chargeCode: charge.chargeCode
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
return res.status(500).json({ error: error.message });
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
createProductRoutes() {
|
|
298
|
+
this.app.get(`${this.basePath}/products/list`, async (req, res) => {
|
|
299
|
+
try {
|
|
300
|
+
const products = await this.db.queryDatabase(
|
|
301
|
+
"select p.*, c.name as category_name from products p left join categories c on p.category_id = c.id where p.active = ? order by p.created_at desc",
|
|
302
|
+
[1]
|
|
303
|
+
);
|
|
304
|
+
return res.status(200).json({ error: null, products });
|
|
305
|
+
} catch (error) {
|
|
306
|
+
return res.status(500).json({ error: error.message, products: [] });
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
this.app.get(`${this.basePath}/products/list/:category`, async (req, res) => {
|
|
311
|
+
try {
|
|
312
|
+
const { category } = req.params;
|
|
313
|
+
const products = await this.db.queryDatabase(
|
|
314
|
+
"select p.*, c.name as category_name from products p left join categories c on p.category_id = c.id where p.active = ? and c.name = ? order by p.created_at desc",
|
|
315
|
+
[1, category]
|
|
316
|
+
);
|
|
317
|
+
return res.status(200).json({ error: null, products });
|
|
318
|
+
} catch (error) {
|
|
319
|
+
return res.status(500).json({ error: error.message, products: [] });
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
this.app.get(`${this.basePath}/product/:id`, async (req, res) => {
|
|
324
|
+
try {
|
|
325
|
+
const { id } = req.params;
|
|
326
|
+
const products = await this.db.queryDatabase(
|
|
327
|
+
"select p.*, c.name as category_name from products p left join categories c on p.category_id = c.id where p.id = ?",
|
|
328
|
+
[id]
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
if (products.length === 0) {
|
|
332
|
+
return res.status(404).json({ error: "product not found", product: null });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return res.status(200).json({ error: null, product: products[0] });
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return res.status(500).json({ error: error.message, product: null });
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
this.app.post(`${this.basePath}/product/create`, this.express.json(), this.adminMiddleware(), async (req, res) => {
|
|
342
|
+
try {
|
|
343
|
+
const { name, description, price, category_id, image_url, stock, active, action, action_params } = req.body;
|
|
344
|
+
|
|
345
|
+
if (!name || !price) {
|
|
346
|
+
return res.status(400).json({ error: "name and price are required", product: null });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (action && !this.resolveAction(action)) {
|
|
350
|
+
return res.status(400).json({ error: `unknown action '${action}'`, product: null });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const result = await this.db.queryDatabase(
|
|
354
|
+
"insert into products (name, description, price, category_id, image_url, stock, active, action, action_params) values (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
355
|
+
[
|
|
356
|
+
name,
|
|
357
|
+
description || null,
|
|
358
|
+
price,
|
|
359
|
+
category_id || null,
|
|
360
|
+
image_url || null,
|
|
361
|
+
stock || 0,
|
|
362
|
+
active !== undefined ? active : 1,
|
|
363
|
+
action || null,
|
|
364
|
+
action_params ? JSON.stringify(action_params) : null
|
|
365
|
+
]
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const products = await this.db.queryDatabase(
|
|
369
|
+
"select * from products where id = ?",
|
|
370
|
+
[result.insertId]
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
return res.status(201).json({ error: null, product: products[0] });
|
|
374
|
+
} catch (error) {
|
|
375
|
+
return res.status(500).json({ error: error.message, product: null });
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
this.app.post(`${this.basePath}/product/update/:id`, this.express.json(), this.adminMiddleware(), async (req, res) => {
|
|
380
|
+
try {
|
|
381
|
+
const { id } = req.params;
|
|
382
|
+
const { name, description, price, category_id, image_url, stock, active, action, action_params } = req.body;
|
|
383
|
+
|
|
384
|
+
if (action && !this.resolveAction(action)) {
|
|
385
|
+
return res.status(400).json({ error: `unknown action '${action}'`, product: null });
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
await this.db.queryDatabase(
|
|
389
|
+
"update products set name = ?, description = ?, price = ?, category_id = ?, image_url = ?, stock = ?, active = ?, action = ?, action_params = ? where id = ?",
|
|
390
|
+
[
|
|
391
|
+
name,
|
|
392
|
+
description,
|
|
393
|
+
price,
|
|
394
|
+
category_id,
|
|
395
|
+
image_url,
|
|
396
|
+
stock,
|
|
397
|
+
active,
|
|
398
|
+
action || null,
|
|
399
|
+
action_params ? JSON.stringify(action_params) : null,
|
|
400
|
+
id
|
|
401
|
+
]
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
const products = await this.db.queryDatabase(
|
|
405
|
+
"select * from products where id = ?",
|
|
406
|
+
[id]
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
if (products.length === 0) {
|
|
410
|
+
return res.status(404).json({ error: "product not found", product: null });
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return res.status(200).json({ error: null, product: products[0] });
|
|
414
|
+
} catch (error) {
|
|
415
|
+
return res.status(500).json({ error: error.message, product: null });
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
this.app.delete(`${this.basePath}/product/delete/:id`, this.adminMiddleware(), async (req, res) => {
|
|
420
|
+
try {
|
|
421
|
+
const { id } = req.params;
|
|
422
|
+
|
|
423
|
+
const products = await this.db.queryDatabase(
|
|
424
|
+
"select * from products where id = ?",
|
|
425
|
+
[id]
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
if (products.length === 0) {
|
|
429
|
+
return res.status(404).json({ error: "product not found", success: false });
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
await this.db.queryDatabase(
|
|
433
|
+
"delete from products where id = ?",
|
|
434
|
+
[id]
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
return res.status(200).json({ error: null, success: true });
|
|
438
|
+
} catch (error) {
|
|
439
|
+
return res.status(500).json({ error: error.message, success: false });
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
createCategoryRoutes() {
|
|
445
|
+
this.app.get(`${this.basePath}/categories/list`, async (req, res) => {
|
|
446
|
+
try {
|
|
447
|
+
const categories = await this.db.queryDatabase(
|
|
448
|
+
"select * from categories order by name",
|
|
449
|
+
[]
|
|
450
|
+
);
|
|
451
|
+
return res.status(200).json({ error: null, categories });
|
|
452
|
+
} catch (error) {
|
|
453
|
+
return res.status(500).json({ error: error.message, categories: [] });
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
this.app.post(`${this.basePath}/category/create`, this.express.json(), this.adminMiddleware(), async (req, res) => {
|
|
458
|
+
try {
|
|
459
|
+
const { name, description, parent_id } = req.body;
|
|
460
|
+
|
|
461
|
+
if (!name) {
|
|
462
|
+
return res.status(400).json({ error: "name is required", category: null });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const result = await this.db.queryDatabase(
|
|
466
|
+
"insert into categories (name, description, parent_id) values (?, ?, ?)",
|
|
467
|
+
[name, description || null, parent_id || null]
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const categories = await this.db.queryDatabase(
|
|
471
|
+
"select * from categories where id = ?",
|
|
472
|
+
[result.insertId]
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
return res.status(201).json({ error: null, category: categories[0] });
|
|
476
|
+
} catch (error) {
|
|
477
|
+
return res.status(500).json({ error: error.message, category: null });
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
this.app.post(`${this.basePath}/category/update/:id`, this.express.json(), this.adminMiddleware(), async (req, res) => {
|
|
482
|
+
try {
|
|
483
|
+
const { id } = req.params;
|
|
484
|
+
const { name, description, parent_id } = req.body;
|
|
485
|
+
|
|
486
|
+
await this.db.queryDatabase(
|
|
487
|
+
"update categories set name = ?, description = ?, parent_id = ? where id = ?",
|
|
488
|
+
[name, description, parent_id, id]
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
const categories = await this.db.queryDatabase(
|
|
492
|
+
"select * from categories where id = ?",
|
|
493
|
+
[id]
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
if (categories.length === 0) {
|
|
497
|
+
return res.status(404).json({ error: "category not found", category: null });
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return res.status(200).json({ error: null, category: categories[0] });
|
|
501
|
+
} catch (error) {
|
|
502
|
+
return res.status(500).json({ error: error.message, category: null });
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
this.app.delete(`${this.basePath}/category/delete/:id`, this.adminMiddleware(), async (req, res) => {
|
|
507
|
+
try {
|
|
508
|
+
const { id } = req.params;
|
|
509
|
+
|
|
510
|
+
const categories = await this.db.queryDatabase(
|
|
511
|
+
"select * from categories where id = ?",
|
|
512
|
+
[id]
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
if (categories.length === 0) {
|
|
516
|
+
return res.status(404).json({ error: "category not found", success: false });
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
await this.db.queryDatabase(
|
|
520
|
+
"delete from categories where id = ?",
|
|
521
|
+
[id]
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
return res.status(200).json({ error: null, success: true });
|
|
525
|
+
} catch (error) {
|
|
526
|
+
return res.status(500).json({ error: error.message, success: false });
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hackthedev/dsync-shop",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Part of the dSync library family. dSyncShop provides a complete shop system on top of dSyncPay, handling products, categories, orders and automatic post-purchase actions. It creates and manages its own database tables and registers all routes automatically.",
|
|
5
|
+
"homepage": "https://github.com/NETWORK-Z-Dev/dSyncShop#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/NETWORK-Z-Dev/dSyncShop/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/NETWORK-Z-Dev/dSyncShop.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"author": "",
|
|
15
|
+
"main": "index.mjs",
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/web/index.html
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
|
|
8
|
+
<link rel="stylesheet" href="style.css">
|
|
9
|
+
<script defer src="script.js"></script>
|
|
10
|
+
|
|
11
|
+
<title>Shop</title>
|
|
12
|
+
</head>
|
|
13
|
+
|
|
14
|
+
<body>
|
|
15
|
+
<div class="admin-overlay" id="adminOverlay" onclick="closeAdminPanel()"></div>
|
|
16
|
+
|
|
17
|
+
<div class="sidebar" id="mainSidebar">
|
|
18
|
+
<div class="sidebar-top">
|
|
19
|
+
<button class="sidebar-back-btn" onclick="window.location.href = window.location.origin">
|
|
20
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
21
|
+
<polyline points="15 18 9 12 15 6"/>
|
|
22
|
+
</svg>
|
|
23
|
+
Back
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="sidebar-section-label">categories</div>
|
|
28
|
+
<div class="category-nav" id="categoryNav">
|
|
29
|
+
<button class="category-btn active" data-category="all">all products</button>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="sidebar-bottom">
|
|
33
|
+
<button class="sidebar-admin-btn" id="adminBtn" onclick="openAdminPanel()" style="display:none">
|
|
34
|
+
Admin Panel
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="main-content" id="mainContent">
|
|
40
|
+
<header>
|
|
41
|
+
<div class="header-inner">
|
|
42
|
+
<h1>Shop</h1>
|
|
43
|
+
<p class="header-sub">Browse different products</p>
|
|
44
|
+
</div>
|
|
45
|
+
</header>
|
|
46
|
+
<div class="container">
|
|
47
|
+
<div class="products" id="productsContainer">
|
|
48
|
+
<div class="loading">loading products...</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="modal" id="paymentModal">
|
|
54
|
+
<div class="modal-content">
|
|
55
|
+
<div class="modal-header">confirm purchase</div>
|
|
56
|
+
<div class="modal-body">
|
|
57
|
+
<p><strong>product:</strong> <span id="modalProduct"></span></p>
|
|
58
|
+
<p><strong>price:</strong> <span id="modalPrice"></span></p>
|
|
59
|
+
<p><strong>payment method:</strong> <span id="modalPayment"></span></p>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="modal-footer">
|
|
62
|
+
<button class="btn-modal btn-cancel" onclick="closeModal()">cancel</button>
|
|
63
|
+
<button class="btn-modal btn-confirm" onclick="processPurchase()">confirm</button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div class="modal" id="formModal">
|
|
69
|
+
<div class="modal-content">
|
|
70
|
+
<div class="modal-header" id="formModalTitle">add product</div>
|
|
71
|
+
<div class="modal-body" id="formModalBody"></div>
|
|
72
|
+
<div class="modal-footer">
|
|
73
|
+
<button class="btn-modal btn-cancel" onclick="closeFormModal()">cancel</button>
|
|
74
|
+
<button class="btn-modal btn-confirm" onclick="submitForm()">save</button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div class="admin-panel" id="adminPanel">
|
|
80
|
+
|
|
81
|
+
<div class="admin-topbar">
|
|
82
|
+
<div class="admin-topbar-left">
|
|
83
|
+
<h1>Admin Panel</h1>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="admin-topbar-right">
|
|
86
|
+
<button class="admin-close-btn" onclick="closeAdminPanel()">✕</button>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="admin-tabs">
|
|
91
|
+
<button class="tab-btn active" onclick="switchTab('products', event)">products</button>
|
|
92
|
+
<button class="tab-btn" onclick="switchTab('categories', event)">categories</button>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div class="tab-content active" id="productsTab">
|
|
96
|
+
<div class="tab-toolbar">
|
|
97
|
+
<div class="search-input-wrap">
|
|
98
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
99
|
+
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
100
|
+
</svg>
|
|
101
|
+
<input type="text" id="adminProductSearch" placeholder="search products...">
|
|
102
|
+
</div>
|
|
103
|
+
<div class="tab-toolbar-right">
|
|
104
|
+
<button class="btn-add" onclick="openProductForm()">
|
|
105
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
106
|
+
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
|
107
|
+
</svg>
|
|
108
|
+
add product
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="admin-list" id="productsList"></div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="tab-content" id="categoriesTab">
|
|
116
|
+
<div class="tab-toolbar">
|
|
117
|
+
<div class="tab-toolbar-right">
|
|
118
|
+
<button class="btn-add" onclick="openCategoryForm()">
|
|
119
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
120
|
+
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
|
121
|
+
</svg>
|
|
122
|
+
add category
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="admin-list" id="categoriesList"></div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
</body>
|
|
132
|
+
|
|
133
|
+
</html>
|