Add keys emailing/copying/downloading flow for users to be able to sign back in later
This commit is contained in:
parent
58a103adbb
commit
9e2b5fa9f1
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
<%- include('_header') %>
|
||||
|
||||
<div style="max-width: 800px; margin: 0 auto">
|
||||
|
|
|
@ -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>
|
|
@ -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') %>
|
|
@ -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') %>
|
|
@ -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') %>
|
|
@ -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') %>
|
Loading…
Reference in New Issue