@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/.gitattributes
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
with:
|
|
18
|
+
persist-credentials: true
|
|
19
|
+
|
|
20
|
+
- name: Skip version bump commits
|
|
21
|
+
run: |
|
|
22
|
+
if git log -1 --pretty=%B | grep -q "chore: bump version"; then
|
|
23
|
+
echo "Version bump commit detected, skipping."
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
- uses: actions/setup-node@v4
|
|
28
|
+
with:
|
|
29
|
+
node-version: 20
|
|
30
|
+
registry-url: https://registry.npmjs.org/
|
|
31
|
+
|
|
32
|
+
- run: npm ci
|
|
33
|
+
|
|
34
|
+
- run: |
|
|
35
|
+
git config user.name "github-actions"
|
|
36
|
+
git config user.email "actions@github.com"
|
|
37
|
+
npm version patch -m "chore: bump version %s"
|
|
38
|
+
git push
|
|
39
|
+
|
|
40
|
+
- run: npm publish --access public
|
|
41
|
+
env:
|
|
42
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
43
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# dSyncShop
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
------
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import dSyncShop from '@hackthedev/dsync-shop';
|
|
11
|
+
import dSyncSql from "@hackthedev/dsync-sql"
|
|
12
|
+
import dSyncPay from '@hackthedev/dsync-pay';
|
|
13
|
+
|
|
14
|
+
import express from "express";
|
|
15
|
+
|
|
16
|
+
const app = express();
|
|
17
|
+
const payments = new dSyncPay({...})
|
|
18
|
+
const db = new dSyncSql({...})
|
|
19
|
+
|
|
20
|
+
const shop = new dSyncShop({
|
|
21
|
+
app,
|
|
22
|
+
express,
|
|
23
|
+
payments, // dSyncPay instance
|
|
24
|
+
db, // dsync-sql instance
|
|
25
|
+
basePath: '/shop', // optional, default: '/shop'
|
|
26
|
+
|
|
27
|
+
isAdmin: async (req) => {
|
|
28
|
+
// return true or false
|
|
29
|
+
return req.headers['x-api-key'] === 'your-secret';
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
enrichMetadata: async (req) => {
|
|
33
|
+
// return an object that gets merged into payment metadata
|
|
34
|
+
// return null to reject the request (401)
|
|
35
|
+
const userId = req.headers['x-user-id'];
|
|
36
|
+
const token = req.headers['x-token'];
|
|
37
|
+
if (!userId || !token) return null;
|
|
38
|
+
return { userId, token };
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
productActions: {
|
|
42
|
+
'give_role': {
|
|
43
|
+
label: 'Give Role',
|
|
44
|
+
params: [
|
|
45
|
+
{ key: 'role', label: 'Role ID', type: 'text' }
|
|
46
|
+
],
|
|
47
|
+
handler: async (metadata, product, params) => {
|
|
48
|
+
// metadata contains everything from enrichMetadata + product_id
|
|
49
|
+
// params contains the action_params set on the product
|
|
50
|
+
await giveRole(metadata.userId, params.role);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
------
|
|
58
|
+
|
|
59
|
+
## Options
|
|
60
|
+
|
|
61
|
+
| option | type | required | description |
|
|
62
|
+
| -------------- | -------------- | -------- | ----------------------------------------------------- |
|
|
63
|
+
| app | object | yes | express app instance |
|
|
64
|
+
| express | object | yes | express module |
|
|
65
|
+
| payments | object | yes | dSyncPay instance |
|
|
66
|
+
| db | object | yes | dsync-sql instance |
|
|
67
|
+
| basePath | string | no | route prefix, default: '/shop' |
|
|
68
|
+
| isAdmin | async function | yes | called before admin routes, return true/false |
|
|
69
|
+
| enrichMetadata | async function | no | called before payment creation, return object or null |
|
|
70
|
+
| productActions | object | no | action definitions, see product actions section |
|
|
71
|
+
|
|
72
|
+
------
|
|
73
|
+
|
|
74
|
+
## enrichMetadata
|
|
75
|
+
|
|
76
|
+
Called on every `POST /shop/payment/create` request. Use it to attach user data to the payment metadata server-side. The returned object gets merged into the metadata that dSyncPay carries through the payment flow and delivers to your callbacks.
|
|
77
|
+
|
|
78
|
+
Return `null` to abort the request with a 401.
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
enrichMetadata: async (req) => {
|
|
82
|
+
const userId = req.headers['x-user-id'];
|
|
83
|
+
const token = req.headers['x-token'];
|
|
84
|
+
if (!userId || !token) return null;
|
|
85
|
+
|
|
86
|
+
const valid = await verifyToken(userId, token);
|
|
87
|
+
if (!valid) return null;
|
|
88
|
+
|
|
89
|
+
return { userId, token };
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The client sends the headers:
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
fetch('/shop/payment/create', {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: {
|
|
99
|
+
'Content-Type': 'application/json',
|
|
100
|
+
'x-user-id': '12345',
|
|
101
|
+
'x-token': 'your-token'
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify({ product_id: 1, payment_method: 'paypal' })
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
------
|
|
108
|
+
|
|
109
|
+
## Product Actions
|
|
110
|
+
|
|
111
|
+
Product actions let you define reusable logic that runs automatically after a successful purchase. Each action has a label, a list of params and a handler function.
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
productActions: {
|
|
115
|
+
'give_role': {
|
|
116
|
+
label: 'Give Role',
|
|
117
|
+
params: [
|
|
118
|
+
{ key: 'role', label: 'Role ID', type: 'text' }
|
|
119
|
+
],
|
|
120
|
+
handler: async (metadata, product, params) => {
|
|
121
|
+
await giveRole(metadata.userId, params.role);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
'remove_role': {
|
|
125
|
+
label: 'Remove Role',
|
|
126
|
+
params: [
|
|
127
|
+
{ key: 'role', label: 'Role ID', type: 'text' }
|
|
128
|
+
],
|
|
129
|
+
handler: async (metadata, product, params) => {
|
|
130
|
+
await removeRole(metadata.userId, params.role);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
'give_coins': {
|
|
134
|
+
label: 'Give Coins',
|
|
135
|
+
params: [
|
|
136
|
+
{ key: 'amount', label: 'Amount', type: 'number' }
|
|
137
|
+
],
|
|
138
|
+
handler: async (metadata, product, params) => {
|
|
139
|
+
await addCoins(metadata.userId, params.amount);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Param types
|
|
146
|
+
|
|
147
|
+
| type | description |
|
|
148
|
+
| ------ | ------------- |
|
|
149
|
+
| text | text input |
|
|
150
|
+
| number | numeric input |
|
|
151
|
+
|
|
152
|
+
### Shorthand
|
|
153
|
+
|
|
154
|
+
If an action needs no params, you can pass a plain function instead of an object:
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
productActions: {
|
|
158
|
+
'do_something': async (metadata, product, params) => {
|
|
159
|
+
// ...
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Setting action params on a product
|
|
165
|
+
|
|
166
|
+
When creating or updating a product you set which action it uses and what params to pass:
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
POST /shop/product/create
|
|
170
|
+
{
|
|
171
|
+
"name": "Premium Role",
|
|
172
|
+
"price": 9.99,
|
|
173
|
+
"action": "give_role",
|
|
174
|
+
"action_params": { "role": "123456789" }
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
------
|
|
179
|
+
|
|
180
|
+
## Routes
|
|
181
|
+
|
|
182
|
+
### Products
|
|
183
|
+
|
|
184
|
+
| method | route | auth | description |
|
|
185
|
+
| ------ | ----------------------------- | ------ | ------------------------------ |
|
|
186
|
+
| GET | /shop/products/list | public | list all active products |
|
|
187
|
+
| GET | /shop/products/list/:category | public | list products by category name |
|
|
188
|
+
| GET | /shop/product/:id | public | get single product |
|
|
189
|
+
| POST | /shop/product/create | admin | create product |
|
|
190
|
+
| POST | /shop/product/update/:id | admin | update product |
|
|
191
|
+
| DELETE | /shop/product/delete/:id | admin | delete product |
|
|
192
|
+
|
|
193
|
+
### Categories
|
|
194
|
+
|
|
195
|
+
| method | route | auth | description |
|
|
196
|
+
| ------ | ------------------------- | ------ | ------------------- |
|
|
197
|
+
| GET | /shop/categories/list | public | list all categories |
|
|
198
|
+
| POST | /shop/category/create | admin | create category |
|
|
199
|
+
| POST | /shop/category/update/:id | admin | update category |
|
|
200
|
+
| DELETE | /shop/category/delete/:id | admin | delete category |
|
|
201
|
+
|
|
202
|
+
### Actions
|
|
203
|
+
|
|
204
|
+
| method | route | auth | description |
|
|
205
|
+
| ------ | ------------------ | ----- | --------------------------------------- |
|
|
206
|
+
| GET | /shop/actions/list | admin | list all registered actions with params |
|
|
207
|
+
|
|
208
|
+
### Payments
|
|
209
|
+
|
|
210
|
+
| method | route | auth | description |
|
|
211
|
+
| ------ | -------------------- | -------------- | ------------------------------------------------- |
|
|
212
|
+
| POST | /shop/payment/create | enrichMetadata | create a paypal or coinbase payment for a product |
|
|
213
|
+
|
|
214
|
+
------
|
|
215
|
+
|
|
216
|
+
## Product Create / Update Body
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
{
|
|
220
|
+
name: 'Premium Role', // required
|
|
221
|
+
price: 9.99, // required
|
|
222
|
+
description: '...', // optional
|
|
223
|
+
category_id: 1, // optional
|
|
224
|
+
image_url: 'https://...', // optional
|
|
225
|
+
stock: 0, // optional, default: 0
|
|
226
|
+
active: 1, // optional, default: 1
|
|
227
|
+
action: 'give_role', // optional, must match a registered productAction key
|
|
228
|
+
action_params: { role: '...' } // optional, object, stored as json
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
------
|
|
233
|
+
|
|
234
|
+
## Payment Create Body
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
POST /shop/payment/create
|
|
238
|
+
|
|
239
|
+
{
|
|
240
|
+
product_id: 1,
|
|
241
|
+
payment_method: 'paypal' // or 'crypto'
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Response for paypal:
|
|
246
|
+
|
|
247
|
+
```js
|
|
248
|
+
{
|
|
249
|
+
error: null,
|
|
250
|
+
approvalUrl: 'https://paypal.com/...',
|
|
251
|
+
orderId: '...'
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Response for crypto:
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
{
|
|
259
|
+
error: null,
|
|
260
|
+
hostedUrl: 'https://commerce.coinbase.com/...',
|
|
261
|
+
chargeCode: '...'
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
------
|
|
266
|
+
|
|
267
|
+
## Database Tables
|
|
268
|
+
|
|
269
|
+
dSyncShop automatically creates and manages the following tables via `checkAndCreateTable`:
|
|
270
|
+
|
|
271
|
+
| table | description |
|
|
272
|
+
| ----------- | --------------------------------------------- |
|
|
273
|
+
| categories | product categories with optional parent |
|
|
274
|
+
| products | products with action and action_params fields |
|
|
275
|
+
| orders | completed, failed and cancelled orders |
|
|
276
|
+
| order_items | line items per order |
|
|
277
|
+
|
|
278
|
+
------
|
|
279
|
+
|
|
280
|
+
## isAdmin
|
|
281
|
+
|
|
282
|
+
Called before every admin route. If not provided, all admin routes are public. Return `true` to allow, `false` to respond with 403.
|
|
283
|
+
|
|
284
|
+
```js
|
|
285
|
+
isAdmin: async (req) => {
|
|
286
|
+
return req.headers['x-api-key'] === 'your-secret'; // or whatever
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
You can also use it as a standalone middleware in your own routes:
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
app.get('/something', shop.adminMiddleware(), (req, res) => {
|
|
294
|
+
res.json({ ok: true });
|
|
295
|
+
});
|
|
296
|
+
```
|