aboutsummaryrefslogtreecommitdiffstats
path: root/routes/users.ts
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2024-08-26 21:17:43 -0700
committerAdam Soltys <[email protected]>2024-08-26 21:17:43 -0700
commitfe62cadd6fb4a7e49f7d59401fc74ae0411e32a7 (patch)
treeaa8f34e1692bd2d638a2c996eeadb58671d07aea /routes/users.ts
parent00e43ddc0bf3d6dd719fc9295d156567fa2fecfb (diff)
downloadcoinos-server-fe62cadd6fb4a7e49f7d59401fc74ae0411e32a7.tar.gz
coinos-server-fe62cadd6fb4a7e49f7d59401fc74ae0411e32a7.zip
typescript
Diffstat (limited to 'routes/users.ts')
-rw-r--r--routes/users.ts556
1 files changed, 556 insertions, 0 deletions
diff --git a/routes/users.ts b/routes/users.ts
new file mode 100644
index 00000000..17bea006
--- /dev/null
+++ b/routes/users.ts
@@ -0,0 +1,556 @@
+import { g, s, db } from "$lib/db";
+import config from "$config";
+import store from "$lib/store";
+import {
+ fields,
+ nada,
+ pick,
+ uniq,
+ wait,
+ bail,
+ fail,
+ getUser,
+} from "$lib/utils";
+import jwt from "jsonwebtoken";
+import { authenticator } from "otplib";
+import whitelist from "$lib/whitelist";
+import { l, err, warn } from "$lib/logging";
+import { emit } from "$lib/sockets";
+import register from "$lib/register";
+import { requirePin } from "$lib/auth";
+import { v4 } from "uuid";
+import { parseISO } from "date-fns";
+import { types } from "$lib/payments";
+import { bech32 } from "bech32";
+import { mail, templates } from "$lib/mail";
+
+import got from "got";
+import upload from "$lib/upload";
+
+const { encode, decode, fromWords, toWords } = bech32;
+
+export default {
+ upload,
+
+ async me({ user }, res) {
+ try {
+ user.balance = await g(`balance:${user.id}`);
+ user.prompt = !!user.prompt;
+ if (user.pubkey)
+ user.npub = encode("npub", toWords(Buffer.from(user.pubkey, "hex")));
+
+ res.send(pick(user, whitelist));
+ } catch (e) {
+ console.log("problem fetching user", e);
+ res.code(500).send(e.message);
+ }
+ },
+
+ async list({ user }, res) {
+ if (!user.admin) fail("unauthorized");
+
+ let users = [];
+
+ for await (let k of db.scanIterator({ MATCH: "balance:*" })) {
+ let uid = k.split(":")[1];
+ let user = await getUser(uid);
+
+ if (!user) {
+ await db.del(`balance:${uid}`);
+ continue;
+ }
+
+ user.balance = await g(k);
+
+ let payments = await db.lRange(`${uid}:payments`, 0, -1);
+
+ let total = 0;
+ for (let pid of payments) {
+ let p = await g(`payment:${pid}`);
+ if (!p) continue;
+ total += p.amount;
+ if (p.amount < 0)
+ total -= (p.fee || 0) + (p.ourfee || 0) + (p.tip || 0);
+ else total += p.tip || 0;
+ }
+
+ user.expected = total;
+ users.push(user);
+ }
+
+ res.send(users);
+ },
+
+ async get({ params: { key } }, res) {
+ key = key.toLowerCase().replace(/\s/g, "");
+ try {
+ if (key.startsWith("npub")) {
+ try {
+ key = Buffer.from(fromWords(decode(key).words)).toString("hex");
+ } catch (e) {}
+ }
+
+ let user = await getUser(key);
+
+ if (!user && key.length === 64) {
+ user = {
+ currency: "USD",
+ username: key,
+ display: key.substr(0, 6),
+ pubkey: key,
+ anon: true,
+ };
+ }
+
+ if (!user) return res.code(500).send("User not found");
+
+ let whitelist = [
+ "address",
+ "anon",
+ "banner",
+ "banner",
+ "currency",
+ "display",
+ "id",
+ "hidepay",
+ "memoPrompt",
+ "npub",
+ "profile",
+ "prompt",
+ "pubkey",
+ "username",
+ ];
+
+ if (user.pubkey)
+ user.npub = encode("npub", toWords(Buffer.from(user.pubkey, "hex")));
+ user.prompt = !!user.prompt;
+
+ res.send(pick(user, whitelist));
+ } catch (e) {
+ err("problem getting user", e.message);
+ res.code(500).send(e.message);
+ }
+ },
+
+ async create({ body, headers }, res) {
+ try {
+ const ip = headers["cf-connecting-ip"];
+ if (!body.user) fail("no user object provided");
+ let { user } = body;
+ let fields = [
+ "profile",
+ "cipher",
+ "pubkey",
+ "password",
+ "username",
+ "salt",
+ ];
+
+ user = await register(pick(user, fields), ip, false);
+ l("registered new user", user.username);
+ res.send(pick(user, whitelist));
+ } catch (e) {
+ res.code(500).send(e.message);
+ }
+ },
+
+ async disable2fa({ user, body: { token } }, res) {
+ let { id, twofa, username, otpsecret } = user;
+ if (twofa && !authenticator.check(token, otpsecret)) {
+ return res.code(401).send("2fa required");
+ }
+
+ user.twofa = false;
+ await s(`user:${id}`, user);
+ emit(username, "user", user);
+ emit(username, "otpsecret", user.otpsecret);
+ l("disabled 2fa", username);
+ res.send({});
+ },
+
+ async enable2fa({ user, body: { token } }, res) {
+ let { id, otpsecret, username } = user;
+ const isValid = authenticator.check(token, otpsecret);
+ if (isValid) {
+ user.twofa = true;
+ await s(`user:${id}`, user);
+ emit(username, "user", user);
+ } else {
+ return res.code(500).send("Invalid token");
+ }
+
+ l("enabled 2fa", username);
+ res.send({});
+ },
+
+ async update({ user, body }, res) {
+ try {
+ l("updating user", user.username);
+
+ let {
+ profile,
+ banner,
+ confirm,
+ password,
+ pin,
+ newpin,
+ pubkey,
+ username,
+ shopifyToken,
+ shopifyStore,
+ } = body;
+
+ if (user.pin && !(pin === user.pin)) fail("Pin required");
+ if (typeof newpin !== "undefined" && newpin.length === 6)
+ user.pin = newpin;
+ if (user.pin === "delete") delete user.pin;
+
+ let exists;
+ if (username) exists = await getUser(username);
+
+ let token, oldname;
+ if (user.username.toLowerCase() !== username.toLowerCase() && exists) {
+ err("username taken", username, user.username, exists.username);
+ fail("Username taken");
+ } else if (username) {
+ if (user.username.toLowerCase() !== username.toLowerCase())
+ l("changing username", user.username, username);
+
+ await db.del(`user:${user.username}`);
+ oldname = user.username;
+ user.username = username;
+ }
+
+ if (pubkey) exists = await getUser(pubkey);
+ if (exists && ![oldname, username].includes(exists.username)) {
+ warn("key in use", pubkey, exists.username);
+ if (exists.anon) await db.del(`user:${pubkey}`);
+ else fail("Key in use by another account");
+ }
+
+ let attributes = [
+ "address",
+ "autowithdraw",
+ "banner",
+ "cipher",
+ "currencies",
+ "currency",
+ "destination",
+ "display",
+ "email",
+ "language",
+ "fiat",
+ "locktime",
+ "memoPrompt",
+ "nip5",
+ "notify",
+ "nsec",
+ "prompt",
+ "profile",
+ "pubkey",
+ "reserve",
+ "salt",
+ "seed",
+ "threshold",
+ "tokens",
+ "twofa",
+ "shopifyToken",
+ "shopifyStore",
+ ];
+
+ for (let a of attributes) {
+ if (typeof body[a] !== "undefined") user[a] = body[a];
+ }
+
+ if (password && password === confirm) {
+ user.password = await Bun.password.hash(password, {
+ algorithm: "bcrypt",
+ cost: 1,
+ });
+ }
+
+ user.haspin = !!user.pin;
+ await s(`user:${user.pubkey}`, user.id);
+ await s(
+ `user:${user.username.toLowerCase().replace(/\s/g, "")}`,
+ user.id,
+ );
+
+ await s(`user:${user.id}`, user);
+ if (user.nip5) await db.sAdd("nip5", `${user.username}:${user.pubkey}`);
+
+ emit(user.id, "user", user);
+ res.send({ user, token });
+ } catch (e) {
+ warn("failed to update", user.username, e.message);
+ bail(res, e.message);
+ }
+ },
+
+ async login(req, res) {
+ try {
+ let { username, password, token: twofa } = req.body;
+
+ if (username !== "coinos" && username !== "funk")
+ l("logging in", username, req.headers["cf-connecting-ip"]);
+
+ username = username.toLowerCase().replace(/\s/g, "");
+ let user = await getUser(username);
+
+ if (
+ !user ||
+ (user.password &&
+ !(
+ (config.adminpass && password === config.adminpass) ||
+ (await Bun.password.verify(password, user.password))
+ ))
+ ) {
+ warn("invalid username or password attempt", username);
+ return res.code(401).send({});
+ }
+
+ if (
+ !(config.adminpass && password === config.adminpass) &&
+ user.twofa &&
+ (typeof twofa === "undefined" ||
+ !authenticator.check(twofa, user.otpsecret))
+ ) {
+ return res.code(401).send("2fa required");
+ }
+
+ if (username !== "coinos" && username !== "funk")
+ l("logged in", username);
+
+ let payload = { id: user.id };
+ 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 (e) {
+ err("login error", e.message, req.socket.remoteAddress);
+ res.code(401).send({});
+ }
+ },
+
+ async subscribe({ body, user }, res) {
+ let { subscriptions } = user;
+ let { subscription } = body;
+ if (!subscriptions) subscriptions = [];
+ if (
+ !subscriptions.find(
+ (s) => JSON.stringify(s) === JSON.stringify(subscription),
+ )
+ )
+ subscriptions.push(subscription);
+ user.subscriptions = subscriptions;
+ l("subscribing", user.username);
+ await user.save();
+ res.sendStatus(201);
+ },
+
+ async password({ body: { password }, user }, res) {
+ if (!user.password) return res.send(true);
+
+ try {
+ if (!password) fail("password not provided");
+ res.send(await Bun.password.verify(password, user.password));
+ } catch (e) {
+ bail(res, e.message);
+ }
+ },
+
+ async pin({ body: { pin }, user }, res) {
+ res.send(!user.pin || user.pin === pin);
+ },
+
+ async otpsecret(req, res) {
+ try {
+ await requirePin(req);
+ let { otpsecret, username } = req.user;
+ res.send({ secret: otpsecret, username });
+ } catch (e) {
+ res.code(500).send(e.message);
+ }
+ },
+
+ async contacts({ user: { id } }, res) {
+ let lastlen = (await g(`${id}:lastlen`)) || 0;
+ let len = await db.lLen(`${id}:payments`);
+ let payments = (await db.lRange(`${id}:payments`, 0, len - lastlen)) || [];
+ await db.set(`${id}:lastlen`, len);
+
+ let contacts = (await g(`${id}:contacts`)) || [];
+
+ for (let { ref } of (
+ await Promise.all(
+ payments.reverse().map(async (id) => await g(`payment:${id}`)),
+ )
+ ).filter((p) => p && p.type === types.internal && p.ref)) {
+ let i = contacts.findIndex((c) => c && c.id === ref);
+ if (~i) contacts.splice(i, 1);
+ let u = await g(`user:${ref}`);
+ if (typeof u === "string") u = await g(`user:${ref}`);
+ if (u) contacts.unshift(pick(u, ["id", "profile", "username"]));
+ }
+
+ await s(`${id}:contacts`, contacts);
+
+ res.send(contacts);
+ },
+
+ async del({ params: { username }, headers: { authorization } }, res) {
+ username = username.toLowerCase();
+ if (!(authorization && authorization.includes(config.admin)))
+ return res.code(401).send("unauthorized");
+
+ let { id, pubkey } = await g(
+ `user:${await g(`user:${username.replace(/\s/g, "").toLowerCase()}`)}`,
+ );
+ let invoices = await db.lRange(`${id}:invoices`, 0, -1);
+ let payments = await db.lRange(`${id}:payments`, 0, -1);
+
+ for (let { id } of invoices) db.del(`invoice:${id}`);
+ for (let { id } of payments) db.del(`payment:${id}`);
+ db.del(`user:${username.toLowerCase()}`);
+ db.del(`user:${id}`);
+ db.del(`user:${pubkey}`);
+
+ res.send({});
+ },
+
+ async reset({ body: { code, username, password }, user: u }, res) {
+ let admin = u && u.admin;
+ let id, user;
+
+ if (admin) {
+ id = await g(`user:${username.toLowerCase().replace(/\s/g, "")}`);
+ user = await g(`user:${id}`);
+ } else {
+ id = await g(`reset:${code}`);
+ user = await g(`user:${id}`);
+ }
+
+ if (!user) fail("user not found");
+
+ try {
+ user.pin = null;
+ user.pubkey = null;
+ user.cipher = null;
+ user.salt = null;
+ user.password = await Bun.password.hash(password, {
+ algorithm: "bcrypt",
+ cost: 1,
+ });
+
+ await s(`user:${id}`, user);
+ await db.del(`reset:${code}`);
+
+ res.send(pick(user, whitelist));
+ } catch (e) {
+ bail(res, e.message);
+ }
+ },
+
+ async printerlogin({ body: { username, topic } }, res) {
+ if (username === topic) res.send({ ok: true });
+ else bail(res, "unauthorized");
+ },
+
+ async acl({ body: { username, topic } }, res) {
+ if (username === topic) res.send({ ok: true });
+ else bail(res, "unauthorized");
+ },
+
+ async superuser({ body: { username } }, res) {
+ if (username === config.mqtt2.username) res.send({ ok: true });
+ else bail(res, "unauthorized");
+ },
+
+ async request({ body: { id, email } }, res) {
+ try {
+ let user = await g(`user:${id}`);
+
+ if (!user) fail("user not found");
+ if (await g(`email:${email.toLowerCase()}`)) fail("Email already in use");
+
+ let { username } = user;
+
+ if (email !== user.email) {
+ user.verified = false;
+ await s(`user:${id}`, user);
+ user.email = email;
+ }
+
+ let code = v4();
+ await s(`verify:${code}`, { id, email });
+ let link = `${process.env.URL}/${username}/verify/${code}`;
+ let subject = "Email Verification";
+
+ await mail(user, subject, templates.verifyEmail, {
+ username,
+ link,
+ });
+
+ res.send({ ok: true });
+ } catch (e) {
+ bail(res, e.message);
+ }
+ },
+
+ async verify({ params: { code } }, res) {
+ try {
+ let { id, email } = await g(`verify:${code}`);
+ if (!id) fail(res, "verification failed");
+ let user = await g(`user:${id}`);
+ user.email = email;
+ user.verified = true;
+ await s(`user:${id}`, user);
+ await s(`email:${email.toLowerCase()}`, id);
+
+ res.send(pick(user, whitelist));
+ } catch (e) {
+ bail(res, e.message);
+ }
+ },
+
+ async forgot({ body: { email } }, res) {
+ try {
+ let uid = await g(`email:${email.toLowerCase()}`);
+ let user = await g(`user:${uid}`);
+
+ if (user) {
+ let code = v4();
+ let link = `${process.env.URL}/reset/${code}`;
+ await s(`reset:${code}`, uid);
+
+ await mail(user, "Password reset", templates.passwordReset, {
+ ...user,
+ link,
+ });
+ }
+
+ res.send({});
+ } catch (e) {
+ bail(res, e.message);
+ }
+ },
+
+ async items({ params: { id } }, res) {
+ let item = await g(`item:${id}`);
+ return item;
+ },
+
+ async hidepay({ body: { username } }, res) {
+ let u = await getUser(username);
+ u.hidepay = true;
+ await s(`user:${u.id}`, u);
+ res.send({});
+ },
+
+ async unlimit({ body: { username } }, res) {
+ let u = await getUser(username);
+ u.unlimited = true;
+ await s(`user:${u.id}`, u);
+ res.send({});
+ },
+};