aboutsummaryrefslogtreecommitdiffstats
path: root/src/app.html
blob: d85457474a770825d33cea11bcf605df9f2aa73e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<meta name="description" content="Easy to use bitcoin point of sale app and web wallet." />
		<meta property="og:image" content="/icons/logo.svg" />
		<title>coinos</title>
		%sveltekit.head%
		<script src="https://stamen-maps.a.ssl.fastly.net/js/tile.stamen.js"></script>
		<style>
			img.huechange {
				filter: hue-rotate(170deg);
			}
		</style>
	</head>
	<body>
		<div>%sveltekit.body%</div>
	</body>
</html>
ht .nt { color: #B06; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #369 } /* Name.Variable */ .highlight .ow { color: #080 } /* Operator.Word */ .highlight .w { color: #BBB } /* Text.Whitespace */ .highlight .mb { color: #00D; font-weight: bold } /* Literal.Number.Bin */ .highlight .mf { color: #00D; font-weight: bold } /* Literal.Number.Float */ .highlight .mh { color: #00D; font-weight: bold } /* Literal.Number.Hex */ .highlight .mi { color: #00D; font-weight: bold } /* Literal.Number.Integer */ .highlight .mo { color: #00D; font-weight: bold } /* Literal.Number.Oct */ .highlight .sa { color: #D20; background-color: #FFF0F0 } /* Literal.String.Affix */ .highlight .sb { color: #D20; background-color: #FFF0F0 } /* Literal.String.Backtick */ .highlight .sc { color: #D20; background-color: #FFF0F0 } /* Literal.String.Char */ .highlight .dl { color: #D20; background-color: #FFF0F0 } /* Literal.String.Delimiter */ .highlight .sd { color: #D20; background-color: #FFF0F0 } /* Literal.String.Doc */ .highlight .s2 { color: #D20; background-color: #FFF0F0 } /* Literal.String.Double */ .highlight .se { color: #04D; background-color: #FFF0F0 } /* Literal.String.Escape */ .highlight .sh { color: #D20; background-color: #FFF0F0 } /* Literal.String.Heredoc */ .highlight .si { color: #33B; background-color: #FFF0F0 } /* Literal.String.Interpol */ .highlight .sx { color: #2B2; background-color: #F0FFF0 } /* Literal.String.Other */ .highlight .sr { color: #080; background-color: #FFF0FF } /* Literal.String.Regex */ .highlight .s1 { color: #D20; background-color: #FFF0F0 } /* Literal.String.Single */ .highlight .ss { color: #A60; background-color: #FFF0F0 } /* Literal.String.Symbol */ .highlight .bp { color: #038 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #06B; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #369 } /* Name.Variable.Class */ .highlight .vg { color: #D70 } /* Name.Variable.Global */ .highlight .vi { color: #33B } /* Name.Variable.Instance */ .highlight .vm { color: #369 } /* Name.Variable.Magic */ .highlight .il { color: #00D; font-weight: bold } /* Literal.Number.Integer.Long */
import store from "./store";
import config from "$config";
import app from "$app";
import jwt from "jsonwebtoken";
import { v4 } from "uuid";
import WebSocket from "ws";
import { differenceInSeconds, subSeconds } from "date-fns";
import fastifyWs from "@fastify/websocket";
import whitelist from "./whitelist";
import { l, err, warn } from "$lib/logging";
import { getUser } from "$lib/utils";
import db from "$db";

const code = 1000;

const wss = new WebSocket.Server({ clientTracking: false, noServer: true });

const pick = (O, ...K) => K.reduce((o, k) => ((o[k] = O[k]), o), {});
const all = {};
const subscriptions = [];
const lastSeen = {};

export const emit = (username, type, data) => {
  if (data.username !== undefined) data = pick(data, ...whitelist);

  if (type === "payment" && data.amount > 0) {
    for (let i = subscriptions.length - 1; i >= 0; i--) {
      let s = subscriptions[i];

      if (
        s.invoice.amount === data.amount ||
        (data.address && s.invoice.address === data.address) ||
        (data.address && s.invoice.unconfidential === data.address)
      ) {
        s.ws.send(JSON.stringify({ type, data }));
      }
    }
  }

  if (!store.sockets[username]) return;
  for (let id in store.sockets[username]) {
    const ws = store.sockets[username][id];
    ws.send(JSON.stringify({ type, data }));
  }
};

export const broadcast = (type, data) => {
  const sent = [];
  for (const username in store.sockets) {
    for (const id in store.sockets[username]) {
      const ws = store.sockets[username][id];
      ws.send(JSON.stringify({ type, data }));
      sent.push(ws.id);
    }
  }

  Object.values(all).map(ws => {
    if (!sent.includes(ws.id)) ws.send(JSON.stringify({ type, data }));
  });
};

function subtractSeconds(numOfSeconds, date = new Date()) {
  date.setSeconds(date.getSeconds() - numOfSeconds);

  return date;
}

app.register(fastifyWs);
app.register(async function(app) {
  app.get("/ws", { websocket: true }, ({ socket: ws }, req) => {
    const id = v4();
    ws.id = id;
    ws.beats = 0;
    ws.send(JSON.stringify({ type: "connected", data: id }));
    all[id] = ws;

    ws.on("message", async message => {
      let type, data;
      try {
        ({ type, data } = JSON.parse(message));
      } catch (e) {
        err("coudn't parse socket message");
      }

      switch (type) {
        case "heartbeat":
          ws.beats = 0;
          if (ws.user) emit(ws.user.username, "id", ws.id);
          break;

        case "login":
          let token = data;
          if (lastSeen[token] > subtractSeconds(5)) return;
          lastSeen[token] = new Date();

          try {
            if (!token || token === "null" || !jwt.decode(token))
              return ws.close(code, `bad token ${token}`);
            const username = jwt.decode(token).username;
            if (!username) throw new Error("Invalid JWT token");

            if (!store.sockets[username]) store.sockets[username] = {};
            const existing = Object.keys(store.sockets[username]);
            if (existing.length > 4) {
              const p = existing.find(sid => sid !== id);
              store.sockets[username][p].send(
                JSON.stringify({ type: "logout" })
              );
              setTimeout(() => {
                if (store.sockets[username][p])
                  store.sockets[username][p].close(code, "too many sockets");
              }, 5000);
            }
            store.sockets[username][id] = ws;
            names[id] = username;

            let user = await getUser(username);
            if (user.locked) throw new Error("Account is locked");
            let payments = await user.getPayments({
              where: {
                account_id: user.account_id
              },
              order: [["id", "DESC"]],
              limit: 12,
              include: [
                {
                  model: db.Account,
                  as: "account"
                },
                {
                  model: db.Payment,
                  as: "fee_payment"
                }
              ]
            });

            let accounts = await user.getAccounts();
            
            let keys = await user.getKeys();
            user = user.get({ plain: true });
            user.accounts = accounts;
            user.keys = keys;
            user.payments = payments;

            ws.user = user;
            ws.send(JSON.stringify({ type: "login", data: user }));
            ws.send(
              JSON.stringify({ type: "version", data: config.clientVersion })
            );
          } catch (e) {
            err("caught socket error", e.message);
            if (e.message.includes("locked")) {
              ws.send(JSON.stringify({ type: "locked" }));
              ws.send(JSON.stringify({ type: "logout" }));
            }
            if (e.message.includes("not found"))
              ws.send(JSON.stringify({ type: "logout" }));
            setTimeout(
              () => ws && ws.close(code, `closing due to error ${e.message}`),
              1000
            );
          }
          break;
        case "token":
          if (!ws.user) return;
          ws.token = data;
          break;
        case "lnurl":
          store.sessions[data] = ws;
          break;
        case "updateMemo":
          const payment = await db.Payment.findOne({
            where: {
              user_id: ws.user.id,
              id: data.id
            },
            include: {
              model: db.Account,
              as: "account"
            }
          });
          if (payment) {
            payment.memo = data.memo;
            await payment.save();
            emit(ws.user.username, "payment", payment);
          }
          break;
        case "subscribe":
          subscriptions.push({ invoice: data, ws });
          break;
        default:
          warn("received socket message of unknown type", type, data);
      }
    });

    ws.on("close", function(code, reason) {
      // if (reason) console.log("closing socket", reason.toString(), id);
      try {
        const username = names[id];
        if (store.sockets[username] && store.sockets[username][id]) {
          delete store.sockets[username][id];
        }

        if (all[id]) delete all[id];
      } catch (e) {
        err("problem closing socket", e.message);
      }
    });
  });
});

const names = {};

setInterval(() => {
  for (const username in store.sockets) {
    for (const id in store.sockets[username]) {
      let ws = store.sockets[username][id];
      ws.beats++;
      if (ws.beats > 10) ws.close(code, `${username} ${id} lost heartbeat`);
    }
  }
}, 2000);