diff options
37 files changed, 1009 insertions, 1309 deletions
@@ -1,4 +1,40 @@ +import config from "$config"; import fastify from "fastify"; +import fastifyWs from "@fastify/websocket"; +import fastifyMultipart from "@fastify/multipart"; +import fastifyStatic from "@fastify/static"; +import fastifyPassport from "@fastify/passport"; +import fastifySecureSession from "@fastify/secure-session"; + +import path from "path"; import pino from "pino"; -export default fastify({ logger: pino(), disableRequestLogging: true, maxParamLength: 500 }); +import { jwtStrategy } from "$lib/auth"; +import { socketServer } from "$lib/sockets"; + +const app = fastify({ + logger: pino(), + disableRequestLogging: true, + maxParamLength: 500 +}); + +app.register(fastifySecureSession, { + key: Buffer.from(config.jwt, "hex") +}); + +app.register(fastifyPassport.initialize()); +app.register(fastifyPassport.secureSession()); + +fastifyPassport.use("jwt", jwtStrategy); + +app.register(fastifyMultipart); + +app.register(fastifyStatic, { + root: path.join("/app/uploads"), + prefix: "/public/" +}); + +app.register(fastifyWs); +app.register(socketServer); + +export default app; diff --git a/docker-compose.yml b/docker-compose.yml index 83214b92..02432156 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,6 +39,20 @@ services: volumes: - ./data/lightningb:/root/.lightning restart: always + db: + image: eqalpha/keydb + container_name: db + volumes: + - ./data/db:/data + restart: always + clc: + image: elementsproject/lightningd:latest + container_name: clc + environment: + LIGHTNINGD_NETWORK: regtest + volumes: + - ./data/lightningc:/root/.lightning + restart: always rd: image: eqalpha/keydb container_name: rd @@ -1,24 +1,71 @@ -import app from "./app.js"; -import { l, err } from "$lib/logging"; +import app from "$app"; +import { auth, optional } from "$lib/auth"; +import { getLocations } from "$lib/locations"; +import { getRates, sendRates } from "$lib/rates"; +import { sendHeartbeat } from "$lib/sockets"; -import "$lib/redis"; -import "$lib/sockets"; -import "$lib/passport"; -import "$lib/upload"; -import "$lib/rates"; -import "$lib/notifications"; -import "$lib/mail"; -import "$lib/nostr"; +import email from "$routes/email"; +import locations from "$routes/locations"; +import nostr from "$routes/nostr"; +import rates from "$routes/rates"; +import invoices from "$routes/invoices"; +import users from "$routes/users"; +import payments from "$routes/payments"; +import requests from "$routes/requests"; -import "$routes/locations"; -import "$routes/invoices"; -import "$routes/payments"; -import "$routes/users"; -import "$routes/requests"; +getLocations(); +getRates(); + +setInterval(sendRates, 1000); +setInterval(sendHeartbeat, 2000); + +app.post("/email", email.send); + +app.get("/rate", rates.last); +app.get("/rates", rates.index); + +app.get("/nostr.json", nostr.identities); +app.get("/:pubkey/followers", nostr.followers); +app.get("/:pubkey/follows", nostr.follows); +app.get("/:pubkey/notes", nostr.notes); +app.get("/event/:id", nostr.event); +app.post("/event", nostr.broadcast); + +app.get("/locations", locations.list); + +app.get("/invoice", invoices.get); +app.post("/invoice", optional, invoices.create); + +app.post("/send", auth, payments.send); +app.get("/payments", auth, payments.list); +app.get("/payments/:hash", auth, payments.get); +app.post("/lightning/send", auth, payments.sendLightning); + +app.get("/voucher/:id", payments.voucher); +app.post("/redeem", optional, payments.redeem); + +app.get("/me", auth, users.me); +app.get("/users/:key", users.get); +app.post("/register", users.create); +app.post("/disable2fa", users.disable2fa); +app.post("/2fa", auth, users.enable2fa); +app.post("/user", auth, users.update); +app.post("/upload/:type", auth, users.upload); + +app.post("/login", users.login); +app.post("/logout", optional, users.logout); + +app.post("/subscribe", auth, users.subscribe); +app.post("/password", auth, users.password); +app.post("/otpsecret", auth, users.otpsecret); +app.get("/contacts", auth, users.contacts); + +app.get("/request/:id", requests.get); +app.get("/requests", requests.list); +app.post("/requests", requests.create); +app.post("/requests/delete", requests.destroy); let host = process.env.HOST || "0.0.0.0"; let port = process.env.PORT || 3119; -app.listen({ host, port }, (e, h) => { - if (e) return err("Failed to start:", e.message); -}); +app.listen({ host, port }); diff --git a/lib/auth.js b/lib/auth.js new file mode 100644 index 00000000..9720cd83 --- /dev/null +++ b/lib/auth.js @@ -0,0 +1,37 @@ +import fastifyPassport from "@fastify/passport"; +import { g } from "$lib/db"; +import config from "$config"; +import jwt from "passport-jwt"; + +export const optional = { + preValidation: fastifyPassport.authenticate( + "jwt", + { session: false }, + function(req, res, err, user, info) { + req.user = user; + } + ) +}; + +export const auth = { + preValidation: fastifyPassport.authenticate("jwt", { + authInfo: false, + session: false + }) +}; + +export const requirePin = async ({ body, user }) => { + if (!user || (user.pin && user.pin !== body.pin)) + throw new Error("Invalid pin"); +}; + +export const jwtStrategy = new jwt.Strategy( + { + jwtFromRequest: jwt.ExtractJwt.fromExtractors([ + jwt.ExtractJwt.fromAuthHeaderAsBearerToken(), + req => (req.cookies ? req.cookies.token : null) + ]), + secretOrKey: config.jwt + }, + async (payload, next) => next(null, await g(`user:${payload.id}`)) +); diff --git a/lib/binance.js b/lib/binance.js deleted file mode 100644 index e583d312..00000000 --- a/lib/binance.js +++ /dev/null @@ -1,33 +0,0 @@ -import app from "$app"; -import WebSocket from "ws"; -import { err } from "./logging"; -import store from "./store"; - -export default () => { - const binance = new WebSocket( - "wss://stream.binance.com:9443/ws/btcusdt@ticker" - ); - - binance.onmessage = async function(event) { - try { - let msg = JSON.parse(event.data); - - if (!store.fx) return; - - Object.keys(store.fx).map(symbol => { - store.rates[symbol] = msg.c * store.fx[symbol]; - }); - - store.last = msg.c; - } catch (e) { - err("binance message error", e.message); - } - }; - - binance.onerror = async function(error) { - console.log(error); - err("binance socket error", error.message); - }; - - return binance; -}; diff --git a/lib/db.js b/lib/db.js new file mode 100644 index 00000000..76653c81 --- /dev/null +++ b/lib/db.js @@ -0,0 +1,14 @@ +import { createClient } from "redis"; + +export const db = createClient({ + url: "redis://rd" +}); + +db.on("error", err => console.log("db error", err)); + +await db.connect(); + +export default db; + +export let g = async k => JSON.parse(await db.get(k)); +export let s = (k, v) => db.set(k, JSON.stringify(v)); diff --git a/lib/lightning.js b/lib/lightning.js new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/lib/lightning.js diff --git a/lib/locations.js b/lib/locations.js new file mode 100644 index 00000000..bd2cfb5e --- /dev/null +++ b/lib/locations.js @@ -0,0 +1,20 @@ +import got from "got"; +import { s } from "$lib/db"; + +export let getLocations = async () => { + try { + let locations = await got( + "https://api.btcmap.org/v2/elements?updated_since=2022-09-19" + ).json(); + + locations = locations.filter( + l => + l["osm_json"].tags && l["osm_json"].tags["payment:coinos"] === "yes" + ) + + + await s("locations", locations); + } catch (e) {} + + setTimeout(getLocations, 60000); +}; diff --git a/lib/mail.js b/lib/mail.js deleted file mode 100644 index 966ce578..00000000 --- a/lib/mail.js +++ /dev/null @@ -1,29 +0,0 @@ -import app from "$app"; -import config from "$config"; - -app.post("/email", async (req, res) => { - try { - try { - var postmark = await import("postmark"); - - var client = new postmark.ServerClient(config.postmark); - - await client.sendEmail({ - From: "[email protected]", - To: "[email protected]", - Subject: req.body.subject || "Email Signup", - HtmlBody: JSON.stringify(req.body), - TextBody: JSON.stringify(req.body), - MessageStream: "outbound" - }); - - res.send({ ok: true }); - } catch (e) { - console.log("problem sending email", e); - res.code(500).send(e.message); - } - } catch (e) { - console.log(e); - res.code(500).send(e.message); - } -}); diff --git a/lib/nostr.js b/lib/nostr.js index 08ed8b61..93370587 100644 --- a/lib/nostr.js +++ b/lib/nostr.js @@ -3,16 +3,15 @@ import config from "$config"; import { Relay, RelayPool } from "nostr"; import { broadcast } from "$lib/sockets"; import store from "$lib/store"; -import redis from "$lib/redis"; +import { g, s, db } from "$lib/db"; import { nada, wait } from "$lib/utils"; export let pool; try { pool = RelayPool(config.relays); -} catch(e) { - console.log(e) -} - +} catch (e) { + console.log(e); +} pool.on("open", relay => { if (relay.url.includes(config.relay)) coinos = relay; @@ -32,7 +31,7 @@ export let q = async ( let start = Date.now(); let seen = []; let rejected; - query.since = await redis.get(`since:${sub}`); + query.since = await g(`since:${sub}`); if (now() - query.since < since) return r(); let done = { [sub]: [] }; @@ -56,7 +55,7 @@ export let q = async ( clearTimeout(this.timer); delete timeouts[sub]; pool.unsubscribe(sub); - await redis.set(`since:${sub}`, now()); + await s(`since:${sub}`, now()); }, extend(ev) { if (ev) { @@ -93,7 +92,7 @@ export let q = async ( let seen = []; let getUser = async pubkey => - JSON.parse(await redis.get(`user:${pubkey}`)) || { + JSON.parse(await g(`user:${pubkey}`)) || { username: pubkey.substr(0, 6), pubkey, anon: true @@ -141,37 +140,38 @@ pool.on("event", async (relay, sub, ev) => { updated: ev.created_at }; - await redis.set(`user:${pubkey}`, JSON.stringify(user)); + await s(`user:${pubkey}`, JSON.stringify(user)); coinos.send(ev); } else if (sub.includes("follows")) { let pubkey = sub.split(":")[0]; - await redis.set(`${pubkey}:follows`, JSON.stringify(ev.tags)); + await s(`${pubkey}:follows`, JSON.stringify(ev.tags)); for (let f of ev.tags) { let [_, followPubkey] = f; - await redis.sAdd(`${followPubkey}:followers`, pubkey); + await db.sAdd(`${followPubkey}:followers`, pubkey); } coinos.send(ev); } else if (sub.includes("followers")) { let followed = sub.split(":")[0]; let { pubkey } = ev; - await redis.sAdd(`${followed}:followers`, pubkey); + await db.sAdd(`${followed}:followers`, pubkey); coinos.send(ev); } else if (sub.includes("notes")) { let pubkey = sub.split(":")[0]; - await redis.sAdd(pubkey, ev.id); - await redis.set(`ev:${ev.id}`, JSON.stringify(ev)); + await db.sAdd(pubkey, ev.id); + await s(`ev:${ev.id}`, JSON.stringify(ev)); } } catch (e) { console.log(e); } }); -app.get("/event/:id", async (req, res) => { +export default { +async event (req, res) { try { - let event = JSON.parse(await redis.get(`ev:${req.params.id}`)); + let event = JSON.parse(await g(`ev:${req.params.id}`)); let { pubkey } = event; - event.user = JSON.parse(await redis.get(`user:${pubkey}`)) || { + event.user = JSON.parse(await g(`user:${pubkey}`)) || { username: pubkey.substr(0, 6), pubkey, anon: true, @@ -183,13 +183,13 @@ app.get("/event/:id", async (req, res) => { } catch (e) { res.code(500).send("Problem fetching event"); } -}); +}, -app.get("/:pubkey/notes", async (req, res) => { +async notes (req, res) { try { let { pubkey } = req.params; - let user = JSON.parse(await redis.get(`user:${pubkey}`)); + let user = JSON.parse(await g(`user:${pubkey}`)); if (!user) { user = await db.User.findOne({ @@ -215,12 +215,12 @@ app.get("/:pubkey/notes", async (req, res) => { { since: 0 } ).catch(nada); - await redis.set(`user:${pubkey}`, JSON.stringify(user)); + await g(`user:${pubkey}`, JSON.stringify(user)); - let ids = await redis.sMembers(pubkey); + let ids = await db.sMembers(pubkey); let events = ids.length - ? (await redis.mGet(ids.map(k => "ev:" + k))).map(JSON.parse) + ? (await db.mGet(ids.map(k => "ev:" + k))).map(JSON.parse) : []; res.send(events.map(e => ({ ...e, user }))); @@ -228,15 +228,15 @@ app.get("/:pubkey/notes", async (req, res) => { console.log(e); res.code(500).send("problem fetching user events"); } -}); +}, -app.post("/event", async (req, res) => { +async broadcast (req, res) { let { event } = req.body; pool.send(["EVENT", event]); res.send(event); -}); +}, -app.get("/:pubkey/follows", async (req, res) => { +async follows (req, res) { try { let { pubkey } = req.params; let { tagsonly } = req.query; @@ -252,7 +252,7 @@ app.get("/:pubkey/follows", async (req, res) => { { timeout: 60000, eager: 60000 } ).catch(nada); - let tags = JSON.parse(await redis.get(`${pubkey}:follows`)) || []; + let tags = JSON.parse(await g(`${pubkey}:follows`)) || []; if (tagsonly) return res.send(tags); let follows = []; @@ -265,7 +265,7 @@ app.get("/:pubkey/follows", async (req, res) => { authors: [pubkey] }).catch(nada); - let user = JSON.parse(await redis.get(`user:${pubkey}`)); + let user = JSON.parse(await g(`user:${pubkey}`)); if (!user) user = { @@ -287,14 +287,14 @@ app.get("/:pubkey/follows", async (req, res) => { } }); -app.get("/:pubkey/followers", async (req, res) => { +async (req, res) followers { try { let { pubkey } = req.params; let pubkeys = [ ...new Set([ ...(await got(`${config.nostr}/followers?pubkey=${pubkey}`).json()), - ...(await redis.sMembers(`${pubkey}:followers`)) + ...(await db.sMembers(`${pubkey}:followers`)) ]) ]; @@ -307,7 +307,7 @@ app.get("/:pubkey/followers", async (req, res) => { ).catch(nada); for (let pubkey of pubkeys) { - let user = JSON.parse(await redis.get(`user:${pubkey}`)); + let user = JSON.parse(await g(`user:${pubkey}`)); if (!user || !user.updated) { q(`${pubkey}:profile:f2`, { limit: 1, @@ -315,7 +315,7 @@ app.get("/:pubkey/followers", async (req, res) => { authors: [pubkey] }).catch(nada); - user = JSON.parse(await redis.get(`user:${pubkey}`)); + user = JSON.parse(await g(`user:${pubkey}`)); } if (!user) @@ -336,11 +336,15 @@ app.get("/:pubkey/followers", async (req, res) => { console.log(e); res.code(500).send(e && e.message); } -}); +}, -app.get('/nostr.json', async (req, res) => { +async identities (req, res) { res.send({ - names: { adam: "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8", asoltys: "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8" } + names: { + adam: "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8", + asoltys: + "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8" + } }); -}); - +} +} diff --git a/lib/passport.js b/lib/passport.js deleted file mode 100644 index 25dc0103..00000000 --- a/lib/passport.js +++ /dev/null @@ -1,79 +0,0 @@ -import app from "$app"; -import jwt from "passport-jwt"; -import fastifyPassport from "@fastify/passport"; -import fastifySecureSession from "@fastify/secure-session"; -import config from "$config"; -import fs from "fs"; -import path from "path"; -import { err } from "$lib/logging"; -import { g } from "$lib/redis"; - -app.register(fastifySecureSession, { - key: Buffer.from(config.jwt, "hex") -}); - -app.register(fastifyPassport.initialize()); -app.register(fastifyPassport.secureSession()); - -function cookieExtractor(req) { - if (req && req.cookies) { - return req.cookies["token"]; - } - return null; -} - -fastifyPassport.use( - "jwt", - new jwt.Strategy( - { - jwtFromRequest: jwt.ExtractJwt.fromExtractors([ - jwt.ExtractJwt.fromAuthHeaderAsBearerToken(), - cookieExtractor - ]), - secretOrKey: config.jwt - }, - async (payload, next) => { - try { - let { id } = payload; - let user = await g(`user:${id}`); - user.haspin = !!user.pin; - next(null, user); - } catch (e) { - console.log("payload", payload); - if (!e.message.includes("locked")) { - console.log(e); - err("problem logging in", e); - } - next(null, { username: "guest" }); - } - } - ) -); - -export const optionalAuth = { - preValidation: fastifyPassport.authenticate( - "jwt", - { session: false }, - function(req, res, err, user, info) { - req.user = user; - } - ) -}; - -export const adminAuth = { - preValidation: fastifyPassport.authenticate( - "jwt", - { session: false }, - function(req, res, err, user, info) { - if (!(user && user.admin)) return res.code(401).send("Unauthorized"); - req.user = user; - } - ) -}; - -export const auth = { - preValidation: fastifyPassport.authenticate("jwt", { - authInfo: false, - session: false - }) -}; diff --git a/lib/rates.js b/lib/rates.js index 8064a117..c7d6918c 100644 --- a/lib/rates.js +++ b/lib/rates.js @@ -1,69 +1,60 @@ -import app from "$app"; import got from "got"; import config from "$config"; -import fs from "fs"; -import binance from "./binance"; import { broadcast } from "./sockets"; import store from "./store"; import { err, warn } from "$lib/logging"; +import { g, s } from "$lib/db"; +import WebSocket from "ws"; -let b = binance(); -setInterval(() => { - b.terminate(); - b = binance(); -}, 360000); +let ws; +let connect = () => { + if (ws) ws.terminate(); + ws = new WebSocket("wss://stream.binance.com:9443/ws/btcusdt@ticker"); -let fetchRates = async () => { - let date, fx; + ws.onmessage = async function(event) { + try { + let msg = JSON.parse(event.data); - try { - ({ date, fx } = JSON.parse(fs.readFileSync("data/fx"))); - } catch (e) { - warn("Failed to read fx file"); - } + if (!store.fx) return; - let today = JSON.stringify(new Date()) - .replace(/T.*/, "") - .replace(/"/, ""); + Object.keys(store.fx).map(symbol => { + store.rates[symbol] = msg.c * store.fx[symbol]; + }); + + store.last = msg.c; + } catch (e) { + err("binance message error", e.message); + } + }; + + ws.onerror = async function(error) { + err("binance socket error", error.message); + }; + + setTimeout(connect, 360000); + return ws; +}; - if (date !== today) { +export let sendRates = () => broadcast("rate", store.last); +export let getRates = async () => { + connect(); + + let date, fx; + let rates = await g("rates"); + if (rates) ({ date, fx } = rates); + + if (Date.now() - date > 24 * 60 * 60 * 1000) { try { let { rates: fx } = await got( `https://api.exchangerate.host/latest?base=USD&v={(new Date()).toISOString().split('T')[0]}` ).json(); - fs.writeFileSync("data/fx", JSON.stringify({ date, fx })); + await s("rates", { date, fx }); } catch (e) { - try { - let { rates: fx } = await got( - `http://data.fixer.io/api/latest?access_key=${config.fixer}` - ).json(); - - let USD = fx["USD"]; - Object.keys(fx).map(k => { - fx[k] = parseFloat((fx[k] / fx["USD"]).toFixed(6)); - }); - - fs.writeFileSync("data/fx", JSON.stringify({ date, fx })); - } catch (e) { - err("error fetching rates", e.message); - } + err("error fetching rates", e.message); } } store.fx = fx; - setTimeout(fetchRates, 7200000); + setTimeout(getRates, 7200000); }; -fetchRates(); - -app.get("/rate", async (req, res, next) => { - res.send(store.last); -}); - -app.get("/rates", async (req, res, next) => { - res.send(store.rates); -}); - -setInterval(() => { - broadcast("rate", store.last); -}, 1000); diff --git a/lib/redis.js b/lib/redis.js deleted file mode 100644 index 6aec2af7..00000000 --- a/lib/redis.js +++ /dev/null @@ -1,15 +0,0 @@ -import { createClient } from "redis"; - -const redis = createClient({ - url: "redis://rd" -}); - -redis.on("error", err => console.log("redis error", err)); - -await redis.connect(); - -export default redis; - -export let g = async k => JSON.parse(await redis.get(k)); -export let s = (k, v) => redis.set(k, JSON.stringify(v)); -export let rd = redis; diff --git a/lib/register.js b/lib/register.js index 8ac5355f..63dccb38 100644 --- a/lib/register.js +++ b/lib/register.js @@ -4,7 +4,7 @@ import { authenticator } from "otplib"; import bcrypt from "bcrypt"; import { l } from "$lib/logging"; import { v4 } from "uuid"; -import { s, g } from "$lib/redis"; +import { s, g } from "$lib/db"; import { got } from "got"; export default async (user, ip, requireChallenge) => { diff --git a/lib/send.js b/lib/send.js deleted file mode 100644 index f378cfc1..00000000 --- a/lib/send.js +++ /dev/null @@ -1,201 +0,0 @@ -import config from "$config"; -import { emit } from "$lib/sockets"; -import store from "./store"; -import { - computeConversionFee, - conversionFeeReceiver -} from "../routes/lightning/conversionFee"; -import ln from "$lib/ln"; -import { l, warn, err } from "$lib/logging"; - -export default async (amount, memo, hash, user) => { - l("attempting lightning payment", user.username, amount, hash); - - // decode payreq with cl - if (!amount || payreq.satoshis > amount) amount = payreq.satoshis; - amount = parseInt(amount); - - if (store.seen.includes(hash)) { - warn("attempted to pay a paid invoice", user.username); - throw new Error("Invoice has been paid, can't pay again"); - } - - let error, fee, total, preimage, payment; - - let conversionFee = computeConversionFee(amount); - let conversionFeeDeduction; - await db.transaction(async transaction => { - l("starting lightning send transaction"); - let account; - if (user.account.asset === config.liquid.btcasset) - account = await db.Account.findOne({ - where: { - id: user.account.id, - pubkey: null - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - else - account = await db.Account.findOne({ - where: { - user_id: user.id, - asset: config.liquid.btcasset, - pubkey: null, - balance: { - [Op.gte]: amount - } - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - conversionFeeDeduction = Math.min(account.lightning_credits, conversionFee); - total = amount + conversionFee - conversionFeeDeduction; - - if (!account || account.balance < total) { - warn( - "user attempting to send more than they have", - user.id, - amount, - conversionFee - ); - throw new Error("Insufficient funds"); - } - - await account.decrement( - { balance: total, lightning_credits: conversionFeeDeduction }, - { transaction } - ); - await account.reload({ transaction }); - - l("debited account", account.id, total); - let tokens; - if (!payreq.satoshis) tokens = amount; - - let m; - let { msatoshi, msatoshi_sent, payment_preimage } = await ln.pay( - hash, - tokens ? `${tokens}sats` : undefined - ); - preimage = payment_preimage; - fee = parseInt((msatoshi_sent - msatoshi) / 1000); - - if (store.seen.includes(preimage)) { - warn("duplicate payment detected", preimage); - throw new Error("Duplicate payment detected"); - } - - store.seen.push(preimage); - store.seen.push(hash); - }); - - await db.transaction(async transaction => { - l("starting lightning fee transaction"); - - let account; - if (user.account.asset === config.liquid.btcasset) - account = await db.Account.findOne({ - where: { - id: user.account.id, - pubkey: null - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - else - account = await db.Account.findOne({ - where: { - user_id: user.id, - asset: config.liquid.btcasset, - pubkey: null, - balance: { - [Op.gte]: fee - } - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - l("deducting fee", fee); - await account.decrement({ balance: fee }, { transaction }); - await account.reload({ transaction }); - - if (conversionFeeDeduction) { - conversionFee -= conversionFeeDeduction; - } - - l("conversion fee", conversionFee); - - let receiverAccount = await db.Account.findOne({ - where: { - "$user.username$": conversionFeeReceiver - }, - include: [ - { - model: db.User, - as: "user" - } - ], - lock: transaction.LOCK.UPDATE, - transaction - }); - - let fee_payment; - let fee_payment_id = null; - if (conversionFee) { - await receiverAccount.increment( - { balance: conversionFee }, - { transaction } - ); - await receiverAccount.reload({ transaction }); - fee_payment = await db.Payment.create( - { - amount: conversionFee, - fee: 0, - memo: "Lightning conversion fee", - account_id: receiverAccount.id, - user_id: receiverAccount.user_id, - rate: store.rates[receiverAccount.user.currency], - currency: receiverAccount.user.currency, - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - fee_payment_id = fee_payment.id; - } - - payment = await db.Payment.create( - { - amount: -total, - account_id: account.id, - user_id: user.id, - hash, - memo, - preimage, - rate: store.rates[user.currency], - currency: user.currency, - confirmed: true, - network: "lightning", - fee, - fee_payment_id - }, - { transaction } - ); - - l("created payment record", payment.id); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - payment.fee_payment = fee_payment && fee_payment.get({ plain: true }); - - emit(user.username, "account", payment.account); - emit(user.username, "payment", payment); - - l("sent lightning", user.username, amount, total, fee); - }); - - return payment; -}; diff --git a/lib/sendInternal.js b/lib/sendInternal.js index f801c9cd..0ba0f0b4 100644 --- a/lib/sendInternal.js +++ b/lib/sendInternal.js @@ -7,12 +7,12 @@ import { notify } from "$lib/notifications"; import { l, err } from "$lib/logging"; import { fail } from "$lib/utils"; import ln from "$lib/ln"; -import { g, s, rd } from "$lib/redis"; +import { g, s, db } from "$lib/db"; const { HOSTNAME: hostname } = process.env; export default async ( - { amount, address, payreq, unconfidential, memo, username, tip }, + { amount, hash, memo, tip }, user ) => { amount = parseInt(amount); @@ -20,97 +20,77 @@ export default async ( if (!amount || amount < 0) fail("Amount must be greater than zero"); let fee = 0; - let recipient; + let invoice = await g(`invoice:${hash}`); let t = async () => { - await rd.watch(`user:${user_id}`); - user = await g(`user:${user_id}`); - if (user.balance < amount) throw new Error("Insufficient funds"); - + await db.watch(`user:${user.id}`); + user = await g(`user:${user.id}`); + if (user.balance < amount) fail("Insufficient funds"); user.balance -= amount; - let m = await rd.multi(); + let m = await db.multi(); await s(`user:${user.id}`, user); - if (username && (recipient = await g(`user:${username}`))) { - let { payment_hash: h } = await ln.decode(payreq); - let invoice = payreq && (await g(`invoice:${h}`)); + let p1 = { + hash, + amount: -amount, + memo, + uid: user.id, + rate: store.rates[user.currency], + currency: user.currency, + type: "internal", + created: Date.now() + }; + + await s(`payment:${hash}`, p1); + await db.lPush(`${user.id}:payments`, hash); + + l("sent internal", user.username, -p1.amount); + emit(user.id, "payment", p1); - let t = async () => { - await rd.watch(`user:${recipient.id}`); - recipient = await g(`user:${recipient.id}`); + let { uid } = invoice; + if (uid !== user.id) { + let recipient = await g(`user:${uid}`); + + let u = async () => { + await db.watch(`user:${uid}`); + recipient = await g(`user:${uid}`); recipient.balance += amount; - let m = await rd.multi(); - await s(`user:${recipient.id}`, recipient); - if (!(await m.exec())) t(); + let m = await db.multi(); + await s(`user:${uid}`, recipient); + if (!(await m.exec())) u(); }; - await t(); - - if (!(await m.exec())) t(); - } - await t(); + await u(); - let p2 = { - id: v4(), - amount, - user_id: recipient.id, - rate: store.rates[recipient.currency], - currency: recipient.currency, - memo, - type: "internal", - with_id: user.id, - created_at: Date.now() - }; + let p2 = { + hash, + amount, + uid: recipient.id, + rate: store.rates[recipient.currency], + currency: recipient.currency, + memo, + type: "internal", + with_id: user.id, + created: Date.now() + }; - if (invoice) { let { tip } = invoice; - params.invoice_id = invoice.id; if (tip) { - params.tip = tip; - params.amount -= tip; + p2.tip = tip; + p2.amount -= tip; } invoice.received += amount; - await s(`invoice:${h}`, invoice); - } - - if (invoice) p2.invoice = invoice; - await s(`payment:${p2.id}`, p2); - await rd.lPush(`${recipient.id}:payments`, p2.id); + await s(`invoice:${hash}`, invoice); + await s(`payment:${id}`, p2); + await db.lPush(`${recipient.id}:payments`, id); - emit(recipient.username, "payment", p2); - - l("received internal", recipient.username, amount); - notify(recipient, `Received ${amount} sats`); - callWebhook(invoice, p2); - }; + l("received internal", recipient.username, amount); + emit(recipient.username, "payment", p2); + notify(recipient, `Received ${amount} sats`); + callWebhook(invoice, p2); + } - let p1 = { - id: v4(), - amount: -amount, - memo, - user_id: user.id, - rate: store.rates[user.currency], - currency: user.currency, - with_id: recipient && recipient.id, - hash: `#${v4().substr(0, 6)} ${ - username ? `Payment to ${username}` : "Internal Transfer" - }`, - type: "internal", - created_at: Date.now() + if (!(await m.exec())) t(); }; - - if (!username) { - l("creating redeemable payment"); - params.redeemcode = v4(); - params.hash = `${hostname}/redeem/${params.redeemcode}`; - } - - await s(`payment:${p1.id}`, p1); - await rd.lPush(`${user.id}:payments`, p1.id); - - l("sent internal", user.username, -p1.amount); - - emit(user.username, "payment", p1); - - return p1; + await t(); }; diff --git a/lib/sockets.js b/lib/sockets.js index c94c29a0..7f655570 100644 --- a/lib/sockets.js +++ b/lib/sockets.js @@ -1,49 +1,42 @@ -import store from "$lib/store"; -import config from "$config"; import app from "$app"; +import config from "$config"; +import store from "$lib/store"; 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 { g } from "$lib/redis"; +import { g } from "$lib/db"; +import { pick } from "$lib/utils"; 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 = {}; +const users = {}; -export const emit = (uuid, type, data) => { - if (data.uuid !== undefined) data = pick(data, ...whitelist); - +export const emit = (uid, type, data) => { if (type === "payment" && data.amount > 0) { for (let i = subscriptions.length - 1; i >= 0; i--) { let s = subscriptions[i]; - if (s.invoice.id === data.invoice_id) { + if (s.hash === data.hash) { s.ws.send(JSON.stringify({ type, data })); } } } - if (!store.sockets[uuid]) return; - for (let id in store.sockets[uuid]) { - const ws = store.sockets[uuid][id]; + if (!store.sockets[uid]) return; + for (let id in store.sockets[uid]) { + const ws = store.sockets[uid][id]; ws.send(JSON.stringify({ type, data })); } }; export const broadcast = (type, data) => { const sent = []; - for (const uuid in store.sockets) { - for (const id in store.sockets[uuid]) { - const ws = store.sockets[uuid][id]; + for (const uid in store.sockets) { + for (const id in store.sockets[uid]) { + const ws = store.sockets[uid][id]; ws.send(JSON.stringify({ type, data })); sent.push(ws.id); } @@ -54,14 +47,10 @@ export const broadcast = (type, data) => { }); }; -function subtractSeconds(numOfSeconds, date = new Date()) { - date.setSeconds(date.getSeconds() - numOfSeconds); - return date; -} +export async function socketServer(app) { + let seconds = (n, d = new Date()) => d.setSeconds(d.getSeconds() - n) && d; -app.register(fastifyWs); -app.register(async function(app) { app.get("/ws", { websocket: true }, ({ socket: ws }, req) => { const id = v4(); ws.id = id; @@ -85,41 +74,42 @@ app.register(async function(app) { case "login": let token = data; - if (lastSeen[token] > subtractSeconds(5)) return; + if (lastSeen[token] > seconds(5)) return; lastSeen[token] = new Date(); try { if (!token || token === "null" || !jwt.decode(token)) return ws.close(code, `bad token ${token}`); - let { id: user_id } = jwt.decode(token); - if (!user_id) throw new Error("Invalid JWT token"); - let user = await g(`user:${user_id}`); + let { id: uid } = jwt.decode(token); + if (!uid) throw new Error("Invalid JWT token"); + let user = await g(`user:${uid}`); if (!user) throw new Error("User not found"); - user.haspin = !!user.pin; - if (!store.sockets[user_id]) store.sockets[user_id] = {}; - const existing = Object.keys(store.sockets[user_id]); + if (!store.sockets[uid]) store.sockets[uid] = {}; + const existing = Object.keys(store.sockets[uid]); if (existing.length > 4) { - const p = existing.find(sid => sid !== user_id); - store.sockets[user_id][p].send(JSON.stringify({ type: "logout" })); + const p = existing.find(sid => sid !== uid); + store.sockets[uid][p].send( + JSON.stringify({ type: "logout" }) + ); setTimeout(() => { - if (store.sockets[user_id][p]) - store.sockets[user_id][p].close(code, "too many sockets"); + if (store.sockets[uid][p]) + store.sockets[uid][p].close(code, "too many sockets"); }, 5000); } - store.sockets[user_id][id] = ws; - names[id] = user_id; + store.sockets[uid][id] = ws; + users[id] = uid; ws.user = user; ws.send( - JSON.stringify({ type: "login", data: pick(user, ...whitelist) }) + JSON.stringify({ type: "login", data: pick(user, whitelist) }) ); ws.send( JSON.stringify({ type: "version", data: config.clientVersion }) ); } catch (e) { - console.log(e) + console.log(e); err("caught socket error", e.message); if (e.message.includes("not found")) ws.send(JSON.stringify({ type: "logout" })); @@ -146,9 +136,9 @@ app.register(async function(app) { ws.on("close", function(code, reason) { try { - const uuid = names[id]; - if (store.sockets[uuid] && store.sockets[uuid][id]) { - delete store.sockets[uuid][id]; + const uid = users[id]; + if (store.sockets[uid] && store.sockets[uid][id]) { + delete store.sockets[uid][id]; } if (all[id]) delete all[id]; @@ -157,16 +147,15 @@ app.register(async function(app) { } }); }); -}); +} -const names = {}; -setInterval(() => { - for (const uuid in store.sockets) { - for (const id in store.sockets[uuid]) { - let ws = store.sockets[uuid][id]; +export let sendHeartbeat = () => { + for (const uid in store.sockets) { + for (const id in store.sockets[uid]) { + let ws = store.sockets[uid][id]; ws.beats++; - if (ws.beats > 10) ws.close(code, `${uuid} ${id} lost heartbeat`); + if (ws.beats > 10) ws.close(code, `${uid} ${id} lost heartbeat`); } } -}, 2000); +} diff --git a/lib/store.js b/lib/store.js index 6f0c1c97..bf71219e 100644 --- a/lib/store.js +++ b/lib/store.js @@ -2,7 +2,6 @@ export default { addresses: {}, challenge: {}, change: [], - exceptions: [], issuances: {}, logins: {}, nodes: [], diff --git a/lib/upload.js b/lib/upload.js index 95dfe21a..b37e0bf2 100644 --- a/lib/upload.js +++ b/lib/upload.js @@ -1,7 +1,3 @@ -import app from "$app"; -import { auth } from "./passport"; -import fastifyMultipart from "@fastify/multipart"; -import fastifyStatic from "@fastify/static"; import { fileTypeFromStream } from "file-type"; import pump from "pump"; import Clone from "readable-stream-clone"; @@ -9,21 +5,13 @@ import fs from "fs"; import path from "path"; import sharp from "sharp"; import { pipeline } from "stream"; +import { g, s } from "$lib/db"; -app.register(fastifyMultipart); - -app.register(fastifyStatic, { - root: path.join("/app/uploads"), - prefix: "/public/" -}); - -app.post("/upload/:type", auth, async function(req, res) { - try { - let { type } = req.params; +export default async ({ params: { type }, file, user }, res) => { if (!["banner", "profile"].includes(type)) throw new Error("invalid upload"); - let data = await req.file(); + let data = await file(); let s1 = new Clone(data.file); let s2 = new Clone(data.file); @@ -32,7 +20,7 @@ app.post("/upload/:type", auth, async function(req, res) { if (format !== "image" && !["jpg", "jpeg", "png"].includes(ext)) throw new Error("unsupported file type"); - let name = `${req.user.uuid}-${type}.webp`; + let name = `${user.id}-${type}.webp`; let path = `/app/uploads/${name}`; let size = type === "banner" ? 1920 : 240; @@ -43,12 +31,14 @@ app.post("/upload/:type", auth, async function(req, res) { await new Promise(r => pipeline(s2, t, fs.createWriteStream(path), r)); - req.user[type] = true; - await req.user.save(); + let r = async () => { + await db.watch(`user:${user.id}`); + user = await g(`user:${user.id}`); + user[type] = true; + let m = await db.multi(); + await s(`user:${user.id}`, user); + }; + await r(); res.send(name); - } catch (e) { - console.log(e); - return res.code(500).send(e); - } -}); +} diff --git a/lib/utils.js b/lib/utils.js index b556c46a..e53bf649 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,5 +1,5 @@ import store from "$lib/store"; -import { g } from "$lib/redis"; +import { g } from "$lib/db"; import config from "$config"; export const nada = () => {}; diff --git a/routes/email.js b/routes/email.js new file mode 100644 index 00000000..529307e6 --- /dev/null +++ b/routes/email.js @@ -0,0 +1,24 @@ +import config from "$config"; + +export default { + async send({ body }, res) { + try { + let postmark = await import("postmark"); + let client = new postmark.ServerClient(config.postmark); + + await client.sendEmail({ + From: "[email protected]", + To: "[email protected]", + Subject: body.subject || "Email Signup", + HtmlBody: JSON.stringify(body), + TextBody: JSON.stringify(body), + MessageStream: "outbound" + }); + + res.send({ ok: true }); + } catch (e) { + console.log("problem sending email", e); + res.code(500).send(e.message); + } + } +}; diff --git a/routes/events.js b/routes/events.js index c105dc40..cea82118 100644 --- a/routes/events.js +++ b/routes/events.js @@ -1,6 +1,6 @@ import app from "$app"; import db from "$db"; -import redis from "$lib/redis"; +import redis from "$lib/db"; import { coinos, pool, q } from "$lib/nostr"; import store from "$lib/store"; import { nada, wait } from "$lib/utils"; diff --git a/routes/invoices.js b/routes/invoices.js index 10a80b90..94b0d27d 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -1,21 +1,17 @@ -import app from "$app"; import store from "$lib/store"; -import { optionalAuth } from "$lib/passport"; import { l } from "$lib/logging"; import { emit } from "$lib/sockets"; -import { rd, g, s } from "$lib/redis"; +import { db, g, s } from "$lib/db"; import ln from "$lib/ln"; -app.get("/invoice", async ({ query: { id } }, res) => { - let invoice = await g(`invoice:${id}`); - invoice.user = await g(`user:${invoice.user_id}`); - res.send(invoice); -}); +export default { + async get({ query: { id } }, res) { + let invoice = await g(`invoice:${id}`); + invoice.user = await g(`user:${invoice.uid}`); + res.send(invoice); + }, -app.post( - "/invoice", - optionalAuth, - async ({ body: { invoice, user }, user: me }, res) => { + async create({ body: { invoice, user }, user: me }, res) { let { currency, tip, amount, rate, request_id } = invoice; if (amount < 0) throw new Error("amount out of range"); @@ -23,8 +19,8 @@ app.post( tip = tip || 0; if (!user) user = me; - let user_id = await g(`user:${user.username}`); - user = await g(`user:${user_id}`); + let uid = await g(`user:${user.username}`); + user = await g(`user:${uid}`); if (!user) throw new Error("user not provided"); @@ -46,9 +42,10 @@ app.post( currency, rate, tip, - user_id, + uid, text, - received: 0 + received: 0, + created: Date.now() }; l( @@ -61,7 +58,7 @@ app.post( ); await s(`invoice:${id}`, invoice); - await rd.lPush(`${user.id}:invoices`, id); + await db.lPush(`${uid}:invoices`, id); if (request_id) { let request = await g(`request:${request_id}`); @@ -74,4 +71,4 @@ app.post( res.send(invoice); } -); +}; diff --git a/routes/lightning/conversionFee.js b/routes/lightning/conversionFee.js deleted file mode 100644 index f6cf434c..00000000 --- a/routes/lightning/conversionFee.js +++ /dev/null @@ -1,12 +0,0 @@ -// Code for the conversion fee -// This is in a separate file so that the same function can be used by ./send and ./receive - -const conversionFeeMultiplier = 0.001; -export const conversionFeeReceiver = "coinosfees"; // account that receives the fees - -/** - * Returns the fee you have to pay for withdrawing 'amount' lightning BTC. - */ -export function computeConversionFee(amount) { - return Math.floor(amount * conversionFeeMultiplier); -} diff --git a/routes/lightning/index.js b/routes/lightning/index.js deleted file mode 100644 index ddc10bfe..00000000 --- a/routes/lightning/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import invoice from "./invoice"; -import query from "./query"; -import send from "./send"; -import parse from "./parse"; - -export default { - invoice, - query, - send, - parse -}; diff --git a/routes/lightning/invoice.js b/routes/lightning/invoice.js deleted file mode 100644 index ae974120..00000000 --- a/routes/lightning/invoice.js +++ /dev/null @@ -1,25 +0,0 @@ -import config from "$config"; -import { err } from "$lib/logging"; -import ln from "$lib/ln"; -import { v4 } from "uuid"; - -export default async (req, res) => { - let { amount, memo, tip } = req.body; - if (!tip) tip = 0; - let value = amount + tip; - - try { - if (!memo) memo = "coinos"; - const invoice = await ln.invoice( - value ? `${value}sat` : "any", - v4(), - memo, - 360 - ); - res.send({ text: invoice.bolt11 }); - } catch (e) { - console.log(e); - err("problem creating invoice", e.message); - res.code(500).send(e.message); - } -}; diff --git a/routes/lightning/parse.js b/routes/lightning/parse.js deleted file mode 100644 index 1b77019a..00000000 --- a/routes/lightning/parse.js +++ /dev/null @@ -1,19 +0,0 @@ -import ln from "$lib/ln"; -import store from "$lib/store"; - -export default async (req, res) => { - let { payreq } = req.body; - let hour = 1000 * 60 * 60; - let { last } = store.nodes; - let { nodes } = store; - - if (!last || last > Date.now() - hour) ({ nodes } = await ln.listnodes()); - store.nodes = nodes; - - let twoWeeksAgo = new Date(new Date().setDate(new Date().getDate() - 14)); - let { msatoshi, payee } = await ln.decodepay(payreq); - let node = nodes.find((n) => n.nodeid === payee); - let alias = node ? node.alias : payee.substr(0, 12); - - res.send({ alias, amount: Math.round(msatoshi / 1000) }); -}; diff --git a/routes/lightning/query.js b/routes/lightning/query.js deleted file mode 100644 index c5456a6f..00000000 --- a/routes/lightning/query.js +++ /dev/null @@ -1,19 +0,0 @@ -import { emit } from "$lib/sockets"; - -export default async (req, res) => { - const { payreq } = req.body; - - const invoice = await db.Invoice.findOne({ - include: { - attributes: ["username"], - model: db.User, - as: "user" - }, - where: { - text: payreq - } - }); - - if (invoice) emit(req.user.username, "to", invoice.user); - res.send({}); -}; diff --git a/routes/lightning/receive.js b/routes/lightning/receive.js deleted file mode 100644 index ffb0ad95..00000000 --- a/routes/lightning/receive.js +++ /dev/null @@ -1,92 +0,0 @@ -import { v4 } from "uuid"; -import store from "$lib/store"; -import { emit } from "$lib/sockets"; -import config from "$config"; -import { notify } from "$lib/notifications"; -import { callWebhook } from "$lib/webhooks"; -import { computeConversionFee } from "./conversionFee"; -import ln from "$lib/ln"; -import { l, err, warn } from "$lib/logging"; -import { g, s, rd } from "$lib/redis"; - -let handle = async () => { - let { - payment_hash, - bolt11: request, - pay_index, - status, - msatoshi_received, - payment_preimage: secret - } = await ln.waitanyinvoice(await g("pay_index")); - - await s("pay_index", pay_index); - - let received = Math.round(msatoshi_received / 1000); - - try { - l("incoming lightning payment", received); - - if (!secret) return; - let account, total, user; - - let invoice = await g(`invoice:${payment_hash}`); - if (!invoice) return warn("received lightning with no invoice", request); - - let { text: hash, currency, memo, rate, tip, user_id } = invoice; - let amount = parseInt(received) - tip; - if (amount < 0) throw new Error("amount out of range"); - - let id = v4(); - let payment = { - id, - user_id, - hash, - memo, - amount, - currency, - secret, - rate, - tip, - invoice_id: invoice.id, - created_at: Date.now() - }; - - total = amount + tip; - - invoice.status = "paid"; - invoice.received += total; - await s(`invoice:${payment_hash}`, invoice); - - let t = async () => { - await rd.watch(`user:${user_id}`); - user = await g(`user:${user_id}`); - user.balance += total; - let m = await rd.multi(); - await s(`user:${user.id}`, user); - if (!(await m.exec())) t(); - }; - await t(); - - payment.invoice = invoice; - - await s(`payment:${id}`, payment); - await rd.lPush(`${user.id}:payments`, id); - - callWebhook(invoice, payment); - - emit(user.username, "payment", payment); - notify(user, `Received ${total} SAT`); - - l("lightning payment received", user.username, payment.amount, payment.tip); - } catch (e) { - console.log(e); - err("problem receiving lightning payment", e.message); - } -}; - -let listen = async () => { - await handle(); - listen(); -}; - -listen(); diff --git a/routes/lightning/send.js b/routes/lightning/send.js deleted file mode 100644 index b41d5ed0..00000000 --- a/routes/lightning/send.js +++ /dev/null @@ -1,17 +0,0 @@ -import { err } from "$lib/logging"; -import send from "$lib/send"; -import { requirePin } from "$lib/utils"; - -export default async (req, res) => { - let { amount, route, memo, payreq } = req.body; - let { user } = req; - - try { - await requirePin(req); - res.send(await send(amount, memo, payreq, user)); - } catch (e) { - console.log(e); - err("problem sending lightning payment", user.username, payreq, e.message); - res.code(500).send(e.message); - } -}; diff --git a/routes/locations.js b/routes/locations.js index 1cebc883..e080a7b9 100644 --- a/routes/locations.js +++ b/routes/locations.js @@ -1,23 +1,7 @@ -import app from "$app"; -import redis from "$lib/redis"; -import got from "got"; +import { g } from "$lib/db"; -let locations = []; -app.get("/locations", async (req, res) => { - res.send({ locations }); -}); - -let getLocations = async () => { - try { - let r = await got( - "https://api.btcmap.org/v2/elements?updated_since=2022-09-19" - ).json(); - locations = r.filter( - l => l["osm_json"].tags && l["osm_json"].tags["payment:coinos"] === "yes" - ); - } catch(e) {} - - setTimeout(getLocations, 60000); -}; - -getLocations(); +export default { +async list(req, res) { + res.send({ locations: await g('locations') }); +} +} diff --git a/routes/nostr.js b/routes/nostr.js new file mode 100644 index 00000000..3d326e94 --- /dev/null +++ b/routes/nostr.js @@ -0,0 +1,161 @@ +export default { + async event({ params: { id } }, res) { + let event = JSON.parse(await g(`ev:${id}`)); + let { pubkey } = event; + + event.user = JSON.parse(await g(`user:${pubkey}`)) || { + username: pubkey.substr(0, 6), + pubkey, + anon: true, + follows: [], + followers: [] + }; + + res.send(event); + }, + + async notes({ params: { pubkey } }, res) { + let user = JSON.parse(await g(`user:${pubkey}`)); + + if (!user) { + user = await db.User.findOne({ + where: { pubkey } + }); + } + + if (!user) + user = { + username: pubkey.substr(0, 6), + pubkey, + anon: true, + follows: [], + followers: [] + }; + + let params = { + kinds: [1], + authors: [pubkey] + }, + opts = { since: 0 }; + + q(`${pubkey}:notes`, params, opts).catch(nada); + + await g(`user:${pubkey}`, JSON.stringify(user)); + + let ids = await db.sMembers(pubkey); + + let events = ids.length + ? (await db.mGet(ids.map(k => "ev:" + k))).map(JSON.parse) + : []; + + res.send(events.map(e => ({ ...e, user }))); + }, + + async broadcast(req, res) { + let { event } = req.body; + pool.send(["EVENT", event]); + res.send(event); + }, + + async follows({ params: { pubkey }, query: { tagsOnly } }, res) { + let sub = `${pubkey}:follows`, + params = { + limit: 1, + kinds: [3], + authors: [pubkey] + }, + opts = { timeout: 60000, eager: 60000 }; + + q(sub, params, opts).catch(nada); + + let tags = JSON.parse(await g(`${pubkey}:follows`)) || []; + if (tagsonly) return res.send(tags); + + let follows = []; + for (let f of tags) { + let [_, pubkey] = f; + + q(`${pubkey}:profile:f1`, { + limit: 1, + kinds: [0], + authors: [pubkey] + }).catch(nada); + + let user = JSON.parse(await g(`user:${pubkey}`)); + + if (!user) + user = { + username: pubkey.substr(0, 6), + pubkey, + anon: true + }; + + follows.push(user); + } + + follows = uniq(follows, e => e.pubkey); + follows.sort((a, b) => a.username.localeCompare(b.username)); + + res.send(follows); + }, + + async followers({ params: { pubkey}}, res) { + try { + let pubkeys = [ + ...new Set([ + ...(await got(`${config.nostr}/followers?pubkey=${pubkey}`).json()), + ...(await db.sMembers(`${pubkey}:followers`)) + ]) + ]; + + let followers = []; + + q( + `${pubkey}:followers`, + { kinds: [3], "#p": [pubkey] }, + { timeout: 60000, eager: 60000 } + ).catch(nada); + + for (let pubkey of pubkeys) { + let user = JSON.parse(await g(`user:${pubkey}`)); + if (!user || !user.updated) { + q(`${pubkey}:profile:f2`, { + limit: 1, + kinds: [0], + authors: [pubkey] + }).catch(nada); + + user = JSON.parse(await g(`user:${pubkey}`)); + } + + if (!user) + user = { + username: pubkey.substr(0, 6), + pubkey, + anon: true + }; + + followers.push(user); + } + + followers = uniq(followers, e => e.pubkey); + followers.sort((a, b) => a.username.localeCompare(b.username)); + + res.send(followers); + } catch (e) { + console.log(e); + res.code(500).send(e && e.message); + } + }, + + async identities(req, res) { + res.send({ + names: { + adam: + "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8", + asoltys: + "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8" + } + }); + } +}; diff --git a/routes/payments.js b/routes/payments.js index b328e763..13c0cdb7 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -1,29 +1,19 @@ -import app from "$app"; -import config from "$config"; -import store from "$lib/store"; -import { g, rd } from "$lib/redis"; - -import { auth, adminAuth, optionalAuth } from "$lib/passport"; -import fs from "fs"; -import { join } from "path"; -import send from "./send"; -import { err, l, warn } from "$lib/logging"; -import { fail } from "$lib/utils"; import { emit } from "$lib/sockets"; +import { db, g, s } from "$lib/db"; +import { l, err, warn } from "$lib/logging"; +import { fail } from "$lib/utils"; +import ln from "$lib/ln"; +import sendInternal from "$lib/sendInternal"; +import { requirePin } from "$lib/auth"; -import lnRoutes from "./lightning/index"; -import "./lightning/receive"; - -app.post("/send", auth, send); - -app.post("/lightning/parse", lnRoutes.parse); -app.post("/lightning/query", auth, lnRoutes.query); -app.post("/lightning/send", auth, lnRoutes.send); +export default { + async send(req, res) { + await requirePin(req); + let payment = await sendInternal(req.body, req.user); + res.send(payment); + }, -app.get( - "/payments", - auth, - async ({ user: { id }, query: { start, end, limit, offset } }, res) => { + async list({ user: { id }, query: { start, end, limit, offset } }, res) { if (limit) limit = parseInt(limit); if (offset) offset = parseInt(offset); @@ -31,144 +21,134 @@ app.get( // if (start) where.createdAt[Op.gte] = new Date(parseInt(start)); // if (end) where.createdAt[Op.lte] = new Date(parseInt(end)); - let payments = (await rd.lRange(`${id}:payments`, 0, -1)) || []; - payments = await Promise.all(payments.map(id => g(`payment:${id}`))); + let payments = (await db.lRange(`${id}:payments`, 0, -1)) || []; + payments = await Promise.all(payments.map(hash => g(`payment:${hash}`))); payments = payments.filter(p => p); res.send({ payments, total: payments.length }); - } -); + }, + + async voucher(req, res) { + try { + const { redeemcode } = req.params; + let payment = await g(`voucher:${id}`); + + payment = payment.get({ plain: true }); + payment.redeemer = payment["with"]; + + if (!payment) fail("invalid code"); + + res.send(payment); + } catch (e) { + res.code(500).send(e.message); + } + }, + + async get({ params: { hash } }, res) { + res.send(await g(`payment:${hash}`)); + }, + + async redeem({ body: { redeemcode } }, res) { + try { + await db.transaction(async transaction => { + if (!redeemcode) fail("no code provided"); + + let { user } = req; + + const source = await db.Payment.findOne({ + where: { + redeemcode: req.body.redeemcode + }, + include: { + model: db.Account, + as: "account" + }, + lock: transaction.LOCK.UPDATE, + transaction + }); + + l("redeeming", redeemcode); -// app.get("/voucher/:redeemcode", async (req, res) => { -// try { -// const { redeemcode } = req.params; -// let payment = await db.Payment.findOne({ -// where: { -// redeemcode -// }, -// include: { all: true } -// }); -// -// payment = payment.get({ plain: true }); -// payment.redeemer = payment["with"]; -// -// if (!payment) fail("invalid code"); -// -// res.send(payment); -// } catch (e) { -// res.code(500).send(e.message); -// } -// }); + if (!source) fail("Invalid code"); + if (source.redeemed) fail("Voucher has already been redeemed"); + let { amount } = source; + amount = -amount; -// let redeeming = {}; -// app.post("/redeem", optionalAuth, async function(req, res) { -// const { redeemcode } = req.body; -// try { -// await db.transaction(async transaction => { -// if (redeeming[redeemcode]) fail("redemption in progress"); -// redeeming[redeemcode] = true; -// if (!redeemcode) fail("no code provided"); -// -// let { user } = req; -// -// const source = await db.Payment.findOne({ -// where: { -// redeemcode: req.body.redeemcode -// }, -// include: { -// model: db.Account, -// as: "account" -// }, -// lock: transaction.LOCK.UPDATE, -// transaction -// }); -// -// l("redeeming", redeemcode); -// -// if (!source) fail("Invalid code"); -// if (source.redeemed) fail("Voucher has already been redeemed"); -// let { amount } = source; -// amount = -amount; -// -// if (!user) { -// const ip = -// req.headers["x-forwarded-for"] || req.connection.remoteAddress; -// -// user = await register( -// { -// username: redeemcode.substr(0, 8), -// password: "" -// }, -// ip, -// false -// ); -// -// let payload = { username: user.username }; -// let token = jwt.sign(payload, config.jwt); -// res.cookie("token", token, { -// expires: new Date(Date.now() + 432000000) -// }); -// -// delete redeeming[redeemcode]; -// return res.send({ user }); -// } -// -// let account = await getAccount(source.account.asset, user, transaction); -// let { hash, memo, confirmed, fee, network } = source; -// -// source.redeemed = true; -// (source.with_id = user.id), await source.save({ transaction }); -// -// let payment = await db.Payment.create( -// { -// amount, -// account_id: account.id, -// user_id: user.id, -// hash: "Voucher " + redeemcode, -// memo, -// rate: store.rates[user.currency], -// currency: user.currency, -// confirmed, -// network, -// received: true, -// fee, -// with_id: source.user_id -// }, -// { transaction } -// ); -// -// await account.increment({ balance: amount }, { transaction }); -// await account.reload({ transaction }); -// -// payment = payment.get({ plain: true }); -// payment.account = account.get({ plain: true }); -// emit(user.username, "payment", payment); -// emit(user.username, "account", account); -// -// res.send({ payment }); -// }); -// } catch (e) { -// delete redeeming[redeemcode]; -// console.log(e); -// err("problem redeeming", e.message); -// return res.code(500).send("There was a problem redeeming the voucher"); -// } -// }); -// -// app.post("/checkRedeemCode", auth, async function(req, res) { -// const { redeemcode } = req.body; -// -// const payment = await db.Payment.findOne({ where: { redeemcode } }); -// res.send(payment); -// }); -// -// app.get("/payments/:hash", auth, async function(req, res) { -// try { -// let payment = await db.Payment.findOne({ -// where: { user_id: req.user.id, hash: req.params.hash } -// }); -// -// return payment.get({ plain: true }); -// } catch (e) { -// console.log(e); -// } -// }); + if (!user) { + const ip = + req.headers["x-forwarded-for"] || req.connection.remoteAddress; + + user = await register( + { + username: redeemcode.substr(0, 8), + password: "" + }, + ip, + false + ); + + let payload = { username: user.username }; + let token = jwt.sign(payload, config.jwt); + res.cookie("token", token, { + expires: new Date(Date.now() + 432000000) + }); + + return res.send({ user }); + } + + let account = await getAccount(source.account.asset, user, transaction); + let { hash, memo, confirmed, fee, network } = source; + + source.redeemed = true; + (source.with_id = user.id), await source.save({ transaction }); + + let payment = await db.Payment.create( + { + amount, + account_id: account.id, + user_id: user.id, + hash: "Voucher " + redeemcode, + memo, + rate: store.rates[user.currency], + currency: user.currency, + confirmed, + network, + received: true, + fee, + with_id: source.user_id + }, + { transaction } + ); + + await account.increment({ balance: amount }, { transaction }); + await account.reload({ transaction }); + + payment = payment.get({ plain: true }); + payment.account = account.get({ plain: true }); + emit(user.username, "payment", payment); + emit(user.username, "account", account); + + res.send({ payment }); + }); + } catch (e) { + console.log(e); + err("problem redeeming", e.message); + return res.code(500).send("There was a problem redeeming the voucher"); + } + }, + + async sendLightning({ body: { payreq } }, res) { + let hour = 1000 * 60 * 60; + let { last } = store.nodes; + let { nodes } = store; + + if (!last || last > Date.now() - hour) ({ nodes } = await ln.listnodes()); + store.nodes = nodes; + + let twoWeeksAgo = new Date(new Date().setDate(new Date().getDate() - 14)); + let { msatoshi, payee } = await ln.decodepay(payreq); + let node = nodes.find(n => n.nodeid === payee); + let alias = node ? node.alias : payee.substr(0, 12); + + res.send({ alias, amount: Math.round(msatoshi / 1000) }); + } +}; diff --git a/routes/rates.js b/routes/rates.js new file mode 100644 index 00000000..c36c7ae8 --- /dev/null +++ b/routes/rates.js @@ -0,0 +1,11 @@ +import store from "$lib/store"; + +export default { + async last(req, res) { + res.send(store.last); + }, + + async index(req, res) { + res.send(store.rates); + } +}; diff --git a/routes/requests.js b/routes/requests.js index bea8f7fd..f62c3131 100644 --- a/routes/requests.js +++ b/routes/requests.js @@ -1,38 +1,34 @@ -import app from "$app"; -import { auth } from "$lib/passport"; import { emit } from "$lib/sockets"; -import { g, rd } from "$lib/redis"; +import { g, db } from "$lib/db"; -app.get("/request/:id", auth, async ({ params: { id } }, res) => { - try { - res.send({ request: await g(id) }); - } catch (e) { - console.log(e); - res.code(500).send(e.message); - } -}); +export default { + async get({ params: { id } }, res) { + try { + res.send({ request: await g(id) }); + } catch (e) { + console.log(e); + res.code(500).send(e.message); + } + }, -app.get("/requests", auth, async ({ user: { uuid } }, res) => { - try { - const day = new Date(new Date().setDate(new Date().getDate() - 1)); + async list({ user: { uuid } }, res) { + try { + const day = new Date(new Date().setDate(new Date().getDate() - 1)); - let invoices = await rd.lRange(`${uuid}:invoices`, 0, -1); - let requests = await rd.lRange(`${uuid}:requests`, 0, -1); + let invoices = await db.lRange(`${uuid}:invoices`, 0, -1); + let requests = await db.lRange(`${uuid}:requests`, 0, -1); - res.send({ invoices, requests }); - } catch (e) { - console.log(e); - res.code(500).send(e.message); - } -}); + res.send({ invoices, requests }); + } catch (e) { + console.log(e); + res.code(500).send(e.message); + } + }, -app.post( - "/requests", - auth, - async ( + async create( { body: { recipient, ...params }, user: { username, profile } }, res - ) => { + ) { let { id: recipient_id } = await g(`user:${recipient}`); let request = { recipient_id, ...params }; @@ -44,14 +40,10 @@ app.post( emit(recipient, "request", request); res.send(request); - } -); + }, -app.post( - "/requests/delete", - auth, - async ({ body: { request_id }, user: { id } }, res) => { - await rd.lrem(`user:${id}:requests`, request_id); + async destroy({ body: { request_id }, user: { id } }, res) { + await db.lrem(`user:${id}:requests`, request_id); res.send(); } -); +}; diff --git a/routes/send.js b/routes/send.js index 3f71a496..1bd4910a 100644 --- a/routes/send.js +++ b/routes/send.js @@ -1,6 +1,6 @@ import { err } from "$lib/logging"; import sendInternal from "$lib/sendInternal"; -import { requirePin } from "$lib/utils"; +import { requirePin } from "$lib/auth"; export default async (req, res, next) => { try { diff --git a/routes/users.js b/routes/users.js index c0324803..53b02d02 100644 --- a/routes/users.js +++ b/routes/users.js @@ -1,8 +1,7 @@ import app from "$app"; -import { g, s } from "$lib/redis"; +import { g, s } from "$lib/db"; import config from "$config"; import store from "$lib/store"; -import { auth, optionalAuth } from "$lib/passport"; import { nada, pick, uniq, wait } from "$lib/utils"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; @@ -13,310 +12,293 @@ import { emit } from "$lib/sockets"; import register from "$lib/register"; import { requirePin } from "$lib/utils"; import got from "got"; +import upload from "$lib/upload"; -app.get("/me", auth, async (req, res) => { - try { - res.send(pick(req.user, whitelist)); - } catch (e) { - console.log("problem fetching user", e); - res.code(500).send(e.message); - } -}); +export default { + upload, -app.get("/users/:key", async ({ params: { key } }, res) => { - try { - if (key.startsWith("npub")) { - key = Buffer.from(fromWords(decode(key).words)).toString("hex"); + async me({ user }, res) { + try { + user.balance = await g(`balance:${user.id}`); + res.send(pick(user, whitelist)); + } catch (e) { + console.log("problem fetching user", e); + res.code(500).send(e.message); } + }, - let user = await g(`user:${key}`); - if (typeof user === "string") { - user = await g(`user:${user}`); - } + async get({ params: { key } }, res) { + try { + if (key.startsWith("npub")) { + key = Buffer.from(fromWords(decode(key).words)).toString("hex"); + } - if (!user && key.length === 64) { - user = { - username: key.substr(0, 6), - pubkey: key, - anon: true - }; - } + let user = await g(`user:${key}`); + if (typeof user === "string") { + user = await g(`user:${user}`); + } - if (!user) return res.code(500).send("User not found"); + if (!user && key.length === 64) { + user = { + username: key.substr(0, 6), + pubkey: key, + anon: true + }; + } - let whitelist = [ - "username", - "banner", - "profile", - "address", - "currency", - "pubkey", - "display", - "prompt", - "id" - ]; + if (!user) return res.code(500).send("User not found"); - res.send(pick(user, whitelist)); - } catch (e) { - console.log(e); - res.code(500).send(e.message); - } -}); + let whitelist = [ + "username", + "banner", + "profile", + "address", + "currency", + "pubkey", + "display", + "prompt", + "id" + ]; -app.post("/register", async (req, res) => { - try { - const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress; - let { cipher, pubkey, password, username, salt } = req.body.user; + res.send(pick(user, whitelist)); + } catch (e) { + console.log(e); + res.code(500).send(e.message); + } + }, - let user = { - cipher, - pubkey, - password, - username, - salt - }; + async create(req, res) { + try { + const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress; + let { cipher, pubkey, password, username, salt } = req.body.user; - user = await register(user, ip, false); - l("registered new user", username); - res.send(pick(user, whitelist)); - } catch (e) { - res.code(500).send(e.message); - } -}); + let user = { + cipher, + pubkey, + password, + username, + salt + }; -app.post("/disable2fa", auth, async ({ user, body: { token } }, res) => { - let { id, twofa, username, otpsecret } = user; - if (twofa && !authenticator.check(token, otpsecret)) { - return res.code(401).send("2fa required"); - } + user = await register(user, ip, false); + l("registered new user", username); + res.send(pick(user, whitelist)); + } catch (e) { + res.code(500).send(e.message); + } + }, - user.twofa = false; - await s(`user:${id}`, user); - emit(username, "user", user); - emit(username, "otpsecret", user.otpsecret); - l("disabled 2fa", username); - res.send({}); -}); + 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"); + } -app.post("/2fa", auth, async ({ user, body: { token } }, res) => { - let { id, otpsecret, username } = user; - const isValid = authenticator.check(token, otpsecret); - if (isValid) { - user.twofa = true; + user.twofa = false; await s(`user:${id}`, user); emit(username, "user", user); - } else { - return res.code(500).send("Invalid token"); - } - - l("enabled 2fa", username); - res.send({}); -}); - -app.post("/user", auth, async ({ user, body: { confirm, password, pin, newpin, username }}, res) => { - try { - let { user } = req; - - l("updating user", user.username); - - if (user.pin && !(pin === user.pin)) throw new Error("Pin required"); - if (typeof newpin !== "undefined") user.pin = newpin; - if (!user.pin) user.pin = null; - - let exists; - if (username) exists = await g(`user:${username}`); + emit(username, "otpsecret", user.otpsecret); + l("disabled 2fa", username); + res.send({}); + }, - let token; - if (user.username !== username && exists) { - err("username taken", username, user.username, exists.username); - throw new Error("Username taken"); - } else if (username) { - if (user.username !== username) - l("changing username", user.username, username); - user.username = username; + 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"); } - let attributes = [ - "address", - "cipher", - "currencies", - "currency", - "display", - "email", - "fiat", - "locktime", - "prompt", - "pubkey", - "salt", - "seed", - "tokens", - "twofa", - "unit" - ]; + l("enabled 2fa", username); + res.send({}); + }, - for (let a of attributes) { - if (req.body[a]) user[a] = req.body[a]; - } - - if (password && password === confirm) { - user.password = await bcrypt.hash(password, 1); - } + async update( + { user, body: { confirm, password, pin, newpin, username } }, + res + ) { + try { + let { user } = req; - await user.save(); + l("updating user", user.username); - user = user.get({ plain: true }); - user.haspin = !!user.pin; + if (user.pin && !(pin === user.pin)) throw new Error("Pin required"); + if (typeof newpin !== "undefined") user.pin = newpin; + if (!user.pin) user.pin = null; - emit(user.username, "user", user); - res.send({ user, token }); - } catch (e) { - console.log(e); - err("error updating user", e.message); - res.code(500).send(e.message); - } -}); + let exists; + if (username) exists = await g(`user:${username}`); -let login = async (req, res) => { - try { - const { params, sig, key, password, username, token: twofa } = req.body; - l("logging in", username); + let token; + if (user.username !== username && exists) { + err("username taken", username, user.username, exists.username); + throw new Error("Username taken"); + } else if (username) { + if (user.username !== username) + l("changing username", user.username, username); + user.username = username; + } - if (sig) { - const { callback } = params; + let attributes = [ + "address", + "cipher", + "currencies", + "currency", + "display", + "email", + "fiat", + "locktime", + "prompt", + "pubkey", + "salt", + "seed", + "tokens", + "twofa", + "unit" + ]; - try { - const url = `${callback}&sig=${sig}&key=${key}`; - const response = await got(url).json(); - res.send(response); - } catch (e) { - err("problem calling lnurl login", e.message); - res.code(500).send(e.message); + for (let a of attributes) { + if (req.body[a]) user[a] = req.body[a]; } - return; - } + if (password && password === confirm) { + user.password = await bcrypt.hash(password, 1); + } - let id = await g(`user:${username}`); - let user = await g(`user:${id}`); + await s(`user:${user.id}`, user); - if ( - !user || - (user.password && !(await bcrypt.compare(password, user.password))) - ) { - warn("invalid username or password attempt", username); - return res.code(401).send({}); - } + user.haspin = !!user.pin; - if ( - user.twofa && - (typeof twofa === "undefined" || - !authenticator.check(twofa, user.otpsecret)) - ) { - return res.code(401).send("2fa required"); + emit(user.username, "user", user); + res.send({ user, token }); + } catch (e) { + console.log(e); + err("error updating user", e.message); + res.code(500).send(e.message); } + }, - l( - "login", - username, - req.headers["x-forwarded-for"] || req.socket.remoteAddress - ); + async login(req, res) { + try { + const { params, sig, key, password, username, token: twofa } = req.body; + l("logging in", username); - let payload = { username, 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) { - console.log(e); - err("login error", e.message, req.socket.remoteAddress); - res.code(401).send({}); - } -}; + if (sig) { + const { callback } = params; -app.post("/login", login); + try { + const url = `${callback}&sig=${sig}&key=${key}`; + const response = await got(url).json(); + res.send(response); + } catch (e) { + err("problem calling lnurl login", e.message); + res.code(500).send(e.message); + } -app.post("/logout", optionalAuth, async (req, res) => { - let { subscription } = req.body; - if (!subscription) return res.send({}); + return; + } - const { username } = req.user; + let id = await g(`user:${username}`); + let user = await g(`user:${id}`); - if (username) { - l("logging out", username); - let i = req.user.subscriptions.findIndex( - s => JSON.stringify(s) === subscription - ); - if (i > -1) { - req.user.subscriptions.splice(i, 1); - } - await req.user.save(); - Object.keys(logins).map( - k => logins[k]["username"] === username && delete logins[k] - ); - } + if ( + !user || + (user.password && !(await bcrypt.compare(password, user.password))) + ) { + warn("invalid username or password attempt", username); + return res.code(401).send({}); + } - res.send({}); -}); + if ( + user.twofa && + (typeof twofa === "undefined" || + !authenticator.check(twofa, user.otpsecret)) + ) { + return res.code(401).send("2fa required"); + } -app.get("/vapidPublicKey", function(req, res) { - res.send(config.vapid.publicKey); -}); + l( + "login", + username, + req.headers["x-forwarded-for"] || req.socket.remoteAddress + ); -app.post("/subscribe", auth, async function(req, res) { - let { subscriptions } = req.user; - let { subscription } = req.body; - if (!subscriptions) subscriptions = []; - if ( - !subscriptions.find(s => JSON.stringify(s) === JSON.stringify(subscription)) - ) - subscriptions.push(subscription); - req.user.subscriptions = subscriptions; - l("subscribing", req.user.username); - await req.user.save(); - res.sendStatus(201); -}); + let payload = { username, 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) { + console.log(e); + err("login error", e.message, req.socket.remoteAddress); + res.code(401).send({}); + } + }, -app.post("/password", auth, async function(req, res) { - const { user } = req; - const { password } = req.body; + async logout(req, res) { + let { subscription } = req.body; + if (!subscription) return res.send({}); - if (!user.password) return res.send(true); - res.send(await bcrypt.compare(password, user.password)); -}); + const { username } = req.user; -app.get("/invoices", auth, async function(req, res) { - let invoices = await db.Invoice.findAll({ - where: { user_id: req.user.id } - }); - res.send(invoices); -}); + if (username) { + l("logging out", username); + let i = req.user.subscriptions.findIndex( + s => JSON.stringify(s) === subscription + ); + if (i > -1) { + req.user.subscriptions.splice(i, 1); + } + await req.user.save(); + Object.keys(logins).map( + k => logins[k]["username"] === username && delete logins[k] + ); + } -app.post("/otpsecret", auth, async function(req, res) { - try { - await requirePin(req); - let { otpsecret, username } = req.user; - res.send({ secret: otpsecret, username }); - } catch (e) { - res.code(500).send(e.message); - } -}); + res.send({}); + }, -app.get("/contacts", auth, async function({ user: { uuid } }, res) { - let payments = (await g(`${uuid}:payments`)) || []; - res.send( - Promise.all( - [...new Set(payments.map(p => p.with_id))].map(id => g(`user:${id}`)) + 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); + res.send(await bcrypt.compare(password, user.password)); + }, + + 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); + } + }, -app.post("/checkPassword", auth, async function(req, res) { - try { - let { user } = req; - let result = await bcrypt.compare(req.body.password, user.password); - if (!result) throw new Error(); - res.send(result); - } catch (e) { - res.code(500).send("Invalid password"); + async contacts({ user: { uuid } }, res) { + let payments = (await g(`${uuid}:payments`)) || []; + res.send( + Promise.all( + [...new Set(payments.map(p => p.with_id))].map(id => g(`user:${id}`)) + ) + ); } -}); +}; |
