aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/RegisterOrLogin.svelte
diff options
context:
space:
mode:
authorsecondl1ght <[email protected]>2022-10-25 18:01:56 -0600
committersecondl1ght <[email protected]>2022-10-25 18:01:56 -0600
commita67b375473f1f40e1c0febfac16d7e36e0d72561 (patch)
tree10a5fa750f2ccb53cff3510ab8fbbce108a35eb6 /src/components/RegisterOrLogin.svelte
parentaa6debcd83c0b4067e0f8ce9a11d9c9ac02476ad (diff)
parent5a5ea3ccc327ff52fb1c313998d90e4bfefea90a (diff)
downloadcoinos-ui-a67b375473f1f40e1c0febfac16d7e36e0d72561.tar.gz
coinos-ui-a67b375473f1f40e1c0febfac16d7e36e0d72561.zip
Merge branch 'master' of github.com:coinos/coinos-ui-v2
Diffstat (limited to 'src/components/RegisterOrLogin.svelte')
-rw-r--r--src/components/RegisterOrLogin.svelte197
1 files changed, 114 insertions, 83 deletions
diff --git a/src/components/RegisterOrLogin.svelte b/src/components/RegisterOrLogin.svelte
index 0b954387..bf2d2084 100644
--- a/src/components/RegisterOrLogin.svelte
+++ b/src/components/RegisterOrLogin.svelte
@@ -1,18 +1,28 @@
<script>
+ import { tick } from 'svelte';
+ import { Pincode, PincodeInput } from 'svelte-pincode';
import { fly } from 'svelte/transition';
import { enhance } from '$app/forms';
import { Icon } from '$comp';
import { focus } from '$lib/utils';
- import { redirect } from '$lib/store';
+ import { loginRedirect } from '$lib/store';
import { t } from '$lib/translations';
+ let token, tokenInput;
+ let code = [];
+
+ $: need2fa = form?.message === '2fa';
+ $: if (need2fa) tokenInput && tick().then(tokenInput.focusFirstInput);
+
export let form;
export let pageID;
- let username,
- password,
- email,
- twofa = '';
+ let username, password, email, submit;
+
+ $: update(form);
+ let update = (form) => form && ({ username, password } = form);
+
+ $: token && token?.length === 6 && tick().then(() => submit.click());
let revealPassword = false;
@@ -31,101 +41,122 @@
<div class="flex justify-center items-center">
<div class="shadow-xl rounded-3xl p-10 pb-12 space-y-5 w-full mx-5 md:mx-0 md:w-[400px]">
- <h1 class="text-2xl font-bold text-center">{$t('login.' + pageID)}</h1>
+ {#if need2fa}
+ <h1 class="text-2xl font-bold text-center">Enter 2FA Code</h1>
- {#if form?.error}
- <div class="text-red-600 text-center" in:fly>
- {form.error}
- </div>
- {/if}
+ {#if form?.token}
+ <div class="text-red-600 text-center" in:fly>Invalid token, try again</div>
+ {/if}
+
+ <form class="space-y-5" {action} method="POST" use:enhance>
+ <input type="hidden" name="username" value={username} />
+ <input type="hidden" name="password" value={password} />
+ <input type="hidden" name="token" value={token} />
+
+ <div>
+ <Pincode bind:code bind:value={token} bind:this={tokenInput}>
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ <PincodeInput />
+ </Pincode>
+ </div>
+ <button
+ bind:this={submit}
+ type="submit"
+ class="bg-black text-white w-full rounded-2xl p-4 font-semibold hover:opacity-80"
+ >{$t('login.' + pageID)}
+ </button>
+ </form>
+ {:else}
+ <h1 class="text-2xl font-bold text-center">{$t('login.' + pageID)}</h1>
- <form class="space-y-5" {action} method="POST" use:enhance>
- {#if $redirect}
- <input type="hidden" name="redirect" value={$redirect} />
+ {#if form?.error}
+ <div class="text-red-600 text-center" in:fly>
+ {form.error}
+ </div>
{/if}
- <div>
- <label for="username" class="font-semibold">{$t('login.username')}</label>
- <input
- name="username"
- type="text"
- required
- class="bg-primary"
- bind:value={username}
- use:focus
- autocapitalize="none"
- />
- </div>
+ <form class="space-y-5" {action} method="POST" use:enhance>
+ {#if $loginRedirect}
+ <input type="hidden" name="loginRedirect" value={$loginRedirect} />
+ {/if}
- <div class="relative">
- <label for="password" class="block font-semibold">{$t('login.password')}</label>
- {#if revealPassword}
+ <div>
+ <label for="username" class="font-semibold">{$t('login.username')}</label>
<input
- name="password"
+ name="username"
type="text"
required
class="bg-primary"
- bind:value={password}
+ bind:value={username}
+ use:focus
autocapitalize="none"
/>
- {:else}
- <input
- name="password"
- type="password"
- required
- class="bg-primary"
- bind:value={password}
- autocapitalize="none"
- />
- {/if}
- <button
- type="button"
- on:click={() => (revealPassword = !revealPassword)}
- class="absolute right-5 top-10"
- >
- <Icon icon={revealPassword ? 'eye' : 'eye-off'} />
- </button>
- </div>
+ </div>
- {#if pageID === 'signIn'}
- <div>
- <label for="token" class="font-semibold"
- >{$t('login.token')}
- <span class="text-secondary">({$t('login.optional')})</span></label
+ <div class="relative">
+ <label for="password" class="block font-semibold">{$t('login.password')}</label>
+ {#if revealPassword}
+ <input
+ name="password"
+ type="text"
+ required
+ class="bg-primary"
+ bind:value={password}
+ autocapitalize="none"
+ />
+ {:else}
+ <input
+ name="password"
+ type="password"
+ required
+ class="bg-primary"
+ bind:value={password}
+ autocapitalize="none"
+ />
+ {/if}
+ <button
+ type="button"
+ on:click={() => (revealPassword = !revealPassword)}
+ class="absolute right-5 top-10"
>
- <input name="token" type="text" class="bg-primary" bind:value={twofa} />
+ <Icon icon={revealPassword ? 'eye' : 'eye-off'} />
+ </button>
</div>
- {/if}
- {#if pageID === 'register'}
- <p class="text-secondary text-sm">
- {$t('login.passwordRecommendation')}
- </p>
- {:else}
- <!-- TODO -->
- <!-- <div class="flex justify-end items-center"> -->
- <!-- <a href="/forgot" class="underline underline-offset-4 text-black text-sm" -->
- <!-- >{$t('login.forgotUserOrPassword')}</a -->
- <!-- > -->
- <!-- </div> -->
- {/if}
+ {#if pageID === 'register'}
+ <p class="text-secondary text-sm">
+ {$t('login.passwordRecommendation')}
+ </p>
+ {:else}
+ <!-- TODO -->
+ <!-- <div class="flex justify-end items-center"> -->
+ <!-- <a href="/forgot" class="underline underline-offset-4 text-black text-sm" -->
+ <!-- >{$t('login.forgotUserOrPassword')}</a -->
+ <!-- > -->
+ <!-- </div> -->
+ {/if}
- <button
- type="submit"
- class="bg-black text-white w-full rounded-2xl p-4 font-semibold hover:opacity-80"
- >{$t('login.' + pageID)}
- </button>
- </form>
+ <button
+ type="submit"
+ class="bg-black text-white w-full rounded-2xl p-4 font-semibold hover:opacity-80"
+ >{$t('login.' + pageID)}
+ </button>
+ </form>
- <p class="text-secondary text-center font-medium">
- {pageID === 'register' ? $t('login.haveAccount') : $t('login.noAccount')}
- <a
- href={pageID === 'register' ? '/login' : '/register'}
- class="block md:inline text-black underline underline-offset-4 hover:opacity-80"
- >
- {pageID === 'register' ? $t('login.signIn') : $t('login.register')}
- </a>
- </p>
+ <p class="text-secondary text-center font-medium">
+ {pageID === 'register' ? $t('login.haveAccount') : $t('login.noAccount')}
+ <a
+ href={pageID === 'register' ? '/login' : '/register'}
+ class="block md:inline text-black underline underline-offset-4 hover:opacity-80"
+ >
+ {pageID === 'register' ? $t('login.signIn') : $t('login.register')}
+ </a>
+ </p>
+ {/if}
</div>
</div>
</div>
or: #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 */
const axios = require("axios");
const lnurl = require("lnurl");
const jwt = require("jsonwebtoken");
const qs = require("query-string");
const persist = require("../lib/persist");

logins = persist('data/logins.json');
recipients = persist('data/recipients.json');
payments = persist('data/payments.json');
withdrawals = persist('data/withdrawals.json');

lnurlServer = lnurl.createServer(config.lnurl);

var optionalAuth = function(req, res, next) {
  passport.authenticate("jwt", { session: false }, function(err, user, info) {
    req.user = user;
    next();
  })(req, res, next);
};

app.get(
  "/url",
  ah(async (req, res, next) => {
    try {
      const code = await db.Code.findOne({
        where: {
          code: req.query.code
        }
      });

      if (code) {
        res.send(code.text);
      } else {
        throw new Error("code not found");
      }
    } catch (e) {
      l.error("couldn't find url", e.message);
    }
  })
);

app.get(
  "/withdraw",
  auth,
  ah(async (req, res, next) => {
    const { min, max } = req.query;
    try {
      const result = await lnurlServer.generateNewUrl("withdrawRequest", {
        minWithdrawable: min * 1000,
        maxWithdrawable: max * 1000,
        defaultDescription: "coinos voucher"
      });

      withdrawals[result.secret] = req.user;

      res.send(result);
    } catch (e) {
      l.error("problem generating withdrawl url", e.message);
      res.status(500).send(e.message);
    }
  })
);

app.post(
  "/code",
  ah(async (req, res, next) => {
    try {
      const { encoded } = req.body.lnurl;
      const code = await db.Code.findOrCreate({
        where: {
          code: `lnurl:${encoded.substr(-32)}`,
          text: encoded
        }
      });

      res.send(code);
    } catch (e) {
      l.error(e.errors);
      res.status(500).send(e.message);
    }
  })
);

app.post(
  "/withdraw",
  auth,
  ah(async (req, res, next) => {
    const { user } = req;
    const {
      amount: value,
      params: { callback, k1 }
    } = req.body;
    const invoice = await lnp.addInvoice({ value });
    const { payment_request: pr } = invoice;
    const url = `${callback}?k1=${k1}&pr=${pr}`;

    await db.Invoice.create({
      user_id: user.id,
      text: pr,
      rate: app.get("rates")[user.currency],
      currency: user.currency,
      amount: value,
      tip: 0,
      network: "bitcoin"
    });

    try {
      result = (await axios.get(url)).data;
      res.send(result);
    } catch (e) {
      l.error("failed to withdraw", e.message);
      res.status(500).send(e.message);
    }
  })
);

app.get(
  "/pay/:username",
  ah(async (req, res, next) => {
    const { username } = req.params;
    let user = await db.User.findOne({
      where: {
        username
      }
    });

    let { amount, minSendable, maxSendable } = req.query;
    minSendable = minSendable || 1000;
    maxSendable = maxSendable || 1000000000;
    if (parseInt(amount)) minSendable = maxSendable = amount * 1000;

    let invoices = await db.Invoice.findAll({
      where: {
        amount
      },
      order: [["id", "DESC"]],
      limit: 1
    });

    if (invoices.length) await invoices[0].destroy();

    try {
      const result = await lnurlServer.generateNewUrl("payRequest", {
        minSendable,
        maxSendable,
        metadata: JSON.stringify([["text/plain", `paying ${user.username}`]])
      });

      recipients[result.secret] = user;
      l.info("recipient", user.username, result.secret);
      res.send(result);
    } catch (e) {
      l.error("problem generating payment url", e.message);
      res.status(500).send(e.message);
    }
  })
);

app.post(
  "/pay",
  auth,
  ah(async (req, res, next) => {
    const { user } = req;
    const {
      amount,
      comment,
      params: { callback, k1 }
    } = req.body;

    let url = `${callback}${callback.includes("?") ? "&" : "?"}amount=${amount *
      1000}${comment ? "&comment=" + comment : ""}`;
    console.log(url);

    try {
      const parts = callback.split("/");
      const secret = parts[parts.length - 1];
      payments[secret] = user;

      if (recipients[secret]) {
        url = `${req.protocol}://${req.get("host")}/api/send`;
        const { data } = await axios.post(
          url,
          {
            amount,
            memo: comment,
            username: recipients[secret].username
          },
          {
            headers: {
              Authorization: req.get("Authorization")
            }
          }
        );

        res.send(data);
      } else {
        const { data } = await axios.get(url);
        res.send(await send(amount, "", data.pr, user));
      }
    } catch (e) {
      l.error("failed to send payment", e.message);
      res.status(500).send(e.message);
    }
  })
);

app.get(
  "/login",
  optionalAuth,
  ah(async (req, res, next) => {
    try {
      const result = await lnurlServer.generateNewUrl("login");

      if (req.user) {
        logins[result.secret] = req.user.username;
      }

      res.send(result);
    } catch (e) {
      l.error("problem generating login url", e.message);
      res.status(500).send(e.message);
    }
  })
);

app.get(
  "/decode",
  ah(async (req, res, next) => {
    const { text } = req.query;

    try {
      const url = lnurl.decode(text);
      let spl = url.split("?");
      if (spl.length > 1 && qs.parse(spl[1]).tag === "login") {
        return res.send({
          tag: "login",
          k1: qs.parse(spl[1]).k1,
          callback: url,
          domain: url
            .split("://")[1]
            .split("/")[0]
            .split("@")
            .slice(-1)[0]
            .split(":")[0]
        });
      }

      const { data: params } = await axios.get(url);
      res.send(params);
    } catch (e) {
      l.error("problem decoding lnurl", e.message);
      res.status(500).send(e.message);
    }
  })
);

lnurlServer.bindToHook(
  "middleware:signedLnurl:afterCheckSignature",
  async (req, res, next) => {
    let user;
    const { amount: msats, key, tag, pr, k1 } = req.query;

    if (msats) {
      amount = Math.round(msats / 1000);
      const parts = req.originalUrl.split("/");
      const secret = parts[parts.length - 1].split("?")[0];
      user = payments[secret];

      if (user) {
        let account = await db.Account.findOne({
          where: {
            user_id: user.id,
            asset: config.liquid.btcasset,
            pubkey: null
          }
        });

        if (account.balance < amount) {
          throw new Error("Insufficient funds");
        }
      }

      const recipient = recipients[secret];

      if (recipient) {
        setTimeout(async () => {
          let { invoices } = await lnp.listInvoices({
            pending_only: true
          });

          if (invoices.length) {
            let invoice = await db.Invoice.create({
              user_id: recipient.id,
              text: invoices[invoices.length - 1].payment_request,
              rate: app.get("rates")[recipient.currency],
              currency: recipient.currency,
              amount,
              tip: 0,
              network: "lightning"
            });
            l.info("invoice created", invoice.text, invoice.amount);
          }
        }, 100);
      }
    }

    if (tag === "login") {
      let username = logins[k1];

      if (!username) {
        const keyObj = await db.Key.findOne({
          where: { hex: key },
          include: [
            {
              model: db.User,
              as: "user"
            }
          ]
        });

        if (keyObj) ({ user } = keyObj);

        if (!user) {
          let ip =
            req.headers["x-forwarded-for"] || req.connection.remoteAddress;

          let username = `satoshi-${key.substr(0, 12)}`;
          let existing = await db.User.findOne({ where: { username } });
          if (existing) user = existing;
          else {
            user = await register(
              {
                username,
                password: key
              },
              ip
            );
          }
        }

        ({ username } = user);
      }

      logins[key] = { k1, username };
    }

    if (pr) {
      user = withdrawals[k1];
      if (!user) {
        if (next) next(new Error("withdrawal not found"));
        return;
      }

      try {
        let decoded = await lnp.decodePayReq({ pay_req: pr });
        let amount = decoded.num_satoshis;

        await db.transaction(async transaction => {
          let account = await db.Account.findOne({
            where: {
              user_id: user.id,
              asset: config.liquid.btcasset
            },
            lock: transaction.LOCK.UPDATE,
            transaction
          });

          if (account.balance < amount) {
            throw new Error("Insufficient funds");
          }

          let payment = await db.Payment.create(
            {
              amount: -amount,
              account_id: account.id,
              user_id: user.id,
              hash: pr,
              rate: app.get("rates")[user.currency],
              currency: user.currency,
              confirmed: true,
              network: "lightning"
            },
            { transaction }
          );

          setTimeout(async () => {
            try {
              let { payments } = await lnp.listPayments({
                include_incomplete: false,
                max_payments: 5,
                reversed: true
              });

              let p = payments.find(p => p.payment_request === pr);
              if (p) {
                l.info("found payment", pr);
                payment.fee = p.fee;
                await account.decrement({ balance: p.fee }, { transaction });
                await account.reload({ transaction });
                await payment.save();
                payment = payment.get({ plain: true });
                payment.account = account.get({ plain: true });
                emit(user.username, "account", account);
                emit(user.username, "payment", payment);
              } else {
                l.warn("payment not found, fee not set", pr);
              }
            } catch (e) {
              l.error("problem trying to get ln withdrawal payment", e);
            }
          }, 1000);

          await account.decrement({ balance: amount }, { transaction });
          await account.reload({ transaction });

          let p = payment.get({ plain: true });
          p.account = account.get({ plain: true });

          emit(user.username, "account", p.account);
          emit(user.username, "payment", p);
        });
      } catch (e) {
        l.error("failed to process withdrawal", e.message);
        return next(e);
      }
    }

    if (next) next(req, res);
  }
);

lnurlServer.bindToHook("login", async key => {
  try {
    if (!key) throw new Error("login key not defined");

    const exists = await db.Key.findOne({
      where: { hex: key },
      include: [{ model: db.User, as: "user" }]
    });

    let user;
    if (logins[key] && logins[key] !== "undefined") {
      const { username } = logins[key];
      user = await db.User.findOne({
        where: { username }
      });

      if (user) {
        const [k, created] = await db.Key.findOrCreate({
          where: {
            user_id: user.id,
            hex: key
          }
        });

        if (created) {
          l.info("added key", username, k);
          emit(username, "key", k);
        }
      } else {
        l.info("user not found");
        user = await register({
          username: key.substr(0, 8),
          password: key
        });
      }
    } else if (exists) ({ user } = exists);

    if (user && user.username) {
      const payload = { username: user.username };
      const token = jwt.sign(payload, config.jwt);
      const ws = sessions[logins[key].k1];
      if (ws && ws.send)
        ws.send(JSON.stringify({ type: "token", data: { token, key } }));
    }
  } catch (e) {
    l.error("problem with login hook", e.message);
  }
});