redirect on private struct

This commit is contained in:
Your Name 2026-08-19 11:21:36 -04:00
parent e179814859
commit 8bcaf264fa
3 changed files with 39 additions and 10 deletions

View file

@ -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" });
} }

View file

@ -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>

View file

@ -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>