fix(auth): restore /echo prefix after proxy strips it from next param

The reverse proxy strips /echo/ before Python, so next=/workspace.html.
Both the JS redirect and the server-side already-logged-in path now
prepend /echo to produce a valid public URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-29 14:11:22 +00:00
parent 38259f3cfd
commit 77df09974c
2 changed files with 17 additions and 6 deletions

View File

@@ -10,7 +10,7 @@ import os
import sys import sys
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path from pathlib import Path
from urllib.parse import quote as _urlquote from urllib.parse import quote as _urlquote, parse_qs, urlparse
# Make dashboard/ importable for the handler submodules (constants, # Make dashboard/ importable for the handler submodules (constants,
# habits_helpers, handlers.*). Tests rely on this as well. # habits_helpers, handlers.*). Tests rely on this as well.
@@ -243,11 +243,19 @@ class TaskBoardHandler(
return return
elif self.path in ('/echo/login', '/login') or \ elif self.path in ('/echo/login', '/login') or \
self.path.startswith(('/echo/login?', '/login?')): self.path.startswith(('/echo/login?', '/login?')):
# If already logged in, redirect to workspace; otherwise serve # If already logged in, redirect to next (or workspace); otherwise serve login.html.
# login.html (created in Lane B2).
if self._check_dashboard_cookie(): if self._check_dashboard_cookie():
qs = parse_qs(urlparse(self.path).query)
next_vals = qs.get('next', [])
nxt = next_vals[0] if next_vals else ''
# Proxy strips /echo/ before Python, so nxt is e.g. /workspace.html.
# Re-add the prefix so the browser lands on the right public URL.
if nxt and nxt.startswith('/') and '://' not in nxt:
dest = '/echo' + nxt
else:
dest = '/echo/workspace.html'
self.send_response(302) self.send_response(302)
self.send_header('Location', '/echo/workspace.html') self.send_header('Location', dest)
self.send_header('Content-Length', '0') self.send_header('Content-Length', '0')
self.end_headers() self.end_headers()
return return

View File

@@ -257,8 +257,11 @@
// relative /echo/ path to prevent open-redirect attacks. // relative /echo/ path to prevent open-redirect attacks.
var params = new URLSearchParams(window.location.search); var params = new URLSearchParams(window.location.search);
var next = params.get('next') || ''; var next = params.get('next') || '';
var dest = (next && /^\/echo\/[^/]/.test(next) && next.indexOf('/echo/login') !== 0) // The proxy strips /echo/ before Python, so `next` is
? next // e.g. "/workspace.html". Re-add the /echo prefix for
// the browser. Guard against open-redirect (no ://).
var dest = (next && /^\/[^/]/.test(next) && next.indexOf('://') === -1)
? '/echo' + next
: '/echo/workspace.html'; : '/echo/workspace.html';
window.location.assign(dest); window.location.assign(dest);
return; return;