diff --git a/app/app.py b/app/app.py index f3bc79e..9c6922b 100644 --- a/app/app.py +++ b/app/app.py @@ -493,7 +493,7 @@ def page(title, body, active=""):
Chartered by St. Luke's Lutheran Church · Zieglerville, PA
Cradle of Liberty Council · A family program. Every kid welcome.
VISIT
{MEETING_DAYS} · Pack {PACK_CLOCK} · Troop {TROOP_CLOCK}
St. Luke's Lutheran Church
Zieglerville, PA
EXPLORE
-Cub Scouts · Pack 73Troop 73 · Ages 11-17{PROGRAM_YEAR} CalendarForms & documentsFind a unit near youJoin us
+Cub Scouts · Pack 73Troop 73 · Ages 11-17{PROGRAM_YEAR} CalendarForms & documentsFind a unit near youSign in · leaders & familiesJoin us
FOLLOW
Pack 73Troop 73
diff --git a/app/auth.py b/app/auth.py index 51073ac..547829a 100644 --- a/app/auth.py +++ b/app/auth.py @@ -220,7 +220,7 @@ def invite_accept(request: Request, token: str, full_name: str = Form(""), tok = identity.start_session(person["id"], ip=_client_ip(request), user_agent=request.headers.get("user-agent")) - resp = RedirectResponse(url="/account", status_code=303) + resp = RedirectResponse(url="/leaders/", status_code=303) resp.set_cookie(COOKIE, tok, max_age=identity.SESSION_ABSOLUTE_DAYS * 86400, httponly=True, secure=COOKIE_SECURE, samesite="lax", path="/") return resp @@ -395,7 +395,7 @@ def reset_accept(request: Request, token: str, password: str = Form(""), confirm status_code=e.status) tok = identity.start_session(person["id"], ip=_client_ip(request), user_agent=request.headers.get("user-agent")) - resp = RedirectResponse(url="/account", status_code=303) + resp = RedirectResponse(url="/leaders/", status_code=303) resp.set_cookie(COOKIE, tok, max_age=identity.SESSION_ABSOLUTE_DAYS * 86400, httponly=True, secure=COOKIE_SECURE, samesite="lax", path="/") return resp diff --git a/app/identity.py b/app/identity.py index 14eb749..4e88a69 100644 --- a/app/identity.py +++ b/app/identity.py @@ -692,7 +692,7 @@ def consume_invite(token, full_name, password, preferred_name=None, phone=None, # Rows, not signed cookies. A leader stepping down has to be revocable now # rather than at token expiry, and "sign out everywhere" has to be possible. -def safe_next(value, default="/account"): +def safe_next(value, default="/leaders/"): """Where to send someone after login. Only a same-origin relative path survives: a single leading slash, no scheme, no protocol-relative `//`, no backslash or control character that a browser might normalise into diff --git a/tests/smoke_identity.py b/tests/smoke_identity.py index f8505ff..bf9666d 100644 --- a/tests/smoke_identity.py +++ b/tests/smoke_identity.py @@ -266,6 +266,14 @@ kinds = {r["kind"] for r in I.list_events(limit=500)} check("people actions in the log", {"invite.revoked", "person.roles", "person.disabled", "person.enabled", "password.reset_issued", "password.reset", "password.changed"} <= kinds) +print("\nfooter sign-in") +import os as _os +_src = open(_os.path.join(_os.path.dirname(__file__), "..", "app", "app.py")).read() +_foot = _src[_src.find("")] +check("public footer carries one quiet sign-in link before Join", _foot.count('href="/login"') == 1 and _foot.index('href="/login"') < _foot.index('href="/join"')) +check("sign-in lands on the console by default; unsafe next still refused", + I.safe_next("") == "/leaders/" and I.safe_next("//evil") == "/leaders/" and I.safe_next("/leaders/roster") == "/leaders/roster") + print("\nmeeting words") D = dict(MEETING_DAY="Tuesday", MEETING_DAYS="Tuesdays", MEETING_DAY_ABBR="Tue", PACK_TIME="6:00 PM", TROOP_TIME="7:30 PM", PACK_CLOCK="6:00", TROOP_CLOCK="7:30") @@ -288,13 +296,13 @@ check("live seed rows reproduce the constants", I.meeting_words(I.list_units(), print("\nsafe_next") check("relative path passes", I.safe_next("/leaders/") == "/leaders/") check("query string kept", I.safe_next("/leaders/nearby?x=1") == "/leaders/nearby?x=1") -check("empty falls back", I.safe_next("") == "/account") -check("None falls back", I.safe_next(None) == "/account") -check("absolute URL rejected", I.safe_next("https://evil.example/") == "/account") -check("protocol-relative rejected", I.safe_next("//evil.example/") == "/account") -check("backslash form rejected", I.safe_next("/\\evil.example") == "/account") -check("control char rejected", I.safe_next("/leaders\r\nX: y") == "/account") -check("no leading slash rejected", I.safe_next("leaders/") == "/account") +check("empty falls back", I.safe_next("") == "/leaders/") +check("None falls back", I.safe_next(None) == "/leaders/") +check("absolute URL rejected", I.safe_next("https://evil.example/") == "/leaders/") +check("protocol-relative rejected", I.safe_next("//evil.example/") == "/leaders/") +check("backslash form rejected", I.safe_next("/\\evil.example") == "/leaders/") +check("control char rejected", I.safe_next("/leaders\r\nX: y") == "/leaders/") +check("no leading slash rejected", I.safe_next("leaders/") == "/leaders/") check("custom default honoured", I.safe_next("nope", default="/") == "/") print("\n%d passed, %d failed" % (PASS, FAIL))