diff options
| author | Adam Soltys <[email protected]> | 2024-08-26 21:17:43 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2024-08-26 21:17:43 -0700 |
| commit | fe62cadd6fb4a7e49f7d59401fc74ae0411e32a7 (patch) | |
| tree | aa8f34e1692bd2d638a2c996eeadb58671d07aea /routes/users.ts | |
| parent | 00e43ddc0bf3d6dd719fc9295d156567fa2fecfb (diff) | |
| download | coinos-server-fe62cadd6fb4a7e49f7d59401fc74ae0411e32a7.tar.gz coinos-server-fe62cadd6fb4a7e49f7d59401fc74ae0411e32a7.zip | |
typescript
Diffstat (limited to 'routes/users.ts')
| -rw-r--r-- | routes/users.ts | 556 |
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({}); + }, +}; |
