@nka212bg/backend-utils 0.1.61 → 0.1.62
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/package.json +7 -6
- package/socket.js +0 -57
- package/webPush.js +118 -0
package/package.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nka212bg/backend-utils",
|
|
3
3
|
"author": "nka212bg",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.62",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@maxmind/geoip2-node": "^5.0.0",
|
|
8
|
-
"
|
|
8
|
+
"adm-zip": "^0.5.16",
|
|
9
9
|
"formidable": "^2.1.5",
|
|
10
|
+
"mysql2": "^3.11.0",
|
|
10
11
|
"nodemailer": "^8.0.8",
|
|
11
|
-
"adm-zip": "^0.5.16",
|
|
12
12
|
"os-utils": "^0.0.14",
|
|
13
|
-
"stripe": "^14.23.0",
|
|
14
|
-
"mysql2": "^3.11.0",
|
|
15
|
-
"sqlite3": "^6.0.1",
|
|
16
13
|
"sharp": "^0.33.5",
|
|
14
|
+
"sqlite3": "^6.0.1",
|
|
15
|
+
"stripe": "^14.23.0",
|
|
16
|
+
"systeminformation": "^5.21.24",
|
|
17
|
+
"web-push": "^3.6.7",
|
|
17
18
|
"ws": "^7.3.0"
|
|
18
19
|
}
|
|
19
20
|
}
|
package/socket.js
CHANGED
|
@@ -115,60 +115,3 @@ module.exports = (server, { authenticate } = {}) => {
|
|
|
115
115
|
return socket;
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
////////////////////////////////////////
|
|
123
|
-
|
|
124
|
-
// "web-push": "^3.2.2", // package.json
|
|
125
|
-
// const webPush = require("web-push");
|
|
126
|
-
// const VAPID_KEYS = webPush.generateVAPIDKeys();
|
|
127
|
-
|
|
128
|
-
// webPush.setVapidDetails(process.env.VAPID_ORIGIN, process.env.VAPID_PUBLIC_KEY, process.env.VAPID_PRIVATE_KEY);
|
|
129
|
-
|
|
130
|
-
// module.exports = (app) => {
|
|
131
|
-
// app.get("vapidPublicKey", function (req, res) {
|
|
132
|
-
// res.send(process.env.VAPID_PUBLIC_KEY);
|
|
133
|
-
// });
|
|
134
|
-
|
|
135
|
-
// app.post("register", function (req, res) {
|
|
136
|
-
// // A real world application would store the subscription info.
|
|
137
|
-
// res.sendStatus(201);
|
|
138
|
-
// });
|
|
139
|
-
|
|
140
|
-
// app.post("sendNotification", function (req, res) {
|
|
141
|
-
// const subscription = req.body.subscription;
|
|
142
|
-
// const payload = req.body.payload;
|
|
143
|
-
// const options = {
|
|
144
|
-
// TTL: req.body.ttl,
|
|
145
|
-
// };
|
|
146
|
-
|
|
147
|
-
// setTimeout(function () {
|
|
148
|
-
// webPush
|
|
149
|
-
// .sendNotification(subscription, payload, options)
|
|
150
|
-
// .then(function () {
|
|
151
|
-
// res.sendStatus(201);
|
|
152
|
-
// })
|
|
153
|
-
// .catch(function (error) {
|
|
154
|
-
// console.log(error);
|
|
155
|
-
// res.sendStatus(500);
|
|
156
|
-
// });
|
|
157
|
-
// }, req.body.delay * 1000);
|
|
158
|
-
|
|
159
|
-
// fetch("./sendNotification", {
|
|
160
|
-
// method: "post",
|
|
161
|
-
// headers: {
|
|
162
|
-
// "Content-type": "application/json",
|
|
163
|
-
// },
|
|
164
|
-
// body: JSON.stringify({
|
|
165
|
-
// subscription: subscription,
|
|
166
|
-
// payload: payload,
|
|
167
|
-
// delay: delay,
|
|
168
|
-
// ttl: ttl,
|
|
169
|
-
// }),
|
|
170
|
-
// });
|
|
171
|
-
// });
|
|
172
|
-
|
|
173
|
-
// return app;
|
|
174
|
-
// };
|
package/webPush.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const { existsSync, readFileSync, writeFileSync, mkdirSync } = require("fs");
|
|
2
|
+
const webPush = require("web-push");
|
|
3
|
+
|
|
4
|
+
!existsSync(`${__basedir}/state`) && mkdirSync(`${__basedir}/state`, { recursive: true });
|
|
5
|
+
const subscriptionsFilePath = `${__basedir}/state/pushSubscriptions.json`;
|
|
6
|
+
|
|
7
|
+
// { "<userId>": { "<endpoint>": subscription } }
|
|
8
|
+
let SUBSCRIPTIONS = existsSync(subscriptionsFilePath) ? JSON.parse(readFileSync(subscriptionsFilePath, "utf8") || "{}") : {};
|
|
9
|
+
|
|
10
|
+
const val = (v) => (typeof v == "function" ? v() : v);
|
|
11
|
+
const toSubscription = (s) => (typeof s == "string" ? JSON.parse(s) : s);
|
|
12
|
+
|
|
13
|
+
// sync on purpose, overlapping async writeFile calls on the same file can corrupt it
|
|
14
|
+
const saveSubscriptions = () => writeFileSync(subscriptionsFilePath, JSON.stringify(SUBSCRIPTIONS));
|
|
15
|
+
|
|
16
|
+
// -------- user's devices --------
|
|
17
|
+
// shared by all instances, it's one file on disk
|
|
18
|
+
const subscriptionState = {
|
|
19
|
+
is(userId) {
|
|
20
|
+
return Object.values(SUBSCRIPTIONS[userId] || {});
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
// an endpoint belongs to one user only (same browser, different account)
|
|
24
|
+
subscribe(userId, subscription) {
|
|
25
|
+
subscription = toSubscription(subscription);
|
|
26
|
+
if (!userId || !subscription?.endpoint) return;
|
|
27
|
+
|
|
28
|
+
removeEndpoint(subscription.endpoint);
|
|
29
|
+
(SUBSCRIPTIONS[userId] ||= {})[subscription.endpoint] = subscription;
|
|
30
|
+
saveSubscriptions();
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
unsubscribe(endpoint) {
|
|
34
|
+
if (removeEndpoint(endpoint)) saveSubscriptions();
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const removeEndpoint = (endpoint) => {
|
|
39
|
+
let change;
|
|
40
|
+
Object.keys(SUBSCRIPTIONS).forEach((userId) => {
|
|
41
|
+
if (!(endpoint in SUBSCRIPTIONS[userId])) return;
|
|
42
|
+
|
|
43
|
+
delete SUBSCRIPTIONS[userId][endpoint];
|
|
44
|
+
if (!Object.keys(SUBSCRIPTIONS[userId]).length) delete SUBSCRIPTIONS[userId];
|
|
45
|
+
change = true;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return change;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// the subject must be an https: URL or a mailto:, so anything else (http local dev) falls back to the smtp user's email
|
|
52
|
+
const smtpUser = () => (process.env.SMTP_URL ? decodeURIComponent(new URL(process.env.SMTP_URL).username) : process.env.SMTP_USER);
|
|
53
|
+
const vapidSubject = (subject) => (/^(https|mailto):/.test(subject || "") ? subject : `mailto:${smtpUser()}`);
|
|
54
|
+
|
|
55
|
+
// every call creates a separate instance with its own config:
|
|
56
|
+
// const { sendPush, subscriptionState, publicKey } = require(".../webPush")({ subject: () => __baseUrl, options: { TTL: 86400, urgency: "high" } }); // publicKey()
|
|
57
|
+
// subscriptionState.subscribe(userId, subscription); sendPush({ userId, payload: { title, body } });
|
|
58
|
+
// const { sendPush } = require(".../webPush"); // default instance, env config only
|
|
59
|
+
//
|
|
60
|
+
// empty values fall back to process.env (VAPID_SUBJECT, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY)
|
|
61
|
+
// subject, publicKey, privateKey, options can also be functions, resolved on each send (e.g. subject: () => __baseUrl)
|
|
62
|
+
const createInstance = (opts = {}) => {
|
|
63
|
+
const config = {
|
|
64
|
+
subject: "",
|
|
65
|
+
publicKey: "",
|
|
66
|
+
privateKey: "",
|
|
67
|
+
options: {}, // web-push sendNotification options { TTL, urgency, topic… }
|
|
68
|
+
...opts,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const publicKey = () => val(config.publicKey) || process.env.VAPID_PUBLIC_KEY;
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
subscriptionState,
|
|
75
|
+
|
|
76
|
+
// for the client's pushManager.subscribe({ applicationServerKey })
|
|
77
|
+
publicKey,
|
|
78
|
+
|
|
79
|
+
// userId: sends to the user's stored subscriptions and deletes the expired ones
|
|
80
|
+
// subscriptions: or pass them directly, one or an array, objects or JSON strings
|
|
81
|
+
// options are merged over the instance options
|
|
82
|
+
// "data" means the push service accepted it, not that the device showed it (offline devices get it later, within TTL)
|
|
83
|
+
// "expired" (404/410) subscriptions are dead, delete them if you passed them directly
|
|
84
|
+
async sendPush({ userId, subscriptions, payload, options } = {}) {
|
|
85
|
+
if (userId) subscriptions = subscriptionState.is(userId);
|
|
86
|
+
if (!Array.isArray(subscriptions)) subscriptions = subscriptions ? [subscriptions] : [];
|
|
87
|
+
if (!subscriptions.length) return { error: "missing_subscriptions", data: [] };
|
|
88
|
+
if (!payload) return { error: "missing_payload", data: [] };
|
|
89
|
+
|
|
90
|
+
subscriptions = subscriptions.map(toSubscription);
|
|
91
|
+
if (typeof payload != "string") payload = JSON.stringify(payload);
|
|
92
|
+
|
|
93
|
+
const vapidDetails = {
|
|
94
|
+
subject: vapidSubject(val(config.subject) || process.env.VAPID_SUBJECT),
|
|
95
|
+
publicKey: publicKey(),
|
|
96
|
+
privateKey: val(config.privateKey) || process.env.VAPID_PRIVATE_KEY,
|
|
97
|
+
};
|
|
98
|
+
options = { ...val(config.options), ...options, vapidDetails };
|
|
99
|
+
|
|
100
|
+
const results = await Promise.allSettled(subscriptions.map((s) => webPush.sendNotification(s, payload, options)));
|
|
101
|
+
|
|
102
|
+
const data = [];
|
|
103
|
+
const expired = [];
|
|
104
|
+
const errors = [];
|
|
105
|
+
results.forEach((r, i) => {
|
|
106
|
+
if (r.status == "fulfilled") return data.push(subscriptions[i]);
|
|
107
|
+
[404, 410].includes(r.reason.statusCode) ? expired.push(subscriptions[i]) : errors.push(r.reason);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (userId && expired.length && expired.map((s) => removeEndpoint(s.endpoint)).some(Boolean)) saveSubscriptions();
|
|
111
|
+
|
|
112
|
+
return { error: errors.length ? errors : null, expired: expired.length ? expired : null, data };
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// callable for a configured instance, plain require gives the default one
|
|
118
|
+
module.exports = Object.assign(createInstance, createInstance());
|