← back to Animals
Honor ?next= after login/signup so claimers return to the claim flow
2f8b7b59d524ad1f3c0bb3cfe455f787d055ea5c · 2026-05-26 10:15:39 -0700 · SteveStudio2
requireUserHtml already redirects logged-out users to /login?next=/clinic/:id/claim,
but the auth forms hardcoded location.href='/circles' and ignored it. Now both
forms read ?next= (with an open-redirect guard: only single-leading-slash local
paths, rejecting //host and backslash), carry it across the login<->signup
cross-links, and redirect there on success.
Files touched
M src/server/community.jsM src/server/community_render.js
Diff
commit 2f8b7b59d524ad1f3c0bb3cfe455f787d055ea5c
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Tue May 26 10:15:39 2026 -0700
Honor ?next= after login/signup so claimers return to the claim flow
requireUserHtml already redirects logged-out users to /login?next=/clinic/:id/claim,
but the auth forms hardcoded location.href='/circles' and ignored it. Now both
forms read ?next= (with an open-redirect guard: only single-leading-slash local
paths, rejecting //host and backslash), carry it across the login<->signup
cross-links, and redirect there on success.
---
src/server/community.js | 4 ++--
src/server/community_render.js | 22 ++++++++++++++++------
2 files changed, 18 insertions(+), 8 deletions(-)
diff --git a/src/server/community.js b/src/server/community.js
index 31dc49b..62cad77 100644
--- a/src/server/community.js
+++ b/src/server/community.js
@@ -67,8 +67,8 @@ function sanitizePhotoUrls(urls) {
const MARKETPLACE_BODY_MAX = 10_000;
// ── auth pages ────────────────────────────────────────────────────────────
-community.get('/signup', (req, res) => res.send(renderSignup({ user: req.user })));
-community.get('/login', (req, res) => res.send(renderLogin({ user: req.user })));
+community.get('/signup', (req, res) => res.send(renderSignup({ user: req.user, next: req.query.next })));
+community.get('/login', (req, res) => res.send(renderLogin({ user: req.user, next: req.query.next })));
community.post('/auth/signup', signup);
community.post('/auth/login', login);
community.post('/auth/logout', logout);
diff --git a/src/server/community_render.js b/src/server/community_render.js
index b577066..cee5be6 100644
--- a/src/server/community_render.js
+++ b/src/server/community_render.js
@@ -44,13 +44,23 @@ function footer() {
}
// ── auth pages ────────────────────────────────────────────────────────────
-export function renderSignup({ user }) {
+// Only ever return a local path — blocks //evil.com, \evil, and absolute URLs
+// so a poisoned ?next= can't turn the auth pages into an open redirect.
+function safeNext(next) {
+ return (typeof next === 'string' && /^\/[^/\\]/.test(next)) ? next : '';
+}
+function withNext(path, next) {
+ const n = safeNext(next);
+ return n ? `${path}?next=${encodeURIComponent(n)}` : path;
+}
+
+export function renderSignup({ user, next }) {
if (user) return head('Already signed in') + nav(user) + `<main class="container"><h1>You're signed in</h1><p><a href="/marketplace">Browse marketplace</a> or <a href="/circles">your circles</a>.</p></main>` + footer() + '</body></html>';
return head('Join PawCircles') + nav(null) + `
<main class="container narrow">
<h1>Join PawCircles</h1>
<p class="muted">Free. Your ZIP unlocks your local pet community — vets, marketplace, lost-pet board, recommendations.</p>
- <form class="auth-form" onsubmit="event.preventDefault();const fd=new FormData(this);const obj=Object.fromEntries(fd);obj.species_pets=fd.getAll('species_pets');fetch('/auth/signup',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(obj)}).then(r=>r.json()).then(j=>{if(j.ok){location.href='/circles'}else{this.querySelector('.err').textContent=j.error||'signup failed'}});">
+ <form class="auth-form" onsubmit="event.preventDefault();const fd=new FormData(this);const obj=Object.fromEntries(fd);obj.species_pets=fd.getAll('species_pets');const n=new URLSearchParams(location.search).get('next');const dest=(n&&/^\/[^/\\]/.test(n))?n:'/circles';fetch('/auth/signup',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(obj)}).then(r=>r.json()).then(j=>{if(j.ok){location.href=dest}else{this.querySelector('.err').textContent=j.error||'signup failed'}});">
<label>Email <input name="email" type="email" required></label>
<label>Password <input name="password" type="password" minlength="8" required></label>
<label>Display name <input name="full_name"></label>
@@ -68,22 +78,22 @@ export function renderSignup({ user }) {
<button type="submit">Create account</button>
<p class="err" style="color:#c0382b"></p>
</form>
- <p class="muted small">Already have an account? <a href="/login">Log in</a>.</p>
+ <p class="muted small">Already have an account? <a href="${withNext('/login', next)}">Log in</a>.</p>
</main>` + footer() + '</body></html>';
}
-export function renderLogin({ user }) {
+export function renderLogin({ user, next }) {
if (user) return head('Already signed in') + nav(user) + `<main class="container"><h1>You're signed in.</h1></main>` + footer() + '</body></html>';
return head('Log in') + nav(null) + `
<main class="container narrow">
<h1>Log in</h1>
- <form class="auth-form" onsubmit="event.preventDefault();const fd=new FormData(this);fetch('/auth/login',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(Object.fromEntries(fd))}).then(r=>r.json()).then(j=>{if(j.ok){location.href='/circles'}else{this.querySelector('.err').textContent=j.error||'login failed'}});">
+ <form class="auth-form" onsubmit="event.preventDefault();const fd=new FormData(this);const n=new URLSearchParams(location.search).get('next');const dest=(n&&/^\/[^/\\]/.test(n))?n:'/circles';fetch('/auth/login',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(Object.fromEntries(fd))}).then(r=>r.json()).then(j=>{if(j.ok){location.href=dest}else{this.querySelector('.err').textContent=j.error||'login failed'}});">
<label>Email <input name="email" type="email" required></label>
<label>Password <input name="password" type="password" required></label>
<button type="submit">Log in</button>
<p class="err" style="color:#c0382b"></p>
</form>
- <p class="muted small">No account? <a href="/signup">Sign up</a>.</p>
+ <p class="muted small">No account? <a href="${withNext('/signup', next)}">Sign up</a>.</p>
</main>` + footer() + '</body></html>';
}
← cda6f25 snapshot — gitify backup 2026-05-19
·
back to Animals
·
Add lost-pet alert opt-in surface (/me prefs page + signup c ecd4ce9 →