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

View File

@ -8,6 +8,7 @@ const {
reconstructKeys,
uploadPicture,
isPhone,
ssbFolder,
} = require("./utils");
const queries = require("./queries");
const serveBlobs = require("./serve-blobs");
@ -22,10 +23,12 @@ const cookieEncrypter = require("cookie-encrypter");
const expressLayouts = require("express-ejs-layouts");
const mobileRoutes = require("./mobile-routes");
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}`;
};
@ -43,16 +46,18 @@ app.set("view engine", "ejs");
app.set("views", `${__dirname}/../views`);
app.use(express.static(`${__dirname}/../public`));
app.use(fileUpload());
const cookieSecret =
process.env.COOKIES_SECRET || "set_cookie_secret_you_are_unsafe"; // has to be 32-bits
const cookieOptions = {
httpOnly: true,
signed: true,
expires: new Date(253402300000000), // Friday, 31 Dec 9999 23:59:59 GMT, nice date from stackoverflow
sameSite: "Lax",
};
app.use(cookieParser(cookieSecret));
if (mode != "client") {
if (mode != "standalone") {
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(expressLayouts);
@ -71,28 +76,32 @@ app.use(async (req, res, next) => {
syncing: ssb.isSyncing(),
};
res.locals.context = req.context;
let key;
try {
const key = req.signedCookies["ssb_key"];
if (!key) return next();
if (mode == "standalone") {
const isLoggedOut = fs.existsSync(`${ssbFolder()}/logged-out`);
const parsedKey = JSON.parse(key);
if (!parsedKey.id) return next();
key = !isLoggedOut && ssbKeys.loadSync(`${ssbFolder()}/secret`);
} else {
key = req.signedCookies["ssb_key"];
if (key) key = JSON.parse(key);
}
} catch (_) {}
if (!key || !key.id) return next();
ssb.client().identities.addUnboxer(parsedKey);
req.context.profile = await queries.getProfile(parsedKey.id);
req.context.profile.key = parsedKey;
ssb.client().identities.addUnboxer(key);
req.context.profile = (await queries.getProfile(key.id)) || {};
req.context.profile.key = key;
const isRootUser =
req.context.profile.id == ssb.client().id ||
process.env.NODE_ENV != "production";
const isRootUser =
req.context.profile.id == ssb.client().id ||
process.env.NODE_ENV != "production";
req.context.profile.debug = isRootUser;
req.context.profile.admin = isRootUser || mode == "client";
req.context.profile.debug = isRootUser;
req.context.profile.admin = isRootUser || mode == "standalone";
next();
} catch (e) {
next(e);
}
next();
});
app.use((_req, res, next) => {
res.locals.profileUrl = profileUrl;
@ -174,20 +183,30 @@ router.get(
);
const doLogin = async (submittedKey, res) => {
let decodedKey;
try {
const 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("/");
decodedKey = reconstructKeys(submittedKey);
} catch (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) => {
@ -215,7 +234,11 @@ router.get("/download", { public: true }, (_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("/");
});
@ -234,7 +257,13 @@ router.post("/signup", { public: true }, async (req, res) => {
const pictureLink = picture && (await uploadPicture(ssb.client(), picture));
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({
key,
@ -252,8 +281,6 @@ router.post("/signup", { public: true }, async (req, res) => {
debug("Published about", { about: key.id, name, image: pictureLink });
await queries.autofollow(key.id);
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) => {
const secretFile = `
# 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}
`;
const secretFile = humanifyKey(req.context.profile.key);
res.contentType("text/plain");
res.header("Content-Disposition", "attachment; filename=secret");

View File

@ -14,27 +14,32 @@ let ssbSecret = ssbKeys.loadOrCreateSync(
);
let syncing = false;
Client(ssbSecret, ssbConfig, async (err, server) => {
if (err) throw err;
const connectClient = (ssbSecret) => {
Client(ssbSecret, ssbConfig, async (err, server) => {
if (err) throw err;
ssbClient = server;
ssbClient = server;
queries.progress(({ rate, feeds, incompleteFeeds, progress, total }) => {
if (incompleteFeeds > 0) {
if (!syncing) debug("syncing");
syncing = true;
} else {
syncing = false;
}
queries.progress(({ rate, feeds, incompleteFeeds, progress, total }) => {
if (incompleteFeeds > 0) {
if (!syncing) debug("syncing");
syncing = true;
} else {
syncing = false;
}
metrics.ssbProgressRate.set(rate);
metrics.ssbProgressFeeds.set(feeds);
metrics.ssbProgressIncompleteFeeds.set(incompleteFeeds);
metrics.ssbProgressProgress.set(progress);
metrics.ssbProgressTotal.set(total);
metrics.ssbProgressRate.set(rate);
metrics.ssbProgressFeeds.set(feeds);
metrics.ssbProgressIncompleteFeeds.set(incompleteFeeds);
metrics.ssbProgressProgress.set(progress);
metrics.ssbProgressTotal.set(total);
});
console.log("SSB Client ready");
});
console.log("SSB Client ready");
});
};
module.exports.client = () => ssbClient;
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 { writeKey, ssbFolder } = require("./utils");
let envKey =
const envKey =
process.env.SSB_KEY &&
Buffer.from(process.env.SSB_KEY, "base64").toString("utf8");
if (envKey) {
try {
writeKey(envKey, "/secret");
console.log("Writing SSB_KEY from env");
} catch (_) {}
const secretExists = fs.existsSync(`${ssbFolder()}/secret`);
if (!secretExists && envKey) {
writeKey(envKey, "/secret");
console.log("Writing SSB_KEY from env");
if (!fs.existsSync(`${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
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",
"scripts": {
"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-3": "SSB_PORT=8011 PORT=3002 CONFIG_FOLDER=feedless-user3 electron .",
"clear": "rm -rf ~/.feedless; rm -rf ~/.feedless-user2; rm -rf ~/.feedless-user3",
@ -58,4 +58,4 @@
"devDependencies": {
"electron": "^8.2.0"
}
}
}