@nka212bg/backend-utils 0.1.60 → 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/location/index.js CHANGED
@@ -5,21 +5,24 @@ const { locales, timeZones } = require(`./localelib`);
5
5
  const Reader = require("@maxmind/geoip2-node").Reader;
6
6
  const GeoDB = Reader.openBuffer(readFileSync(`${__dirname}/GeoLite2-City.mmdb`));
7
7
 
8
- exports.userIp = (req) => {
8
+ // x-forwarded-for is client-controlled, trust it only behind a reverse proxy (nginx, Cloudflare...)
9
+ exports.userIp = (req, { trustProxy = false } = {}) => {
9
10
  if (!req) throw new Error("Request object is missing, can't work like that! That's absurd!!!");
10
11
 
11
- let ip = (req.headers["x-forwarded-for"] || req.socket?.remoteAddress || req.connection?.remoteAddress || req.ip)?.split(",")[0].replace(/::ffff:/gi, "");
12
+ let ip = ((trustProxy && req.headers["x-forwarded-for"]) || req.socket?.remoteAddress || req.connection?.remoteAddress || req.ip)
13
+ ?.split(",")[0]
14
+ .trim()
15
+ .replace(/::ffff:/gi, "");
12
16
 
13
- // if (["127.0.0.1", "::1"].includes(ip)) return "146.70.185.8";
14
17
  return ip;
15
18
  };
16
19
 
17
20
  // user geo
18
- exports.location = ({ req, ip }) => {
21
+ exports.location = ({ req, ip, trustProxy }) => {
19
22
  let userIP;
20
23
  try {
21
24
  if (!ip && !req) throw "missing_required";
22
- userIP = ip || this.userIp(req);
25
+ userIP = ip || this.userIp(req, { trustProxy });
23
26
  const geo = GeoDB.city(userIP);
24
27
 
25
28
  const country = locales[geo.country.isoCode];
package/package.json CHANGED
@@ -1,19 +1,20 @@
1
1
  {
2
2
  "name": "@nka212bg/backend-utils",
3
3
  "author": "nka212bg",
4
- "version": "0.1.60",
4
+ "version": "0.1.62",
5
5
  "main": "index.js",
6
6
  "dependencies": {
7
7
  "@maxmind/geoip2-node": "^5.0.0",
8
- "systeminformation": "^5.21.24",
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());