redirect on private struct
This commit is contained in:
parent
e179814859
commit
8bcaf264fa
3 changed files with 39 additions and 10 deletions
41
index.js
41
index.js
|
|
@ -77,6 +77,11 @@ app.use("/plumbing", require("./plumbing"));
|
||||||
// list, exactly like its user-facing routes do.
|
// list, exactly like its user-facing routes do.
|
||||||
app.param("structure_id", (req, res, next, id) => {
|
app.param("structure_id", (req, res, next, id) => {
|
||||||
if (!model.canAccessStructure(req.session.userId, id)) {
|
if (!model.canAccessStructure(req.session.userId, id)) {
|
||||||
|
// Logged-out visitors get a chance to sign in and come back; logged-in
|
||||||
|
// non-members stay 404 so we never confirm the structure exists to them.
|
||||||
|
if (!req.session.userId && req.method === "GET") {
|
||||||
|
return res.redirect("/login?next=" + encodeURIComponent(req.originalUrl));
|
||||||
|
}
|
||||||
return res.status(404).send("Not found");
|
return res.status(404).send("Not found");
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
|
|
@ -373,46 +378,61 @@ buildRoutes();
|
||||||
// | | _____| || |___ | | | |
|
// | | _____| || |___ | | | |
|
||||||
// |_______||_______||_______||___| |_|
|
// |_______||_______||_______||___| |_|
|
||||||
|
|
||||||
|
// Only same-site absolute paths survive as post-login redirect targets, so a
|
||||||
|
// crafted ?next= can't bounce someone to another origin (open redirect). A
|
||||||
|
// leading `//` is protocol-relative and would escape our origin, so reject it.
|
||||||
|
function safeNext(next) {
|
||||||
|
if (typeof next !== "string" || !next.startsWith("/") || next.startsWith("//")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
app.post("/register", async (req, res) => {
|
app.post("/register", async (req, res) => {
|
||||||
|
const next = safeNext(req.body.next);
|
||||||
try {
|
try {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
const hashedPassword = await bcrypt.hash(password, 10); // 10 is the saltRounds
|
const hashedPassword = await bcrypt.hash(password, 10); // 10 is the saltRounds
|
||||||
const userId = model.createUser(username, hashedPassword);
|
const userId = model.createUser(username, hashedPassword);
|
||||||
req.session.userId = userId;
|
req.session.userId = userId;
|
||||||
res.redirect("/workshop");
|
res.redirect(next || "/workshop");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return res.send(eta.render("auth/register", { error: e }));
|
return res.send(eta.render("auth/register", { error: e, next }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/login", async (req, res) => {
|
app.post("/login", async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
const next = safeNext(req.body.next);
|
||||||
const user = model.getUser(username);
|
const user = model.getUser(username);
|
||||||
|
|
||||||
if (user && (await bcrypt.compare(password, user.password))) {
|
if (user && (await bcrypt.compare(password, user.password))) {
|
||||||
req.session.userId = user.id;
|
req.session.userId = user.id;
|
||||||
return res.redirect("/");
|
return res.redirect(next || "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.send(
|
return res.send(
|
||||||
eta.render("auth/login", {
|
eta.render("auth/login", {
|
||||||
error: "are you sure you entered that right?",
|
error: "are you sure you entered that right?",
|
||||||
|
next,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/register", async (req, res) => {
|
app.get("/register", async (req, res) => {
|
||||||
|
const next = safeNext(req.query.next);
|
||||||
if (req.session.userId) {
|
if (req.session.userId) {
|
||||||
return res.redirect("/");
|
return res.redirect(next || "/");
|
||||||
}
|
}
|
||||||
return res.send(eta.render("auth/register", { error: null }));
|
return res.send(eta.render("auth/register", { error: null, next }));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/login", async (req, res) => {
|
app.get("/login", async (req, res) => {
|
||||||
|
const next = safeNext(req.query.next);
|
||||||
if (req.session.userId) {
|
if (req.session.userId) {
|
||||||
return res.redirect("/");
|
return res.redirect(next || "/");
|
||||||
}
|
}
|
||||||
return res.send(eta.render("auth/login", { error: null }));
|
return res.send(eta.render("auth/login", { error: null, next }));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.all("/logout", async (req, res) => {
|
app.all("/logout", async (req, res) => {
|
||||||
|
|
@ -1068,8 +1088,13 @@ app.all("*", async (req, res) => {
|
||||||
const route = routeMatch.route;
|
const route = routeMatch.route;
|
||||||
|
|
||||||
// Private structures are invisible to uninvited users: 404, so a private
|
// Private structures are invisible to uninvited users: 404, so a private
|
||||||
// route is indistinguishable from one that doesn't exist.
|
// route is indistinguishable from one that doesn't exist. Logged-out
|
||||||
|
// visitors on a GET get bounced to login and returned here afterward;
|
||||||
|
// logged-in non-members still 404.
|
||||||
if (!model.canAccessStructure(req.session.userId, route.structure_id)) {
|
if (!model.canAccessStructure(req.session.userId, route.structure_id)) {
|
||||||
|
if (!req.session.userId && verb === "GET") {
|
||||||
|
return res.redirect("/login?next=" + encodeURIComponent(req.originalUrl));
|
||||||
|
}
|
||||||
return res.status(404).json({ success: false, message: "Path not found" });
|
return res.status(404).json({ success: false, message: "Path not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
<form action="/login" method="POST">
|
<form action="/login" method="POST">
|
||||||
|
<% if (it.next) { %><input type="hidden" name="next" value="<%= it.next %>"><% } %>
|
||||||
<input name="username" placeholder="username">
|
<input name="username" placeholder="username">
|
||||||
<input name="password" placeholder="password">
|
<input name="password" placeholder="password">
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
come home
|
come home
|
||||||
</button>
|
</button>
|
||||||
<% if (it.error) { %>
|
<% if (it.error) { %>
|
||||||
<%= error %>
|
<%= it.error %>
|
||||||
<% } %>
|
<% } %>
|
||||||
<br>
|
<br>
|
||||||
<div> or you can <a href="/register">move in here</a>.
|
<div> or you can <a href="/register<%= it.next ? '?next=' + encodeURIComponent(it.next) : '' %>">move in here</a>.
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<form action="/register" method="POST">
|
<form action="/register" method="POST">
|
||||||
|
<% if (it.next) { %><input type="hidden" name="next" value="<%= it.next %>"><% } %>
|
||||||
<input name="username" placeholder="username">
|
<input name="username" placeholder="username">
|
||||||
<input name="password" placeholder="password">
|
<input name="password" placeholder="password">
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
|
|
@ -7,4 +8,6 @@
|
||||||
<% if (it.error) { %>
|
<% if (it.error) { %>
|
||||||
<%= it.error %>
|
<%= it.error %>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<br>
|
||||||
|
<div> or you can <a href="/login<%= it.next ? '?next=' + encodeURIComponent(it.next) : '' %>">come home</a>.
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue