Add keys emailing/copying/downloading flow for users to be able to sign back in later

This commit is contained in:
Rogerio Chaves 2020-04-16 21:54:01 +02:00
parent 58a103adbb
commit 9e2b5fa9f1
No known key found for this signature in database
GPG Key ID: E6AF5440509B1D94
13 changed files with 292 additions and 25 deletions

View File

@ -12,6 +12,8 @@ const {
reconstructKeys,
readKey,
uploadPicture,
identityFilename,
ssbFolder,
} = require("./utils");
const queries = require("./queries");
const serveBlobs = require("./serve-blobs");
@ -20,6 +22,8 @@ const debug = require("debug")("express");
const fileUpload = require("express-fileupload");
const Sentry = require("@sentry/node");
const metrics = require("./metrics");
const sgMail = require("@sendgrid/mail");
const ejs = require("ejs");
let ssbServer;
let mode = process.env.MODE || "client";
@ -60,7 +64,7 @@ let profileUrl = (id, path = "") => {
};
const SENTRY_DSN = process.env.SENTRY_DSN;
if (SENTRY_DSN) {
if (SENTRY_DSN && process.env.NODE_ENV == "production") {
Sentry.init({
dsn: SENTRY_DSN,
});
@ -128,9 +132,6 @@ router.get("/", async (req, res) => {
if (!req.context.profile) {
return res.render("index");
}
if (!req.context.profile.name) {
return res.redirect("/about");
}
const [posts, friends, vanishingMessages] = await Promise.all([
queries.getPosts(ssbServer, req.context.profile),
@ -162,6 +163,8 @@ router.post("/login", async (req, res) => {
decodedKey.private = "[removed]";
debug("Login with key", decodedKey);
await queries.autofollow(ssbServer, decodedKey.id);
res.redirect("/");
} catch (e) {
debug("Error on login", e);
@ -204,21 +207,54 @@ router.post("/signup", async (req, res) => {
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 });
await queries.autofollow(ssbServer, profileId);
res.redirect("/");
});
router.get("/keys", (req, res) => {
res.render("keys", {
useEmail: process.env.SENDGRID_API_KEY,
key: req.cookies["ssb_key"],
});
});
router.post("/keys/email", async (req, res) => {
const email = req.body.email;
let html = await ejs.renderFile("views/email_sign_in.ejs", {
host: `http://${req.headers.host}`,
ssb_key: req.cookies["ssb_key"],
});
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const msg = {
to: email,
from: "nobody@social.com",
subject: `Login button for ${req.context.profile.name}`,
html,
};
await sgMail.send(msg);
res.render("keys_sent");
});
router.get("/keys/copy", (req, res) => {
res.render("keys_copy", { key: req.cookies["ssb_key"] });
});
router.get("/keys/download", async (req, res) => {
const identities = await ssbServer.identities.list();
const index = identities.indexOf(req.context.profile.id) - 1;
const filename = identityFilename(index);
const secretPath = `${ssbFolder()}/identities/${filename}`;
res.attachment("secret");
res.sendFile(secretPath);
});
router.get("/profile/:id(*)", async (req, res) => {
const id = req.params.id;
@ -423,7 +459,7 @@ router.get("/metrics", (_req, res) => {
res.end(metrics.register.metrics());
});
if (SENTRY_DSN) {
if (SENTRY_DSN && process.env.NODE_ENV == "production") {
// The error handler must be before any other error middleware and after all controllers
app.use(Sentry.Handlers.errorHandler());
}

View File

@ -305,15 +305,30 @@ const getProfile = async (ssbServer, id) => {
const progress = (ssbServer, callback) => {
pull(
ssbServer.replicate.changes(),
pull.drain(
callback,
(err) => {
console.error("Progress drain error", err);
}
)
pull.drain(callback, (err) => {
console.error("Progress drain error", err);
})
);
};
const autofollow = async (ssbServer, id) => {
console.log("ssbServer.id", ssbServer.id);
const isFollowing = await ssbServer.friends.isFollowing({
source: ssbServer.id,
dest: id,
});
if (!isFollowing) {
await ssbServer.publish({
type: "contact",
contact: id,
following: true,
autofollow: true,
});
}
};
setInterval(() => {
debugProfile("Clearing profile cache");
profileCache = {};
@ -330,4 +345,5 @@ module.exports = {
profileCache,
getFriendshipStatus,
progress,
autofollow,
};

View File

@ -44,9 +44,13 @@ module.exports.writeKey = (key, path) => {
fs.writeFileSync(secretPath, key, { mode: 0x100, flag: "wx" });
};
module.exports.identityFilename = (index) => {
return "secret_" + leftpad(index, 2, "0") + ".butt";
};
module.exports.nextIdentityFilename = async (ssbServer) => {
const identities = await ssbServer.identities.list();
return "secret_" + leftpad(identities.length - 1, 2, "0") + ".butt";
return module.exports.identityFilename(identities.length - 1);
};
// From ssb-keys

114
app/package-lock.json generated
View File

@ -20,6 +20,33 @@
"sumchecker": "^3.0.1"
}
},
"@sendgrid/client": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-7.0.1.tgz",
"integrity": "sha512-HZhDD1bctv5rM0wqAz9LhJC1IL9YHn5jJvxPqiK/3f3WCQjRvraJ1AkqkFFNFd9lPBVLmcrORX04lojwl+5ZaA==",
"requires": {
"@sendgrid/helpers": "^7.0.1",
"axios": "^0.19.2"
}
},
"@sendgrid/helpers": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-7.0.1.tgz",
"integrity": "sha512-i/zsissq1upgdywtuJKysaplJJZC24GdtEKiJC1IRlXvBHzIjH4eU+rqUFO8h+hGji3UMURGgMFuLUXTUYvZ9w==",
"requires": {
"chalk": "^2.0.1",
"deepmerge": "^4.2.2"
}
},
"@sendgrid/mail": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-7.0.1.tgz",
"integrity": "sha512-yFkhjrYQvwpdy8eUiDxLLgPp9o5jHQzjJ5qUkgMr2fuPuYSKKqbpPir1PXIHx0ek2VKkTsvj/7Z5UQk6hPZcrQ==",
"requires": {
"@sendgrid/client": "^7.0.1",
"@sendgrid/helpers": "^7.0.1"
}
},
"@sentry/apm": {
"version": "5.15.4",
"resolved": "https://registry.npmjs.org/@sentry/apm/-/apm-5.15.4.tgz",
@ -177,6 +204,14 @@
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz",
"integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g=="
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"requires": {
"color-convert": "^1.9.0"
}
},
"anymatch": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
@ -221,6 +256,14 @@
"resolved": "https://registry.npmjs.org/author-regex/-/author-regex-1.0.0.tgz",
"integrity": "sha1-0IiFvmubv5Q5/gh8dihyRfCoFFA="
},
"axios": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
"requires": {
"follow-redirects": "1.5.10"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@ -451,6 +494,23 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"dependencies": {
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
}
}
},
"charwise": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/charwise/-/charwise-3.0.1.tgz",
@ -504,6 +564,19 @@
"mimic-response": "^1.0.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@ -754,6 +827,11 @@
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
},
"deepmerge": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
},
"defer-to-connect": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz",
@ -1312,6 +1390,29 @@
}
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
"for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -1516,6 +1617,11 @@
"function-bind": "^1.1.1"
}
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"has-network": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/has-network/-/has-network-0.0.1.tgz",
@ -10459,6 +10565,14 @@
"debug": "^4.1.0"
}
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"requires": {
"has-flag": "^3.0.0"
}
},
"tape": {
"version": "4.13.2",
"resolved": "https://registry.npmjs.org/tape/-/tape-4.13.2.tgz",

View File

@ -14,6 +14,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@sendgrid/mail": "^7.0.1",
"@sentry/node": "^5.15.4",
"chokidar": "^3.3.1",
"cookie-parser": "^1.4.5",

View File

@ -23,6 +23,7 @@ input[type="submit"] {
border: none;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
button.button-big,
a.button.button-big,
@ -44,6 +45,7 @@ input[type="submit"].button-secondary:hover {
}
input[type="text"],
input[type="email"],
textarea {
line-height: 32px;
padding: 0 6px;
@ -304,3 +306,11 @@ button.notification-box:hover {
.undo-request:hover:after {
content: " (undo)";
}
.key-block {
white-space: pre-wrap;
background: #f5f5f5;
border: 1px solid #ccc;
border-radius: 3px;
padding: 10px;
}

View File

@ -7,13 +7,13 @@
<link rel="stylesheet" href="/style.css">
</head>
<body <%- typeof body_class == "undefined" ? "" : `class="${body_class}"` %>>
<% if (context.profile) { %>
<% if (context.profile && typeof hideHeader == "undefined") { %>
<header>
<div class="logo">
<a href="/">Social</a>
</div>
<nav>
<a href="/">Home</a>
<a href="/">Profile</a>
<a href="/about">About me</a>
<a href="/pubs">Pubs</a>
<a href="/debug">Debug</a>

View File

@ -1,3 +1,4 @@
<%- include('_header') %>
<div style="max-width: 800px; margin: 0 auto">

View File

@ -0,0 +1,14 @@
<div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; line-height: 1.3em; font-size: 16px; padding: 30px 0">
<h1 style="font-weight: bold; line-height: 1.3em; margin: 0; padding: 10px 0 0 0; font-weight: 200;">Login button</h1>
<p>Welcome to Social, please use the button below to login to your account:</p>
<form action="<%= host %>/login" style="padding: 20px 0">
<input type="hidden" name="ssb_key" value="<%= ssb_key %>" />
<input type="submit" value="Login to Social" style="background: #08d; color: #fff; border-radius: 3px; padding: 8px 10px; border: none; cursor: pointer; text-decoration: none; display: inline-block; padding: 16px 20px; font-size: 18px;">
</form>
<p>
Never delete or forward this email, it is they key to accessing your account
</p>
<p>
From your friends at Social 😉
</p>
</div>

View File

@ -2,6 +2,6 @@
<h1 style="margin-top: 50px">Social had an error</h1>
<p style="margin: 30px 0">If you are a developer, this should help:</p>
<pre style="white-space: pre-wrap"><%- error.stack %></pre>
<pre style="white-space: pre-wrap"><%= error.stack %></pre>
<%- include('_footer') %>

41
app/views/keys.ejs Normal file
View File

@ -0,0 +1,41 @@
<%- include('_header', { hideHeader: true }) %>
<div style="max-width: 800px; margin: 0 auto">
<h1 style="padding-top: 50px">Save your keys</h1>
<p style="padding-top: 20px">
Congratulations! Your account was created successfully.
</p>
<% if (useEmail) { %>
<p>
Now we will send you an email that allows you to sign back in next time, your email addess will not be stored or used for anything else.
</p>
<p>
<b>Never delete or forward this email, it will be your only way back in.</b>
</p>
<form action="/keys/email" method="POST">
<div style="padding: 20px 0 30px 0">
<label>
Email: <br />
<input type="email" name="email">
</label>
</div>
<input class="button-big" type="submit" value="Send">
</form>
<p>or</p>
<p>
<a href="/keys/copy">No thanks, just let me download my key</a>
</p>
<% } else { %>
<p>
Now please download or copy your key, <b>it is your only way to sign back in.</b>
</p>
<pre class="key-block"><%= key %></pre>
<p style="margin: 30px 0 20px 0">
<a class="button button-big" href="/keys/download">Download Key</a>
</p>
<p>then <a href="/">continue to profile</a></p>
<% } %>
</div>
<%- include('_footer') %>

19
app/views/keys_copy.ejs Normal file
View File

@ -0,0 +1,19 @@
<%- include('_header', { hideHeader: true }) %>
<div style="max-width: 800px; margin: 0 auto">
<h1 style="padding-top: 50px">Save your keys</h1>
<p>
Your <a href="/keys/download">download</a> is starting, alternatively, you can copy your key as text:
</p>
<pre class="key-block"><%= key %></pre>
<a href="/">Continue to profile</a>
</div>
<script>
setTimeout(() => {
window.location = "/keys/download";
}, 500);
</script>
<%- include('_footer') %>

11
app/views/keys_sent.ejs Normal file
View File

@ -0,0 +1,11 @@
<%- include('_header', { hideHeader: true }) %>
<div style="max-width: 800px; margin: 0 auto">
<h1 style="padding-top: 50px">Email Sent</h1>
<p style="padding: 20px 0">Now open it to be sure you received, hit back if you didn't to try again.</p>
<a href="/">Continue to profile</a>
</div>
<%- include('_footer') %>