diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | db/models/users.js | 15 | ||||
| -rw-r--r-- | index.js | 3 | ||||
| -rw-r--r-- | lib/notifications.js | 23 | ||||
| -rw-r--r-- | lib/sockets.js | 93 | ||||
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | routes/bitcoin/receive.js | 10 | ||||
| -rw-r--r-- | routes/info.js (renamed from routes/balances.js) | 11 | ||||
| -rw-r--r-- | routes/lightning/receive.js | 6 | ||||
| -rw-r--r-- | routes/liquid/receive.js | 8 | ||||
| -rw-r--r-- | routes/send.js | 3 | ||||
| -rw-r--r-- | routes/users.js | 41 | ||||
| -rw-r--r-- | yarn.lock | 68 |
13 files changed, 234 insertions, 49 deletions
@@ -9,6 +9,7 @@ config.js .env config/config.json config/index.js +config/clientversion yarn-error.log .pnp* .yarn* diff --git a/db/models/users.js b/db/models/users.js index 11331cf7..1c16f737 100644 --- a/db/models/users.js +++ b/db/models/users.js @@ -127,6 +127,21 @@ const attributes = { comment: null, field: "updatedAt" }, + subscriptions: { + type: DataTypes.TEXT, + get: function() { + return JSON.parse(this.getDataValue("subscriptions")); + }, + set: function(value) { + return this.setDataValue("subscriptions", JSON.stringify(value)); + }, + allowNull: true, + defaultValue: null, + primaryKey: false, + autoIncrement: false, + comment: null, + field: "subscriptions" + }, twofa: { type: DataTypes.INTEGER(1), allowNull: true, @@ -30,9 +30,10 @@ require("./lib/utils"); require("./lib/sockets"); require("./lib/passport"); require("./lib/rates"); +require("./lib/notifications"); require("./routes/assets"); -require("./routes/balances"); +require("./routes/info"); require("./routes/invoices"); require("./routes/payments"); require("./routes/users"); diff --git a/lib/notifications.js b/lib/notifications.js new file mode 100644 index 00000000..710a5bfe --- /dev/null +++ b/lib/notifications.js @@ -0,0 +1,23 @@ +const webpush = require("web-push"); + +if (config.vapid) { + webpush.setVapidDetails( + config.vapid.url, + config.vapid.publicKey, + config.vapid.privateKey + ); +} + +notify = (user, payload) => { + let { subscriptions } = user; + + if (subscriptions) { + subscriptions.map(async subscription => { + try { + await webpush.sendNotification(subscription, payload); + } catch (e) { + l.error("problem sending notification", e); + } + }); + } +}; diff --git a/lib/sockets.js b/lib/sockets.js index 89beffb3..07af88d8 100644 --- a/lib/sockets.js +++ b/lib/sockets.js @@ -37,47 +37,66 @@ const names = {}; wss.on("connection", function(ws, req) { const id = uuidv4(); - setTimeout(() => ws.ok || ws.close(), 2000); + setTimeout(() => ws.user || ws.close(), 2000); - ws.on("message", async token => { - if (!token || token === "null") return; + ws.on("message", async (message) => { + let type, data; try { - const username = jwt.decode(token).username; - if (!sockets[username]) sockets[username] = {}; - sockets[username][id] = ws; - names[id] = username; + ({ type, data } = JSON.parse(message)); + } catch (e) { + l.error("coudn't parse socket message"); + } - let user = await getUser(username); - let payments = await user.getPayments({ - where: { - account_id: user.account_id, - }, - order: [["id", "DESC"]], - limit: 12, - include: { - model: db.Account, - as: "account" - } - }); - let accounts = await user.getAccounts(); - user = user.get({ plain: true }); - user.accounts = accounts; - user.payments = payments; + let token; + switch (type) { + case "login": + token = data; + if (!token || token === "null") return; + try { + const username = jwt.decode(token).username; + if (!sockets[username]) sockets[username] = {}; + sockets[username][id] = ws; + names[id] = username; - ws.ok = true; - ws.send(JSON.stringify({ type: "nodes", data: networks })); - ws.send(JSON.stringify({ type: "rates", data: app.get("rates") })); - ws.send(JSON.stringify({ type: "login", data: user })); - l.info("socket open", username, id); - } catch (e) { - ws.close(); - l.error( - "problem opening socket", - token, - e.message, - id, - req.connection.remoteAddress - ); + let user = await getUser(username); + let payments = await user.getPayments({ + where: { + account_id: user.account_id + }, + order: [["id", "DESC"]], + limit: 12, + include: { + model: db.Account, + as: "account" + } + }); + let accounts = await user.getAccounts(); + user = user.get({ plain: true }); + user.accounts = accounts; + user.payments = payments; + + ws.user = user; + ws.send(JSON.stringify({ type: "login", data: user })); + ws.send(JSON.stringify({ type: "version", data: clientVersion })); + l.info("socket open", username, id); + } catch (e) { + ws.close(); + l.error( + "problem opening socket", + token, + e.message, + id, + req.connection.remoteAddress + ); + } + break; + case "token": + token = data; + if (!ws.user) return; + ws.token = token; + break; + default: + l.warn("received socket message of unknown type", type, data); } }); diff --git a/package.json b/package.json index 9a582d46..d982b218 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "sequelize": "^5.21.3", "text-to-image": "^2.3.0", "uuid": "^3.3.2", + "web-push": "^3.4.4", "ws": "^7.1.2", "zeromq": "^5.1.0" }, diff --git a/routes/bitcoin/receive.js b/routes/bitcoin/receive.js index 6928dd5c..0593584e 100644 --- a/routes/bitcoin/receive.js +++ b/routes/bitcoin/receive.js @@ -106,7 +106,8 @@ zmqRawTx.on("message", async (topic, message, sequence) => { emit(user.username, "account", account); emit(user.username, "payment", payment); - l.info("bitcoin detected", user.username, o.value); + l.info("bitcoin detected", user.username, value); + notify(user, `${value} SAT payment detected`); } }) ); @@ -149,9 +150,11 @@ setInterval(async () => { const { user } = p; if (p) { + let total = p.amount + p.tip; + p.confirmed = 1; - p.account.balance += p.amount + p.tip; - p.account.pending -= Math.min(p.account.pending, p.amount + p.tip); + p.account.balance += total; + p.account.pending -= Math.min(p.account.pending, total); await p.account.save(); await p.save(); @@ -159,6 +162,7 @@ setInterval(async () => { emit(user.username, "account", p.account); emit(user.username, "payment", p); l.info("bitcoin confirmed", user.username, p.amount, p.tip); + notify(user, `${total} SAT payment confirmed`); } else { l.warn("couldn't find payment", hash); } diff --git a/routes/balances.js b/routes/info.js index ba834306..81cb2095 100644 --- a/routes/balances.js +++ b/routes/info.js @@ -1,6 +1,14 @@ const sequelize = require("sequelize"); const { Op } = sequelize; +const fs = require("fs"); +try { + clientVersion = fs.readFileSync('config/clientversion').toString(); + l.info("client version", clientVersion); +} catch(e) { + l.warn("no client version found"); +} + app.get("/info", async (req, res) => { const accounts = await db.Account.findAll({ attributes: [ @@ -49,6 +57,9 @@ app.get("/info", async (req, res) => { liquid, lnchannel, lnwallet, + rates: app.get("rates"), + nodes: networks, + clientVersion, }; info.total = diff --git a/routes/lightning/receive.js b/routes/lightning/receive.js index 6a9c95c8..292f19e2 100644 --- a/routes/lightning/receive.js +++ b/routes/lightning/receive.js @@ -48,8 +48,9 @@ const handlePayment = async msg => { }, { transaction }); - invoice.received += amount + tip; - account.balance += amount + tip; + let total = amount + tip; + invoice.received += total; + account.balance += total; await account.save({ transaction }); await invoice.save({ transaction }); @@ -60,6 +61,7 @@ const handlePayment = async msg => { payment.account = account.get({ plain: true }); emit(user.username, "payment", payment); emit(user.username, "account", payment.account); + notify(user, `Received ${total} SAT`); l.info( "lightning payment received", diff --git a/routes/liquid/receive.js b/routes/liquid/receive.js index b3f82a2d..9a97aa47 100644 --- a/routes/liquid/receive.js +++ b/routes/liquid/receive.js @@ -121,6 +121,7 @@ zmqRawTx.on("message", async (topic, message, sequence) => { emit(user.username, "payment", payment); l.info("liquid detected", user.username, asset, value); + notify(user, `${value} SAT payment detected`); } }) ); @@ -231,9 +232,10 @@ setInterval(async () => { const { user } = p; if (p) { + let total = p.amount + p.tip; p.confirmed = 1; - p.account.balance += p.amount + p.tip; - p.account.pending -= Math.min(p.account.pending, p.amount + p.tip); + p.account.balance += total; + p.account.pending -= Math.min(p.account.pending, total); await p.account.save(); await p.save(); @@ -247,6 +249,8 @@ setInterval(async () => { p.amount, p.tip ); + + notify(user, `${total} SAT payment confirmed`); } else { l.warn("couldn't find payment", hash); } diff --git a/routes/send.js b/routes/send.js index 837a9e45..8b4137d8 100644 --- a/routes/send.js +++ b/routes/send.js @@ -122,7 +122,8 @@ module.exports = async (req, res) => { emit(user.username, "account", account); emit(user.username, "payment", payment); - l.info("received internal", user.username, payment.amount); + l.info("received internal", user.username, amount); + notify(user, `Received ${amount} SAT`); }); } catch (e) { l.error( diff --git a/routes/users.js b/routes/users.js index fc2f2975..9de84b83 100644 --- a/routes/users.js +++ b/routes/users.js @@ -101,7 +101,10 @@ app.post("/register", async (req, res) => { let ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; - if (!challenge[ip] || user.response.toLowerCase() !== challenge[ip].toLowerCase()) { + if ( + !challenge[ip] || + user.response.toLowerCase() !== challenge[ip].toLowerCase() + ) { l.info("failed challenge", ip, user.response, challenge[ip]); return res.status(500).send("Failed challenge"); } @@ -203,7 +206,8 @@ app.post("/user", auth, async (req, res) => { twofa, pin, password, - passconfirm + passconfirm, + tokens } = req.body; let exists = await db.User.findOne({ @@ -231,6 +235,7 @@ app.post("/user", auth, async (req, res) => { user.currency = currency; user.currencies = currencies; + user.tokens = tokens; user.twofa = twofa; user.pin = pin; @@ -290,6 +295,19 @@ app.post("/login", async (req, res) => { } }); + +app.post("/logout", auth, async (req, res) => { + let { subscription } = req.body; + l.info("logging out", req.user.username); + if (!subscription) return res.end(); + let i = req.user.subscriptions.findIndex(s => JSON.stringify(s) === subscription); + if (i > -1) { + req.user.subscriptions.splice(i, 1); + } + await req.user.save(); + res.end(); +}); + app.post("/address", auth, async (req, res) => { if (!config.bitcoin) return res.status(500).send("Bitcoin not configured"); @@ -363,3 +381,22 @@ app.post("/shiftAccount", auth, async (req, res) => { }); setInterval(() => (faucet = 2000), DAY); + +app.get("/vapidPublicKey", function(req, res) { + res.send(config.vapid.publicKey); +}); + +app.post("/subscribe", auth, async function(req, res) { + let { subscriptions } = req.user; + let { subscription } = req.body; + if (!subscriptions) subscriptions = []; + if ( + !subscriptions.find(s => JSON.stringify(s) === JSON.stringify(subscription)) + ) + subscriptions.push(subscription); + req.user.subscriptions = subscriptions; + l.info("subscribing", req.user.username); + await req.user.save(); + res.sendStatus(201); +}); + @@ -278,6 +278,13 @@ agent-base@4, agent-base@^4.2.0, agent-base@^4.3.0: dependencies: es6-promisify "^5.0.0" +agent-base@6: + version "6.0.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.0.tgz#5d0101f19bbfaed39980b22ae866de153b93f09a" + integrity sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw== + dependencies: + debug "4" + agent-base@~4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" @@ -420,6 +427,16 @@ ascli@~1: colour "~0.7.1" optjs "~3.2.2" +asn1.js@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.3.0.tgz#439099fe9174e09cff5a54a9dda70260517e8689" + integrity sha512-WHnQJFcOrIWT1RLOkFFBQkFVvyt9BPOOrH+Dp152Zk4R993rSzXUGPmkybIcUFhHE2d/iHH+nCaOWVCDbO8fgA== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + safer-buffer "^2.1.0" + asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" @@ -656,7 +673,7 @@ bluebird@^3.5.0: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -bn.js@^4.11.8, bn.js@^4.4.0: +bn.js@^4.0.0, bn.js@^4.11.8, bn.js@^4.4.0: version "4.11.9" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== @@ -2117,6 +2134,13 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" + version "1.1.0" + resolved "https://registry.yarnpkg.com/http_ece/-/http_ece-1.1.0.tgz#74780c6eb32d8ddfe9e36a83abcd81fe0cd4fb75" + integrity sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA== + dependencies: + urlsafe-base64 "~1.0.0" + https-proxy-agent@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81" @@ -2125,6 +2149,14 @@ https-proxy-agent@^3.0.0: agent-base "^4.3.0" debug "^3.1.0" +https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + [email protected], iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -2444,6 +2476,15 @@ jwa@^1.4.1: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + jws@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" @@ -2452,6 +2493,14 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + keyv@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" @@ -4436,6 +4485,11 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" +urlsafe-base64@^1.0.0, urlsafe-base64@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz#23f89069a6c62f46cf3a1d3b00169cefb90be0c6" + integrity sha1-I/iQaabGL0bPOh07ABac77kL4MY= + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -4484,6 +4538,18 @@ [email protected]: dependencies: async "1.5" +web-push@^3.4.4: + version "3.4.4" + resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.4.4.tgz#b11523ada0f4b8c2481f65d1d059acd45ba27ca0" + integrity sha512-tB0F+ccobsfw5jTWBinWJKyd/YdCdRbKj+CFSnsJeEgFYysOULvWFYyeCxn9KuQvG/3UF1t3cTAcJzBec5LCWA== + dependencies: + asn1.js "^5.3.0" + http_ece "1.1.0" + https-proxy-agent "^5.0.0" + jws "^4.0.0" + minimist "^1.2.5" + urlsafe-base64 "^1.0.0" + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" |
