footer sign-in link; sign-in, invite and reset land on the console
One quiet 'Sign in · leaders & families' in the public footer, after the site's own links and before Join, which stays the only call to action. Nothing on the public site pointed at /login before; the only ways in were typing the URL or an invite link. A fresh sign-in, an accepted invite and a used reset link now land on /leaders/, which sends a member on to Family and a leader to Summary. tests/smoke_identity.py 123 -> 125.
This commit is contained in:
+1
-1
@@ -493,7 +493,7 @@ def page(title, body, active=""):
|
|||||||
<div style="font-size:14px;line-height:1.7">Chartered by St. Luke's Lutheran Church · Zieglerville, PA<br>Cradle of Liberty Council · A family program. Every kid welcome.</div></div>
|
<div style="font-size:14px;line-height:1.7">Chartered by St. Luke's Lutheran Church · Zieglerville, PA<br>Cradle of Liberty Council · A family program. Every kid welcome.</div></div>
|
||||||
<div><div class="fh">VISIT</div><div style="font-size:14px;line-height:1.9">{MEETING_DAYS} · Pack {PACK_CLOCK} · Troop {TROOP_CLOCK}<br>St. Luke's Lutheran Church<br>Zieglerville, PA</div></div>
|
<div><div class="fh">VISIT</div><div style="font-size:14px;line-height:1.9">{MEETING_DAYS} · Pack {PACK_CLOCK} · Troop {TROOP_CLOCK}<br>St. Luke's Lutheran Church<br>Zieglerville, PA</div></div>
|
||||||
<div style="display:flex;flex-direction:column;gap:8px"><div class="fh" style="margin-bottom:4px">EXPLORE</div>
|
<div style="display:flex;flex-direction:column;gap:8px"><div class="fh" style="margin-bottom:4px">EXPLORE</div>
|
||||||
<a href="/cubs">Cub Scouts · Pack 73</a><a href="/troop">Troop 73 · Ages 11-17</a><a href="/calendar">{PROGRAM_YEAR} Calendar</a><a href="/documents">Forms & documents</a><a href="/find-a-unit">Find a unit near you</a><a class="gold" href="/join">Join us</a></div>
|
<a href="/cubs">Cub Scouts · Pack 73</a><a href="/troop">Troop 73 · Ages 11-17</a><a href="/calendar">{PROGRAM_YEAR} Calendar</a><a href="/documents">Forms & documents</a><a href="/find-a-unit">Find a unit near you</a><a href="/login" style="color:#C3CBDA">Sign in · leaders & families</a><a class="gold" href="/join">Join us</a></div>
|
||||||
<div style="display:flex;flex-direction:column;gap:8px"><div class="fh" style="margin-bottom:4px">FOLLOW</div>
|
<div style="display:flex;flex-direction:column;gap:8px"><div class="fh" style="margin-bottom:4px">FOLLOW</div>
|
||||||
<a class="fb" href="https://www.facebook.com/Pack73GreenLane" target="_blank" rel="noopener" aria-label="Pack 73 on Facebook"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" style="flex:none"><path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"/></svg>Pack 73</a><a class="fb" href="https://www.facebook.com/Troop73GreenLane" target="_blank" rel="noopener" aria-label="Troop 73 on Facebook"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" style="flex:none"><path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"/></svg>Troop 73</a></div>
|
<a class="fb" href="https://www.facebook.com/Pack73GreenLane" target="_blank" rel="noopener" aria-label="Pack 73 on Facebook"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" style="flex:none"><path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"/></svg>Pack 73</a><a class="fb" href="https://www.facebook.com/Troop73GreenLane" target="_blank" rel="noopener" aria-label="Troop 73 on Facebook"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" style="flex:none"><path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"/></svg>Troop 73</a></div>
|
||||||
</div></footer>
|
</div></footer>
|
||||||
|
|||||||
+2
-2
@@ -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),
|
tok = identity.start_session(person["id"], ip=_client_ip(request),
|
||||||
user_agent=request.headers.get("user-agent"))
|
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,
|
resp.set_cookie(COOKIE, tok, max_age=identity.SESSION_ABSOLUTE_DAYS * 86400,
|
||||||
httponly=True, secure=COOKIE_SECURE, samesite="lax", path="/")
|
httponly=True, secure=COOKIE_SECURE, samesite="lax", path="/")
|
||||||
return resp
|
return resp
|
||||||
@@ -395,7 +395,7 @@ def reset_accept(request: Request, token: str, password: str = Form(""), confirm
|
|||||||
status_code=e.status)
|
status_code=e.status)
|
||||||
tok = identity.start_session(person["id"], ip=_client_ip(request),
|
tok = identity.start_session(person["id"], ip=_client_ip(request),
|
||||||
user_agent=request.headers.get("user-agent"))
|
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,
|
resp.set_cookie(COOKIE, tok, max_age=identity.SESSION_ABSOLUTE_DAYS * 86400,
|
||||||
httponly=True, secure=COOKIE_SECURE, samesite="lax", path="/")
|
httponly=True, secure=COOKIE_SECURE, samesite="lax", path="/")
|
||||||
return resp
|
return resp
|
||||||
|
|||||||
+1
-1
@@ -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
|
# 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.
|
# 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
|
"""Where to send someone after login. Only a same-origin relative path
|
||||||
survives: a single leading slash, no scheme, no protocol-relative `//`,
|
survives: a single leading slash, no scheme, no protocol-relative `//`,
|
||||||
no backslash or control character that a browser might normalise into
|
no backslash or control character that a browser might normalise into
|
||||||
|
|||||||
+15
-7
@@ -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",
|
check("people actions in the log", {"invite.revoked", "person.roles", "person.disabled", "person.enabled",
|
||||||
"password.reset_issued", "password.reset", "password.changed"} <= kinds)
|
"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("<footer"):_src.find("</footer>")]
|
||||||
|
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")
|
print("\nmeeting words")
|
||||||
D = dict(MEETING_DAY="Tuesday", MEETING_DAYS="Tuesdays", MEETING_DAY_ABBR="Tue", PACK_TIME="6:00 PM",
|
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")
|
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")
|
print("\nsafe_next")
|
||||||
check("relative path passes", I.safe_next("/leaders/") == "/leaders/")
|
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("query string kept", I.safe_next("/leaders/nearby?x=1") == "/leaders/nearby?x=1")
|
||||||
check("empty falls back", I.safe_next("") == "/account")
|
check("empty falls back", I.safe_next("") == "/leaders/")
|
||||||
check("None falls back", I.safe_next(None) == "/account")
|
check("None falls back", I.safe_next(None) == "/leaders/")
|
||||||
check("absolute URL rejected", I.safe_next("https://evil.example/") == "/account")
|
check("absolute URL rejected", I.safe_next("https://evil.example/") == "/leaders/")
|
||||||
check("protocol-relative rejected", I.safe_next("//evil.example/") == "/account")
|
check("protocol-relative rejected", I.safe_next("//evil.example/") == "/leaders/")
|
||||||
check("backslash form rejected", I.safe_next("/\\evil.example") == "/account")
|
check("backslash form rejected", I.safe_next("/\\evil.example") == "/leaders/")
|
||||||
check("control char rejected", I.safe_next("/leaders\r\nX: y") == "/account")
|
check("control char rejected", I.safe_next("/leaders\r\nX: y") == "/leaders/")
|
||||||
check("no leading slash rejected", I.safe_next("leaders/") == "/account")
|
check("no leading slash rejected", I.safe_next("leaders/") == "/leaders/")
|
||||||
check("custom default honoured", I.safe_next("nope", default="/") == "/")
|
check("custom default honoured", I.safe_next("nope", default="/") == "/")
|
||||||
|
|
||||||
print("\n%d passed, %d failed" % (PASS, FAIL))
|
print("\n%d passed, %d failed" % (PASS, FAIL))
|
||||||
|
|||||||
Reference in New Issue
Block a user