Change the way standalone mode identity works, it does not use cookie anymore, only the local /secret file as other standard ssb clients, still if you have no .ssb folder locally it asks you to create or login, so new users have a more standard UX, with logout being possible as well

This commit is contained in:
Rogerio Chaves 2020-05-01 20:52:36 +02:00
parent 924e9a7ae0
commit 65578bf147
No known key found for this signature in database
GPG Key ID: E6AF5440509B1D94
6 changed files with 127 additions and 81 deletions

5
web/.env.sample Normal file
View File

@ -0,0 +1,5 @@
PORT=3000
SSB_KEY=
COOKIES_SECRET=
SENTRY_DSN=
SENDGRID_API_KEY=

View File

@ -1,4 +1,3 @@
let server; let server;
require("./lib/ssb"); require("./lib/ssb");
@ -6,8 +5,8 @@ setTimeout(() => {
server = require("./lib/express"); server = require("./lib/express");
}, 500); }, 500);
let mode = process.env.MODE || "client"; let mode = process.env.MODE || "standalone";
if (mode == "client") { if (mode == "standalone") {
setTimeout(() => { setTimeout(() => {
require("./lib/electron"); require("./lib/electron");
}, 1000); }, 1000);

View File

@ -8,6 +8,7 @@ const {
reconstructKeys, reconstructKeys,
uploadPicture, uploadPicture,
isPhone, isPhone,
ssbFolder,
} = require("./utils"); } = require("./utils");
const queries = require("./queries"); const queries = require("./queries");
const serveBlobs = require("./serve-blobs"); const serveBlobs = require("./serve-blobs");
@ -22,10 +23,12 @@ const cookieEncrypter = require("cookie-encrypter");
const expressLayouts = require("express-ejs-layouts"); const expressLayouts = require("express-ejs-layouts");
const mobileRoutes = require("./mobile-routes"); const mobileRoutes = require("./mobile-routes");
const ejsUtils = require("ejs/lib/utils"); const ejsUtils = require("ejs/lib/utils");
const fs = require("fs");
const ssbKeys = require("ssb-keys");
let mode = process.env.MODE || "client"; const mode = process.env.MODE || "standalone";
let profileUrl = (id, path = "") => { const profileUrl = (id, path = "") => {
return `/profile/${id}${path}`; return `/profile/${id}${path}`;
}; };
@ -43,16 +46,18 @@ app.set("view engine", "ejs");
app.set("views", `${__dirname}/../views`); app.set("views", `${__dirname}/../views`);
app.use(express.static(`${__dirname}/../public`)); app.use(express.static(`${__dirname}/../public`));
app.use(fileUpload()); app.use(fileUpload());
const cookieSecret =
process.env.COOKIES_SECRET || "set_cookie_secret_you_are_unsafe"; // has to be 32-bits
const cookieOptions = { const cookieOptions = {
httpOnly: true, httpOnly: true,
signed: true, signed: true,
expires: new Date(253402300000000), // Friday, 31 Dec 9999 23:59:59 GMT, nice date from stackoverflow expires: new Date(253402300000000), // Friday, 31 Dec 9999 23:59:59 GMT, nice date from stackoverflow
sameSite: "Lax", sameSite: "Lax",
}; };
app.use(cookieParser(cookieSecret)); if (mode != "standalone") {
if (mode != "client") { const cookieSecret =
process.env.COOKIES_SECRET || "set_cookie_secret_you_are_unsafe"; // has to be 32-bits
app.use(cookieParser(cookieSecret));
app.use(cookieEncrypter(cookieSecret)); app.use(cookieEncrypter(cookieSecret));
} }
app.use(expressLayouts); app.use(expressLayouts);
@ -71,28 +76,32 @@ app.use(async (req, res, next) => {
syncing: ssb.isSyncing(), syncing: ssb.isSyncing(),
}; };
res.locals.context = req.context; res.locals.context = req.context;
let key;
try { try {
const key = req.signedCookies["ssb_key"]; if (mode == "standalone") {
if (!key) return next(); const isLoggedOut = fs.existsSync(`${ssbFolder()}/logged-out`);
const parsedKey = JSON.parse(key); key = !isLoggedOut && ssbKeys.loadSync(`${ssbFolder()}/secret`);
if (!parsedKey.id) return next(); } else {
key = req.signedCookies["ssb_key"];
if (key) key = JSON.parse(key);
}
} catch (_) {}
if (!key || !key.id) return next();
ssb.client().identities.addUnboxer(parsedKey); ssb.client().identities.addUnboxer(key);
req.context.profile = await queries.getProfile(parsedKey.id); req.context.profile = (await queries.getProfile(key.id)) || {};
req.context.profile.key = parsedKey; req.context.profile.key = key;
const isRootUser = const isRootUser =
req.context.profile.id == ssb.client().id || req.context.profile.id == ssb.client().id ||
process.env.NODE_ENV != "production"; process.env.NODE_ENV != "production";
req.context.profile.debug = isRootUser; req.context.profile.debug = isRootUser;
req.context.profile.admin = isRootUser || mode == "client"; req.context.profile.admin = isRootUser || mode == "standalone";
next(); next();
} catch (e) {
next(e);
}
}); });
app.use((_req, res, next) => { app.use((_req, res, next) => {
res.locals.profileUrl = profileUrl; res.locals.profileUrl = profileUrl;
@ -174,20 +183,30 @@ router.get(
); );
const doLogin = async (submittedKey, res) => { const doLogin = async (submittedKey, res) => {
let decodedKey;
try { try {
const decodedKey = reconstructKeys(submittedKey); decodedKey = reconstructKeys(submittedKey);
res.cookie("ssb_key", JSON.stringify(decodedKey), cookieOptions);
decodedKey.private = "[removed]";
debug("Login with key", decodedKey);
await queries.autofollow(decodedKey.id);
res.redirect("/");
} catch (e) { } catch (e) {
debug("Error on login", e); debug("Error on login", e);
res.send("Invalid key"); return res.send("Invalid key");
} }
if (mode == "standalone") {
fs.unlinkSync(`${ssbFolder()}/secret`);
fs.writeFileSync(`${ssbFolder()}/secret`, submittedKey, {
mode: 0x100,
flag: "wx",
});
fs.unlinkSync(`${ssbFolder()}/logged-out`);
} else {
res.cookie("ssb_key", JSON.stringify(decodedKey), cookieOptions);
await queries.autofollow(decodedKey.id);
}
decodedKey.private = "[removed]";
debug("Login with key", decodedKey);
res.redirect("/");
}; };
router.get("/login", { public: true }, async (req, res) => { router.get("/login", { public: true }, async (req, res) => {
@ -215,7 +234,11 @@ router.get("/download", { public: true }, (_req, res) => {
}); });
router.get("/logout", async (_req, res) => { router.get("/logout", async (_req, res) => {
res.clearCookie("ssb_key"); if (mode == "standalone") {
fs.writeFileSync(`${ssbFolder()}/logged-out`, "");
} else {
res.clearCookie("ssb_key");
}
res.redirect("/"); res.redirect("/");
}); });
@ -234,7 +257,13 @@ router.post("/signup", { public: true }, async (req, res) => {
const pictureLink = picture && (await uploadPicture(ssb.client(), picture)); const pictureLink = picture && (await uploadPicture(ssb.client(), picture));
const key = await ssb.client().identities.createNewKey(); const key = await ssb.client().identities.createNewKey();
res.cookie("ssb_key", JSON.stringify(key), cookieOptions); if (mode == "standalone") {
fs.writeFileSync(`${ssbFolder()}/secret`, humanifyKey(key));
fs.unlinkSync(`${ssbFolder()}/logged-out`);
} else {
res.cookie("ssb_key", JSON.stringify(key), cookieOptions);
await queries.autofollow(key.id);
}
await ssb.client().identities.publishAs({ await ssb.client().identities.publishAs({
key, key,
@ -252,8 +281,6 @@ router.post("/signup", { public: true }, async (req, res) => {
debug("Published about", { about: key.id, name, image: pictureLink }); debug("Published about", { about: key.id, name, image: pictureLink });
await queries.autofollow(key.id);
res.redirect("/keys"); res.redirect("/keys");
}); });
@ -305,24 +332,28 @@ router.get("/keys/copy", (req, res) => {
}); });
}); });
const humanifyKey = (key) => {
return `
# WARNING: Never show this to anyone.
# WARNING: Never edit it or use it on multiple devices at once.
#
# This is your SECRET, it gives you magical powers. With your secret you can
# sign your messages so that your friends can verify that the messages came
# from you. If anyone learns your secret, they can use it to impersonate you.
#
# If you use this secret on more than one device you will create a fork and
# your friends will stop replicating your content.
#
${JSON.stringify(key)}
#
# The only part of this file that's safe to share is your public name:
#
# ${key.id}
`;
};
router.get("/keys/download", async (req, res) => { router.get("/keys/download", async (req, res) => {
const secretFile = ` const secretFile = humanifyKey(req.context.profile.key);
# WARNING: Never show this to anyone.
# WARNING: Never edit it or use it on multiple devices at once.
#
# This is your SECRET, it gives you magical powers. With your secret you can
# sign your messages so that your friends can verify that the messages came
# from you. If anyone learns your secret, they can use it to impersonate you.
#
# If you use this secret on more than one device you will create a fork and
# your friends will stop replicating your content.
#
${JSON.stringify(req.context.profile.key)}
#
# The only part of this file that's safe to share is your public name:
#
# ${req.context.profile.id}
`;
res.contentType("text/plain"); res.contentType("text/plain");
res.header("Content-Disposition", "attachment; filename=secret"); res.header("Content-Disposition", "attachment; filename=secret");

View File

@ -14,27 +14,32 @@ let ssbSecret = ssbKeys.loadOrCreateSync(
); );
let syncing = false; let syncing = false;
Client(ssbSecret, ssbConfig, async (err, server) => { const connectClient = (ssbSecret) => {
if (err) throw err; Client(ssbSecret, ssbConfig, async (err, server) => {
if (err) throw err;
ssbClient = server; ssbClient = server;
queries.progress(({ rate, feeds, incompleteFeeds, progress, total }) => { queries.progress(({ rate, feeds, incompleteFeeds, progress, total }) => {
if (incompleteFeeds > 0) { if (incompleteFeeds > 0) {
if (!syncing) debug("syncing"); if (!syncing) debug("syncing");
syncing = true; syncing = true;
} else { } else {
syncing = false; syncing = false;
} }
metrics.ssbProgressRate.set(rate); metrics.ssbProgressRate.set(rate);
metrics.ssbProgressFeeds.set(feeds); metrics.ssbProgressFeeds.set(feeds);
metrics.ssbProgressIncompleteFeeds.set(incompleteFeeds); metrics.ssbProgressIncompleteFeeds.set(incompleteFeeds);
metrics.ssbProgressProgress.set(progress); metrics.ssbProgressProgress.set(progress);
metrics.ssbProgressTotal.set(total); metrics.ssbProgressTotal.set(total);
});
console.log("SSB Client ready");
}); });
console.log("SSB Client ready"); };
});
module.exports.client = () => ssbClient; module.exports.client = () => ssbClient;
module.exports.isSyncing = () => syncing; module.exports.isSyncing = () => syncing;
module.exports.reconnectWith = connectClient;
connectClient(ssbSecret);

View File

@ -2,14 +2,14 @@ const fs = require("fs");
const path = require("path"); const path = require("path");
const { writeKey, ssbFolder } = require("./utils"); const { writeKey, ssbFolder } = require("./utils");
let envKey = const envKey =
process.env.SSB_KEY && process.env.SSB_KEY &&
Buffer.from(process.env.SSB_KEY, "base64").toString("utf8"); Buffer.from(process.env.SSB_KEY, "base64").toString("utf8");
if (envKey) { const secretExists = fs.existsSync(`${ssbFolder()}/secret`);
try {
writeKey(envKey, "/secret"); if (!secretExists && envKey) {
console.log("Writing SSB_KEY from env"); writeKey(envKey, "/secret");
} catch (_) {} console.log("Writing SSB_KEY from env");
if (!fs.existsSync(`${ssbFolder()}/gossip.json`)) { if (!fs.existsSync(`${ssbFolder()}/gossip.json`)) {
fs.copyFileSync("gossip.json", `${ssbFolder()}/gossip.json`); fs.copyFileSync("gossip.json", `${ssbFolder()}/gossip.json`);
} }
@ -44,3 +44,9 @@ fs.writeFileSync(
path.join(config.path, "manifest.json"), // ~/.ssb/manifest.json path.join(config.path, "manifest.json"), // ~/.ssb/manifest.json
JSON.stringify(manifest) JSON.stringify(manifest)
); );
// SSB server automatically creates a secret key, but we want the user flow where they choose to create a key or use an existing one
const mode = process.env.MODE || "standalone";
if (mode == "standalone" && !secretExists) {
fs.writeFileSync(`${ssbFolder()}/logged-out`, "");
}

View File

@ -5,7 +5,7 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "MODE=server SSB_PORT=8009 node index.js", "start": "MODE=server SSB_PORT=8009 node index.js",
"start:client": "electron .", "start:standalone": "electron .",
"start:user-2": "SSB_PORT=8010 PORT=3001 CONFIG_FOLDER=feedless-user2 electron .", "start:user-2": "SSB_PORT=8010 PORT=3001 CONFIG_FOLDER=feedless-user2 electron .",
"start:user-3": "SSB_PORT=8011 PORT=3002 CONFIG_FOLDER=feedless-user3 electron .", "start:user-3": "SSB_PORT=8011 PORT=3002 CONFIG_FOLDER=feedless-user3 electron .",
"clear": "rm -rf ~/.feedless; rm -rf ~/.feedless-user2; rm -rf ~/.feedless-user3", "clear": "rm -rf ~/.feedless; rm -rf ~/.feedless-user2; rm -rf ~/.feedless-user3",
@ -58,4 +58,4 @@
"devDependencies": { "devDependencies": {
"electron": "^8.2.0" "electron": "^8.2.0"
} }
} }