diff --git a/app/lib/express.js b/app/lib/express.js index b5008b1..14b8944 100644 --- a/app/lib/express.js +++ b/app/lib/express.js @@ -5,12 +5,20 @@ const bodyParser = require("body-parser"); const Client = require("ssb-client"); const ssbKeys = require("ssb-keys"); const ssbConfig = require("./ssb-config"); -const { asyncRouter, writeKey } = require("./utils"); +const { + asyncRouter, + writeKey, + nextIdentityFilename, + reconstructKeys, + readKey, +} = require("./utils"); const queries = require("./queries"); const serveBlobs = require("./serve-blobs"); const cookieParser = require("cookie-parser"); -const leftpad = require("left-pad"); // I don't believe I'm depending on this const debug = require("debug")("express"); +const fileUpload = require("express-fileupload"); +const pull = require("pull-stream"); +const split = require("split-buffer"); let ssbServer; let mode = process.env.MODE || "server"; @@ -39,6 +47,7 @@ app.use(bodyParser.urlencoded({ extended: true })); app.set("view engine", "ejs"); app.use(express.static("public")); app.use(cookieParser()); +app.use(fileUpload()); app.use(async (req, res, next) => { if (!ssbServer) { setTimeout(() => { @@ -64,8 +73,7 @@ app.use(async (req, res, next) => { const parsedKey = JSON.parse(key); if (!identities.includes(parsedKey.id)) { - const filename = - "secret_" + leftpad(identities.length - 1, 2, "0") + ".butt"; + const filename = await nextIdentityFilename(ssbServer); writeKey(key, `/identities/${filename}`); ssbServer.identities.refresh(); @@ -78,7 +86,7 @@ app.use(async (req, res, next) => { next(e); } }); -app.use((req, res, next) => { +app.use((_req, res, next) => { res.locals.profileUrl = profileUrl; res.locals.imageUrl = (blob) => { const imageHash = blob && typeof blob == "object" ? blob.link : blob; @@ -124,21 +132,6 @@ router.get("/login", (_req, res) => { router.post("/login", async (req, res) => { const submittedKey = req.body.ssb_key; - // From ssb-keys - const reconstructKeys = (keyfile) => { - var privateKey = keyfile - .replace(/\s*\#[^\n]*/g, "") - .split("\n") - .filter((x) => x) - .join(""); - - var keys = JSON.parse(privateKey); - const hasSigil = (x) => /^(@|%|&)/.test(x); - - if (!hasSigil(keys.id)) keys.id = "@" + keys.public; - return keys; - }; - try { const decodedKey = reconstructKeys(submittedKey); res.cookie("ssb_key", JSON.stringify(decodedKey)); @@ -158,10 +151,61 @@ router.get("/logout", async (_req, res) => { res.redirect("/"); }); -router.get("/signup", (_req, res) => { +router.get("/signup", (req, res) => { + if (req.context.profile) { + return res.redirect("/"); + } + res.render("signup"); }); +router.post("/signup", async (req, res) => { + const name = req.body.name; + const picture = req.files && req.files.pic; + + let pictureLink; + if (picture) { + const maxSize = 5 * 1024 * 1024; // 5 MB + if (picture.size > maxSize) throw "Max size exceeded"; + + pictureLink = await new Promise((resolve, reject) => + pull( + pull.values(split(picture.data, 64 * 1024)), + ssbServer.blobs.add((err, result) => { + if (err) return reject(err); + return resolve(result); + }) + ) + ); + } + + const filename = await nextIdentityFilename(ssbServer); + const profileId = await ssbServer.identities.create(); + const key = readKey(`/identities/${filename}`); + if (key.id != profileId) + throw "profileId and key.id don't match, probably race condition, bailing out for safety"; + + debug("Created new user with id", profileId); + + res.cookie("ssb_key", JSON.stringify(key)); + key.private = "[removed]"; + debug("Generated key", key); + + await ssbServer.identities.publishAs({ + id: profileId, + private: false, + content: { + type: "about", + about: profileId, + name: name, + ...(pictureLink ? { image: pictureLink } : {}), + }, + }); + debug("Published about", { about: profileId, name, image: pictureLink }); + + res.redirect("/"); +}); + router.get("/profile/:id", async (req, res) => { const id = req.params.id; @@ -179,10 +223,14 @@ router.get("/profile/:id", async (req, res) => { }); router.post("/publish", async (req, res) => { - await ssbServer.publish({ - type: "post", - text: req.body.message, - root: req.context.profile.id, + await ssbServer.identities.publishAs({ + id: req.context.profile.id, + private: false, + content: { + type: "post", + text: req.body.message, + root: req.context.profile.id, + }, }); res.redirect("/"); @@ -192,9 +240,13 @@ router.post("/publish", async (req, res) => { router.post("/vanish", async (req, res) => { const key = req.body.key; - await ssbServer.publish({ - type: "delete", - dest: key, + await ssbServer.identities.publishAs({ + id: req.context.profile.id, + private: false, + content: { + type: "delete", + dest: key, + }, }); res.send("ok"); @@ -205,19 +257,25 @@ router.post("/profile/:id/publish", async (req, res) => { const visibility = req.body.visibility; if (visibility == "vanishing") { - await ssbServer.private.publish( - { + await ssbServer.identities.publishAs({ + id: req.context.profile.id, + private: true, + content: { + type: "post", + text: req.body.message, + root: id, + recps: [id], + }, + }); + } else { + await ssbServer.identities.publishAs({ + id: req.context.profile.id, + private: false, + content: { type: "post", text: req.body.message, root: id, }, - [id] - ); - } else { - await ssbServer.publish({ - type: "post", - text: req.body.message, - root: id, }); } @@ -247,10 +305,14 @@ router.post("/about", async (req, res) => { const name = req.body.name; if (name != req.context.profile.name) { - await ssbServer.publish({ - type: "about", - about: req.context.profile.id, - name: name, + await ssbServer.identities.publishAs({ + id: req.context.profile.id, + private: false, + content: { + type: "about", + about: req.context.profile.id, + name: name, + }, }); req.context.profile.name = name; } diff --git a/app/lib/monkeypatch/ssb-identities.js b/app/lib/monkeypatch/ssb-identities.js index 6639700..fc17a93 100644 --- a/app/lib/monkeypatch/ssb-identities.js +++ b/app/lib/monkeypatch/ssb-identities.js @@ -1,4 +1,5 @@ -// Monkeypatched to include the refresh function +// 1) Monkeypatched to include the refresh function +// 2) Monkeypatched to allow secret messages without published in the recps var leftpad = require("left-pad"); var path = require("path"); var mkdirp = require("mkdirp"); @@ -94,15 +95,11 @@ exports.init = function (sbot, config) { else if (!content.recps && opts.private) return cb(new Error("opts.private set, but content.recps not set")); else if (!!content.recps && opts.private) { - if (!Array.isArray(content.recps) || !~recps.indexOf(id)) + if (!Array.isArray(content.recps) || !content.recps.length) return cb( new Error( - "content.recps must be an array containing publisher id:" + - id + - " was:" + - JSON.stringify(recps) + - " indexOf:" + - recps.indexOf(id) + "content.recps must be an array containing at least one id, was:" + + JSON.stringify(recps) ) ); content = ssbKeys.box(content, recps); diff --git a/app/lib/utils.js b/app/lib/utils.js index f58b49d..12b312f 100644 --- a/app/lib/utils.js +++ b/app/lib/utils.js @@ -1,4 +1,5 @@ const fs = require("fs"); +const leftpad = require("left-pad"); // I don't believe I'm depending on this module.exports.asyncRouter = (app) => { const debug = require("debug")("router"); @@ -22,13 +23,43 @@ module.exports.asyncRouter = (app) => { }; }; -module.exports.writeKey = (key, path) => { +const ssbFolder = () => { let homeFolder = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE; - let ssbFolder = `${homeFolder}/.${process.env.CONFIG_FOLDER || "social"}`; - let secretPath = `${ssbFolder}${path}`; + return `${homeFolder}/.${process.env.CONFIG_FOLDER || "social"}`; +}; + +module.exports.writeKey = (key, path) => { + let secretPath = `${ssbFolder()}${path}`; // Same options ssb-keys use - fs.mkdirSync(ssbFolder, { recursive: true }); + fs.mkdirSync(ssbFolder(), { recursive: true }); fs.writeFileSync(secretPath, key, { mode: 0x100, flag: "wx" }); }; + +module.exports.nextIdentityFilename = async (ssbServer) => { + const identities = await ssbServer.identities.list(); + return "secret_" + leftpad(identities.length - 1, 2, "0") + ".butt"; +}; + +// From ssb-keys +module.exports.reconstructKeys = (keyfile) => { + var privateKey = keyfile + .replace(/\s*\#[^\n]*/g, "") + .split("\n") + .filter((x) => x) + .join(""); + + var keys = JSON.parse(privateKey); + const hasSigil = (x) => /^(@|%|&)/.test(x); + + if (!hasSigil(keys.id)) keys.id = "@" + keys.public; + return keys; +}; + +module.exports.readKey = (path) => { + let secretPath = `${ssbFolder()}${path}`; + + let keyfile = fs.readFileSync(secretPath, "utf8"); + return module.exports.reconstructKeys(keyfile); +}; diff --git a/app/package-lock.json b/app/package-lock.json index 94372a3..97306f7 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -191,6 +191,14 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, + "busboy": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz", + "integrity": "sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==", + "requires": { + "dicer": "0.3.0" + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -557,6 +565,14 @@ "dev": true, "optional": true }, + "dicer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", + "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", + "requires": { + "streamsearch": "0.1.2" + } + }, "discontinuous-range": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", @@ -781,6 +797,14 @@ } } }, + "express-fileupload": { + "version": "1.1.7-alpha.3", + "resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.1.7-alpha.3.tgz", + "integrity": "sha512-2YRJQqjgfFcYiMr8inico+UQ0UsxuOUyO9wkWkx+vjsEcUI7c1ae38Nv5NKdGjHqL5+J01P6StT9mjZTI7Qzjg==", + "requires": { + "busboy": "^0.3.1" + } + }, "extract-zip": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", @@ -9637,6 +9661,11 @@ } } }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "string.prototype.trim": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz", diff --git a/app/package.json b/app/package.json index 86e709d..83c9d60 100644 --- a/app/package.json +++ b/app/package.json @@ -17,6 +17,7 @@ "debug": "^4.1.1", "ejs": "^3.0.2", "express": "^4.17.1", + "express-fileupload": "^1.1.7-alpha.3", "pull-stream": "^3.6.14", "ssb-about": "^2.0.1", "ssb-backlinks": "^1.0.0", diff --git a/app/public/style.css b/app/public/style.css index 5afe477..e4ebae7 100644 --- a/app/public/style.css +++ b/app/public/style.css @@ -48,7 +48,7 @@ input.button.button-primary:hover { color: #fff; } -input:not([type="submit"]), +input[type="text"], textarea { line-height: 32px; padding: 0 6px; diff --git a/app/views/home.ejs b/app/views/home.ejs index 3126fb3..9bb4714 100644 --- a/app/views/home.ejs +++ b/app/views/home.ejs @@ -54,7 +54,7 @@
- +
<% posts.map(post => { %> diff --git a/app/views/profile.ejs b/app/views/profile.ejs index 5f9dac4..ade5f8f 100644 --- a/app/views/profile.ejs +++ b/app/views/profile.ejs @@ -36,7 +36,7 @@ - + <% posts.map(post => { %> diff --git a/app/views/signup.ejs b/app/views/signup.ejs index e0f4b96..412fb48 100644 --- a/app/views/signup.ejs +++ b/app/views/signup.ejs @@ -3,7 +3,7 @@

Create account

-
+