Commit Graph
5 Commits
Author SHA1 Message Date
thethreemagi 0b337f1f19 meeting words come from the units table; the constants are now defaults
MEETING_DAY, MEETING_DAYS, MEETING_DAY_ABBR, PACK_TIME, TROOP_TIME,
PACK_CLOCK and TROOP_CLOCK were the last thing a leader could not change
without a commit. identity.meeting_words() derives all seven from the units
rows; app.py refreshes the module globals from it on a 30-second TTL in a
middleware, and the page templates - f-strings that read those globals when a
route runs - are untouched. A row that cannot supply a word keeps the
default, so a half-filled unit degrades to today's copy rather than a blank.

The day words come from the pack row: the site's copy assumes both units
meet the same night, and if that changes the copy needs rewriting, not a
bigger constant. Proven on a throwaway container with a DB copy: patching
pack73 to Thursday 18:15 changed the homepage, cubs, troop and meta
description; patching back restored them. tests/smoke_identity.py 59 -> 66.
2026-09-04 16:48:06 -04:00
thethreemagi 5c7a8dd785 login: honour next on both exits, same-origin only
The console's 401 redirect carries next=/leaders/ and /login dropped it, so a
leader arriving cold landed on /account. next now rides the form as a hidden
field and is applied after a successful POST and when an already-signed-in
person hits /login. identity.safe_next() admits only a relative path with a
single leading slash - no scheme, no //, no backslash, no control characters -
so the login page cannot become an open redirect. Ten checks added to
tests/smoke_identity.py; the suite is 59 + 24 + 53, all green.
2026-09-04 16:28:58 -04:00
thethreemagi 32e1c67a6f P2: nearby units CRUD, unit meeting times, site settings - the first admin writes
Nearby rows bump verified_at on every save and deactivate rather than delete.
Unit edits are meeting fields only, gated by unit:write_own scoped to the unit
in the URL. Settings are a typed key registry falling back to code defaults;
find-a-unit now reads its source name and URL from it. link_url and contact
reject attribute-breakout characters and the nearby renderer escapes quotes.
Covered by tests/smoke_admin.py, 53 checks in-process.
2026-09-04 14:10:29 -04:00
thethreemagi 8c8dab12e3 P1: gate members documents per unit, session auth on the admin API
documents.visible() now takes the viewer's unit set. A members document is
served and listed only to a signed-in member of the matching unit; 'both'
reaches any member; owner and admin reach everything including unit types
added later. Everyone else gets 404, never 403.

The gate moved INSIDE find(), so there is one path from a slug to a file and
no route can forget to check. listed() and visible() stay separate functions.

admin_api takes a session first and falls back to X-Admin-Token as break
glass. Still fails closed: no session and no ADMIN_TOKEN is 503. Capability,
not role, decides per route. announcements.created_by now comes from the
session and ignores any value in the request body.

tests/smoke_documents.py, 24 checks, including the invariant that the index
can never list something serving would refuse.
2026-09-04 12:09:19 -04:00
thethreemagi bed93072cb P0: identity layer - units, people, roles, invites, sessions
Adds identity.py (schema, capability map, scrypt passwords, invites,
sessions, login throttle, boot seed) and auth.py (login, invite
acceptance, account page). app.py gains two imports and one wiring
block at EOF; no existing behaviour changes.

Units are a table seeded from the site constants. Roles split: leader
and member per unit in memberships, owner and admin site-wide in
people.global_role, so a unit added later cannot under-grant an admin.

tests/smoke_identity.py covers the rules that are invisible when wrong:
single-use invites, reissue revoking the prior link, expiry, idle and
absolute session bounds, throttling, and the capability split. 49 checks.
2026-09-04 11:29:32 -04:00