aboutsummaryrefslogtreecommitdiffstats
path: root/users.js
blob: 8575bf253f115b1462de7baa4e4d3cd301bba8cd (plain) (blame)
1
2
3
4
5
6pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888 } /* Comment */
.highlight .err { color: #A61717; background-color: #E3D2D2 } /* Error */
.highlight .k { color: #080; font-weight: bold } /* Keyword */
.highlight .ch { color: #888 } /* Comment.Hashbang */
.highlight .cm { color: #888 } /* Comment.Multiline */
.highlight .cp { color: #C00; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888 } /* Comment.Single */
.highlight .cs { color: #C00; font-weight: bold; background-color: #FFF0F0 } /* Comment.Special */
.highlight .gd { color: #000; background-color: #FDD } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #A00 } /* Generic.Error */
.highlight .gh { color: #333 } /* Generic.Heading */
.highlight .gi { color: #000; background-color: #DFD } /* Generic.Inserted */
.highlight .go { color: #888 } /* Generic.Output */
.highlight .gp { color: #555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666 } /* Generic.Subheading */
.highlight .gt { color: #A00 } /* Generic.Traceback */
.highlight .kc { color: #080; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #080; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #080; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #080 } /* Keyword.Pseudo */
.highlight .kr { color: #080; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #00D; font-weight: bold } /* Literal.Number */
.highlight .s { color: #D20; background-color: #FFF0F0 } /* Literal.String */
.highlight .na { color: #369 } /* Name.Attribute */
.highlight .nb { color: #038 } /* Name.Builtin */
.highlight .nc { color: #B06; font-weight: bold } /* Name.Class */
.highlight .no { color: #036; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555 } /* Name.Decorator */
.highlight .ne { color: #B06; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #06B; font-weight: bold } /* Name.Function */
.highlight .nl { color: #369; font-style: italic } /* Name.Label */
.highlight .nn { color: #B06; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #369; font-weight: bold } /* Name.Property */
.highlight .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 */
// Max (and fixed-amount invoices) fill the amount field in the unit it is
// showing: in fiat that is money to the cent, floored so a filled-in maximum
// never parses back to more sats than were offered — not a BTC figure under a
// CAD label (2026-10-08: Max on an ark recipient filled 0.00004978 'CAD').
// Run: bun tools/amount-field-unit-test.js
import assert from 'node:assert/strict';
import puppeteer from './puppeteer.js';

const bundle = await Bun.build({ entrypoints: ['src/app.js'], target: 'browser', minify: true,
  plugins: [{ name: 'amount-field-test', setup(build) {
    build.onLoad({ filter: /src\/app\.js$/ }, async ({ path }) => ({ loader: 'js', contents: await Bun.file(path).text() + `
window.amtApp = { ctx, toggleUnit, get unit() { return unit; } };
` }));
    build.onLoad({ filter: /src\/video-compress\.js$/ }, () => ({ loader: 'js', contents: 'export async function compressVideo(f) { return f; }' }));
  } }] });
assert(bundle.success, bundle.logs.join('\n'));
const html = `<!doctype html><meta charset="utf-8"><div id="app"></div><script type="module">${await bundle.outputs[0].text()}</script>`;
const server = Bun.serve({ port: 0, fetch: (req) => new URL(req.url).pathname === '/'
  ? new Response(html, { headers: { 'Content-Type': 'text/html' } }) : new Response(null, { status: 404 }) });
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
try {
  const page = await browser.newPage(), errors = [];
  page.on('pageerror', (e) => errors.push(e.message));
  await page.setRequestInterception(true);
  page.
const axios = require("axios");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const mailgun = require("mailgun-js");
const uuidv4 = require("uuid/v4");
const BitcoinCore = require("bitcoin-core");
const config = require("./config");
const whitelist = require("./whitelist");
const fb = "https://graph.facebook.com/";
const liquid = new BitcoinCore(config.liquid);
const Sequelize = require("sequelize");
const authenticator = require("otplib").authenticator;

const pick = (O, ...K) => K.reduce((o, k) => ((o[k] = O[k]), o), {});
const l = console.log;
let faucet = 1000;
const DAY = 24 * 60 * 60 * 1000;

const twofa = (req, res, next) => {
  let {
    user,
    body: { token }
  } = req;
  if (!user.twofa || authenticator.check(token, user.otpsecret)) next();
  else res.status(401).send("2fa required");
};

module.exports = (addresses, auth, app, bc, db, emit) => {
  app.get("/liquidate", async (req, res) => {
    let users = await db.User.findAll();
    for (let i = 0; i < users.length; i++) {
      let user = users[i];
      if (!user.confidential) {
        user.confidential = await liquid.getNewAddress();
        user.liquid = (await liquid.getAddressInfo(
          user.confidential
        )).unconfidential;
        await user.save();
      }

      if (!user.otpsecret) {
        user.otpsecret = authenticator.generateSecret();
        await user.save();
      }
    }
    res.end();
  });

  const gift = async user => {
    if (faucet > 0) {
      user.balance = 100;
      faucet -= 100;
      const payment = await db.Payment.create({
        user_id: user.id,
        hash: "Welcome Gift",
        amount: 100,
        currency: user.currency,
        rate: app.get("rates")[user.currency],
        received: true,
        confirmed: 1,
        asset: "GIFT"
      });
      await user.save();
    }
  };

  app.post("/register", async (req, res) => {
    let err = m => res.status(500).send(m);
    let user = req.body;
    if (!user.username) return err("Username required");

    let exists = await db.User.count({ where: { username: user.username } });
    if (exists) return err("Username taken");

    user.address = await bc.getNewAddress("", "bech32");
    user.confidential = await liquid.getNewAddress();
    user.liquid = (await liquid.getAddressInfo(
      user.confidential
    )).unconfidential;
    user.name = user.username;
    let ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress;
    let countries = {
      CA: "CAD",
      US: "USD",
      JP: "JPY",
      CN: "CNY",
      AU: "AUD",
      GB: "GBP"
    };

    if (ip === "127.0.0.1") user.currency = "CAD";
    else { 
      let info = await axios.get(`http://api.ipstack.com/${ip}?access_key=${config.ipstack}`);
      user.currency = countries[info.data.country_code] || "USD";
    }

    user.currencies = [user.currency];
    user.otpsecret = authenticator.generateSecret();

    addresses[user.address] = user.username;
    addresses[user.liquid] = user.username;
    user = await db.User.create(user);
    await gift(user);

    res.send(pick(user, ...whitelist));
  });

  const requestEmail = async user => {
    user.emailToken = uuidv4();
    await user.save();

    let mg = mailgun(config.mailgun);
    let msg = {
      from: "CoinOS <[email protected]>",
      to: user.email,
      subject: "CoinOS Email Verification",
      html: `Visit <a href="https://coinos.io/verify/${user.username}/${
        user.emailToken
      }">https://coinos.io/verify/${user.username}/${
        user.emailToken
      }</a> to verify your email address.`
    };

    try {
      mg.messages().send(msg);
    } catch (e) {
      l(e);
    }
  };

  const requestPhone = async user => {
    user.phoneToken = Math.floor(100000 + Math.random() * 900000);
    await user.save();
    const client = require("twilio")(
      config.twilio.sid,
      config.twilio.authToken
    );

    await client.messages.create({
      body: user.phoneToken,
      from: config.twilio.number,
      to: user.phone
    });
  };

  app.post("/requestEmail", auth, async (req, res) => {
    req.user.email = req.body.email;
    await requestEmail(req.user);
    res.end();
  });

  app.post("/requestPhone", auth, async (req, res) => {
    req.user.phone = req.body.phone;
    await requestPhone(req.user);
    res.end();
  });

  app.post("/disable2fa", auth, twofa, async (req, res) => {
    let { user } = req;
    user.twofa = false;
    await user.save();
    emit(user.username, "user", user);
    emit(user.username, "otpsecret", user.otpsecret);
    res.end();
  });

  app.get("/otpsecret", auth, twofa, async (req, res) => {
    let { user } = req;
    emit(user.username, "otpsecret", user.otpsecret);
    res.end();
  });

  app.post("/2fa", auth, async (req, res) => {
    let { user } = req;
    try {
      const isValid = authenticator.check(req.body.token, req.user.otpsecret);
      if (isValid) {
        user.twofa = true;
        user.save();
        emit(user.username, "user", req.user);
      } else {
        return res.status(500).send("Invalid token");
      }
    } catch (e) {
      l(e);
    }
    res.end();
  });

  app.post("/user", auth, async (req, res) => {
    try {
      let { user } = req;
      let {
        username,
        currency,
        currencies,
        email,
        phone,
        twofa,
        pin,
        password,
        passconfirm
      } = req.body;

      let exists = await db.User.findOne({
        where: { username }
      });

      let token;
      if (user.username !== username && exists) {
        return res.status(500).send("Username taken");
      } else {
        user.username = username;
        addresses[user.address] = username;
        addresses[user.liquid] = user.username;
        token = jwt.sign({ username }, config.jwt);
        res.cookie("token", token, {
          expires: new Date(Date.now() + 432000000)
        });
      }

      if (email && user.email !== email) {
        if (!require("email-validator").validate(email)) {
          return res.status(500).send("invalid email");
        }

        user.email = email;
        user.emailVerified = false;
        requestEmail(user);
      }

      if (user.phone !== phone) {
        user.phone = phone;
        user.phoneVerified = false;
        requestPhone(user);
      }

      user.currencies = currencies;
      user.currency = currency;
      if (currencies.length) {
        if (!currencies.includes(currency)) user.currency = currencies[0];
      } else {
        user.currency = null;
      }

      user.email = email;
      user.phone = phone;
      user.twofa = twofa;
      user.pin = pin;

      if (password && password === passconfirm) {
        user.password = await bcrypt.hash(password, 1);
      }

      await user.save();
      emit(user.username, "user", req.user);
      res.send({ user, token });
    } catch (e) {
      l(e);
    }
  });

  app.post("/forgot", async (req, res) => {
    let user = await db.User.findOne({
      where: {
        email: req.body.email
      }
    });

    if (user) {
      let mg = mailgun(config.mailgun);
      let msg = {
        from: "CoinOS <[email protected]>",
        to: user.email,
        subject: "CoinOS Password Reset",
        html: `Visit <a href="https://coinos.io/reset/${user.username}/${
          user.token
        }">https://coinos.io/reset/${user.username}/${
          user.token
        }</a> to reset your password.`
      };

      try {
        mg.messages().send(msg);
      } catch (e) {
        l(e);
      }
    }

    res.end();
  });

  app.get("/verifyEmail/:username/:token", auth, async (req, res) => {
    let user = await db.User.findOne({
      where: {
        username: req.params.username,
        emailToken: req.params.token
      }
    });

    user.emailToken = uuidv4();

    if (user) {
      user.emailVerified = true;
      await user.save();

      emit(user.username, "user", user);
      emit(user.username, "emailVerified", true);

      res.end();
    } else {
      res.status(500).send("invalid token or username");
    }
  });

  app.get("/verifyPhone/:username/:token", auth, async (req, res) => {
    let user = await db.User.findOne({
      where: {
        username: req.params.username,
        phoneToken: req.params.token
      }
    });

    if (user) {
      user.phoneVerified = true;
      await user.save();

      emit(user.username, "user", user);
      emit(user.username, "phoneVerified", true);
      res.end();
    } else {
      res.status(500).send("invalid token or username");
    }
  });

  app.post("/login", async (req, res) => {
    let twofa = req.body.token;

    try {
      let user = await db.User.findOne({
        where: {
          username: req.body.username
        }
      });

      if (
        !user ||
        (user.password &&
          !(await bcrypt.compare(req.body.password, user.password)))
      ) {
        return res.status(401).end();
      }

      if (user.twofa && (typeof twofa === "undefined" || !authenticator.check(twofa, user.otpsecret)))
        return res.status(401).send("2fa required");

      let payload = { username: user.username };
      let token = jwt.sign(payload, config.jwt);
      res.cookie("token", token, { expires: new Date(Date.now() + 432000000) });

      user = pick(user, ...whitelist);
      res.send({ user, token });
    } catch (err) {
      l(err);
      res.status(401).end();
    }
  });

  app.post("/facebookLogin", async (req, res) => {
    let { accessToken, userID } = req.body;
    let twofa = req.body.token;

    let url = `https://graph.facebook.com/debug_token?input_token=${accessToken}&access_token=${
      config.facebook.appToken
    }`;
    let check = await axios.get(url);
    if (!check.data.data.is_valid) return res.status(401).end();

    try {
      let user = await db.User.findOne({
        where: {
          username: req.body.userID
        }
      });

      if (!user) {
        user = await db.User.create(user);
        user.username = userID;
        user.name = (await axios.get(
          `${fb}/me?access_token=${accessToken}`
        )).data.name;
        user.address = await bc.getNewAddress("", "bech32");
        user.confidential = await liquid.getNewAddress();
        user.liquid = (await liquid.getAddressInfo(
          user.confidential
        )).unconfidential;
        user.password = await bcrypt.hash(accessToken, 1);
        user.balance = 0;
        user.pending = 0;
        let friends = (await axios.get(
          `${fb}/${userID}/friends?access_token=${accessToken}`
        )).data.data;
        if (friends.find(f => f.id === config.facebook.specialFriend)) {
          user.friend = true;
          user.limit = 200;
        }
        await user.save();
        addresses[user.address] = user.username;
        addresses[user.liquid] = user.username;
        await gift(user);
      }

      if (user.twofa && (typeof twofa === "undefined" || !authenticator.check(twofa, user.otpsecret)))
        return res.status(401).send("2fa required");

      user.pic = (await axios.get(
        `${fb}/me/picture?access_token=${accessToken}&redirect=false`
      )).data.data.url;
      user.fbtoken = accessToken;
      await user.save();

      let payload = { username: user.username };
      let token = jwt.sign(payload, config.jwt);
      res.cookie("token", token , { expires: new Date(Date.now() + 432000000) });
      res.send({ user, token });
    } catch (err) {
      l(err);
      res.status(401).end();
    }
  });

  app.get("/me", async (req, res) => {
    let {
      data: {
        data: { url }
      }
    } = await axios.get(
      `${fb}/me/picture?access_token=EAAEIFqWk3ZAwBAEUfxQdH3T5CBKXmU8d7jQ5OTJBJZBiU1ZAp76lO26nh57WolM4R4JoKks9BCc49s8VrlEm2Ub1GlZCEzVD9fGxzUiranXDErDmR5gDUPKX3BhCsGA649a4hmbldRwKFTsmZCGZCergm9ACspKdTZB0WgFgA9wEdemIRIXuwCygNrymmKDh0Wd8nmoT4Hj3wZDZD&redirect=false`
    );
    res.send(url);
  });

  app.get("/friends", auth, async (req, res) => {
    try {
      const {
        data: { data }
      } = await axios.get(
        `${fb}/${req.user.username}/friends?access_token=${req.user.fbtoken}`
      );

      const friends = await Promise.all(
        data.map(async f => {
          f.pic = (await axios.get(
            `${fb}/${f.id}/picture?redirect=false&type=small&access_token=${
              req.user.fbtoken
            }`
          )).data.data.url;
          return f;
        })
      );

      res.send(friends);
    } catch (e) {
      res
        .status(500)
        .send("There was a problem getting your facebook friends: " + e);
    }
  });

  setInterval(() => (faucet = 2000), DAY);
};