seeded events are the site's; every write lands in the action log
calendar_write.owns() now accepts both site namespaces: the 32 events the 2026-08-29 seed stamped site73-<sha1>@greenlanescouts73.org and the site's own @site73.greenlanescouts73.org. The rule exists to keep the site off @band.us and off anything a person adds in a calendar client, not off its own data. Seeded objects are written back at the seeder's object name so an edit replaces in place; proven on a throwaway against the real store (edit, still 32 objects, restored byte-for-byte). auth_events becomes the one action log. Every P2 write - nearby create, update, deactivate; unit meeting edit; setting change; announcement post and take-down - now records who, when, and a one-line before -> after for the fields that changed, alongside the login, key and calendar entries already there. GET /api/admin/history (history:read, admin and above, not scopable on a key) reads it newest first with a kind prefix filter and before= paging; rowid breaks second-resolution ties. tests/smoke_admin.py 102 -> 112.
This commit is contained in:
+30
-2
@@ -93,6 +93,7 @@ CAPS = {
|
||||
"settings:write",
|
||||
"apikeys:own",
|
||||
"api:docs",
|
||||
"history:read",
|
||||
"people:invite_leader",
|
||||
"people:invite_admin",
|
||||
"email:draft",
|
||||
@@ -967,8 +968,8 @@ KEY_MAX_DAYS = 365
|
||||
|
||||
# Scopes a key may never carry, whatever the owner holds. Minting keys from a
|
||||
# key is a loop; the rest are owner powers that belong to a person at a screen.
|
||||
KEY_UNSCOPABLE = {"account:self", "apikeys:own", "api:docs", "people:manage", "secrets:rotate",
|
||||
"people:invite_leader", "people:invite_admin"}
|
||||
KEY_UNSCOPABLE = {"account:self", "apikeys:own", "api:docs", "history:read", "people:manage",
|
||||
"secrets:rotate", "people:invite_leader", "people:invite_admin"}
|
||||
|
||||
|
||||
def scopable_caps(person):
|
||||
@@ -1093,3 +1094,30 @@ def api_key_person(bearer):
|
||||
return p
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# History. auth_events is the one action log: logins and invites since P0,
|
||||
# keys since P3, calendar since P4, and every P2 write since 2026-09-04.
|
||||
# One table, one screen. Not per-row history tables, and not the dropped
|
||||
# generic audit table with workflow columns.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def list_events(limit=200, kind_prefix=None, before=None):
|
||||
con = connect()
|
||||
try:
|
||||
sql = "SELECT e.*, p.email AS actor_email FROM auth_events e LEFT JOIN people p ON p.id = e.actor_id"
|
||||
where, vals = [], []
|
||||
if kind_prefix:
|
||||
where.append("e.kind LIKE ?"); vals.append(kind_prefix + "%")
|
||||
if before:
|
||||
where.append("e.at < ?"); vals.append(before)
|
||||
if where:
|
||||
sql += " WHERE " + " AND ".join(where)
|
||||
# rowid breaks ties: `at` is second-resolution and a screen can write
|
||||
# several rows in one second.
|
||||
sql += " ORDER BY e.at DESC, e.rowid DESC LIMIT ?"
|
||||
vals.append(max(1, min(int(limit), 500)))
|
||||
return [dict(r) for r in con.execute(sql, vals)]
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
Reference in New Issue
Block a user