Files
thethreemagi 752fb08d73 API.md: every admin route tested over HTTP and approved
123 checks across all 48 routes plus /api/docs, driven on a throwaway
copy of the live database with anonymous, member, leader, admin and
owner access; 0 failures. Calendar checks used 2036 probes against the
real store and left it at 32 objects. API.md records, per route, the
capability and what each request actually returned. tests/api_drive.py
is the harness and tests/api_doc.py regenerates the document from its
results, so the next approval run is a rerun, not a rewrite.
2026-09-04 20:27:19 -04:00

529 lines
12 KiB
Markdown

# Green Lane Scouts 73: Admin API
_Tested and approved 2026-09-05. Every route below was driven over HTTP on a throwaway copy of the
live database with four levels of access (anonymous, member, unit leader, admin, plus the site owner
where a route is owner-only): 123 checks, 0 failures. The calendar checks ran against the real store
with 2036 probes and left it as found. The live registry is `GET /api/docs`; this file records what
each route actually did when asked. Regenerate with `tests/api_drive.py` then `tests/api_doc.py`._
**Base:** `https://greenlanescouts73.org`. **Auth:** a browser session (cookie `s73_session`) or a
bearer API key (`Authorization: Bearer gls73_...`), honoured on `/api/admin` only. `401` means no
credential; `403` means a credential without the capability. Error bodies are `{"detail": ...}` or,
from a store guardrail, `{"detail": {"error": ...}}`. Every write lands in the action log with who
did it and what changed. Times are ISO 8601 with an offset.
## Identity
Who is calling, and the live route registry.
### `GET /api/admin/whoami`
Any credential; answers who you are, how you authenticated, and your capabilities.
| Tried | HTTP |
|---|---|
| anon | 401 |
| claude session | 200 |
| member session | 200 |
| admin token from LAN | 200 |
| admin token from WAN | 403 |
| disabled person's session is dead | 401 |
| fresh member session after enable | 200 |
### `GET /api/docs`
Capability `api:docs`, leader and above. Generated from the router on every request.
| Tried | HTTP |
|---|---|
| anon | 401 |
| member (no api:docs) | 403 |
| leader | 200 |
## Summary and leads
Join-form leads are read-only. A claim is who has a lead and when; it is the only thing that changes.
### `GET /api/admin/summary`
Capability `leads:read`.
| Tried | HTTP |
|---|---|
| anon | 401 |
| member | 403 |
| leader | 200 |
### `GET /api/admin/leads`
Capability `leads:read`.
| Tried | HTTP |
|---|---|
| leader | 200 |
| search miss | 200 |
| bearer key works | 200 |
| bearer key from WAN while lan | 403 |
| revoked key dead | 401 |
### `GET /api/admin/leads/{id}`
Capability `leads:read`.
| Tried | HTTP |
|---|---|
| one lead | 200 |
| unknown lead | 404 |
### `POST /api/admin/leads/{id}/claim`
Capability `leads:read`. Take a lead so it is not sitting unclaimed. Outreach v1 is exactly
| Tried | HTTP |
|---|---|
| claim | 200 |
| claim again (self) | 409 |
| claim by another | 409 |
### `POST /api/admin/leads/{id}/release`
Capability `leads:read`. Let a lead go. The holder may; so may an owner (people:manage),
| Tried | HTTP |
|---|---|
| release by another admin (no people:manage) | 403 |
| release by owner | 200 |
| release when nobody has it | 409 |
### `GET /api/admin/mirrors/failed`
Capability `leads:read`.
| Tried | HTTP |
|---|---|
| failed mirrors | 200 |
### `POST /api/admin/mirrors/retry`
Capability `leads:read`. Replay leads whose copy to an external target failed. Idempotent-ish:
| Tried | HTTP |
|---|---|
| retry sheet (nothing owed) | 200 |
| retry ntfy (not implemented) | 422 |
## Announcements
Site banners. `ends_at` is required. Revoke keeps the row.
### `GET /api/admin/announcements`
Capability `announcements:write`. Every announcement with its computed state: live, scheduled, expired,
| Tried | HTTP |
|---|---|
| list | 200 |
### `POST /api/admin/announcements`
Capability `announcements:write`. Post a notice. ends_at is required.
| Tried | HTTP |
|---|---|
| no ends_at | 422 |
| create (2036) | 201 |
| member cannot | 403 |
### `DELETE /api/admin/announcements/{id}`
Capability `announcements:write`. Take one down early. Sets revoked_at; never deletes the row.
| Tried | HTTP |
|---|---|
| revoke | 200 |
| revoke again | 409 |
| revoke unknown | 404 |
## Nearby units
The find-a-unit directory. Deactivate, never delete. Saving bumps `verified_at`.
### `GET /api/admin/nearby`
Capability `nearby:write`. The editing view, so deactivated rows are reachable. nearby:write
| Tried | HTTP |
|---|---|
| list | 200 |
| bearer key out of scope | 403 |
### `POST /api/admin/nearby`
Capability `nearby:write`.
| Tried | HTTP |
|---|---|
| bad unit_type | 422 |
| create | 201 |
### `PATCH /api/admin/nearby/{id}`
Capability `nearby:write`. Partial update. Saving bumps verified_at to today unless the payload
| Tried | HTTP |
|---|---|
| update | 200 |
| unknown field rejected | 422 |
| unknown | 404 |
### `DELETE /api/admin/nearby/{id}`
Capability `nearby:write`. Take a unit off the page. Sets active=0; never deletes the row.
| Tried | HTTP |
|---|---|
| deactivate | 200 |
| deactivate again | 409 |
## Our units
Meeting day, time and place. A unit leader edits their own unit only.
### `GET /api/admin/units`
Capability `unit:write_own`. The units the caller may edit, which is what the screen this feeds
| Tried | HTTP |
|---|---|
| pack leader sees only the pack | 200 |
| admin sees both | 200 |
### `PATCH /api/admin/units/{slug}`
Capability `unit:write_own`.
| Tried | HTTP |
|---|---|
| pack leader cannot edit the troop | 403 |
| bad time | 422 |
| no-change save | 200 |
| unknown | 404 |
## Settings
Typed keys only; `value: null` clears to the code default.
### `GET /api/admin/settings`
Capability `settings:write`.
| Tried | HTTP |
|---|---|
| leader cannot | 403 |
| admin | 200 |
### `PUT /api/admin/settings/{key}`
Capability `settings:write`. Set one value, or clear it back to the code default with value: null.
| Tried | HTTP |
|---|---|
| bad value | 422 |
| set | 200 |
| clear to default | 200 |
| unknown key | 422 |
## API keys
Bearer keys, honoured on `/api/admin` only. Scopes are a subset of the owner's capabilities; a key can never mint keys. LAN-only unless `api_keys_from` is `anywhere`.
### `GET /api/admin/keys`
Capability `apikeys:own`. Your keys, newest first, with state (active / expired / revoked) and
| Tried | HTTP |
|---|---|
| admin token cannot own keys | 403 |
| own list | 200 |
### `POST /api/admin/keys`
Capability `apikeys:own`. Mint a key. Body: label, scopes (list), expires_days (optional, 1-365).
| Tried | HTTP |
|---|---|
| unscopable scope | 422 |
| mint | 201 |
| a key cannot mint keys | 403 |
### `DELETE /api/admin/keys/{id}`
Capability `apikeys:own`. Revoke one of your keys. The row stays. A key that is not yours is
| Tried | HTTP |
|---|---|
| revoke | 200 |
| revoke again | 409 |
| foreign/unknown key | 404 |
## History
The action log, newest first. Admin and above.
### `GET /api/admin/history`
Capability `history:read`. Newest first. `kind` is a prefix filter (calendar., nearby., login.).
| Tried | HTTP |
|---|---|
| leader cannot | 403 |
| admin, filtered | 200 |
## People
Invites and reset links are returned once and handed over by the admin; there is no outbound mail. Grants are limited to what the caller holds. Owner-only: roles, disable, enable.
### `GET /api/admin/people`
Capability `people:invite_leader`. Everyone with an account, their roles and memberships, active
| Tried | HTTP |
|---|---|
| leader cannot | 403 |
| admin | 200 |
### `POST /api/admin/people/invite`
Capability `people:invite_leader`. Mint an invite. Body: email, global_role (optional), units
| Tried | HTTP |
|---|---|
| bad email | 422 |
| admin cannot grant owner | 403 |
| invite | 201 |
### `DELETE /api/admin/people/invite/{id}`
Capability `people:invite_leader`.
| Tried | HTTP |
|---|---|
| revoke invite | 200 |
| revoke again | 404 |
### `PUT /api/admin/people/{id}/roles`
Capability `people:manage`. Replace a person's global role and unit memberships. Body:
| Tried | HTTP |
|---|---|
| admin cannot change roles (owner only) | 403 |
| owner sets roles | 200 |
### `POST /api/admin/people/{id}/disable`
Capability `people:manage`. Disable a person now: sessions end, keys stop, documents close. The
| Tried | HTTP |
|---|---|
| admin cannot disable | 403 |
| owner disables | 200 |
### `POST /api/admin/people/{id}/enable`
Capability `people:manage`.
| Tried | HTTP |
|---|---|
| owner enables | 200 |
### `POST /api/admin/people/{id}/reset`
Capability `people:invite_admin`. Mint a one-time password-reset link (24 h), returned ONCE. Their
| Tried | HTTP |
|---|---|
| admin mints a reset link | 201 |
| owner may reset an admin | 201 |
| unknown person | 404 |
## Roster and family
Families, scouts, and a per-year checklist recording that a thing was collected, never the thing. Nothing medical is stored. `/family` is the signed-in parent's own households.
### `GET /api/admin/roster`
Capability `roster:write`. Households with their scouts and this year's checklist. `unit` is a
| Tried | HTTP |
|---|---|
| member cannot | 403 |
| roster by unit | 200 |
### `POST /api/admin/roster/households`
Capability `roster:write`. A family. Body: parent_name (required), email, phone, second_parent,
| Tried | HTTP |
|---|---|
| no parent | 422 |
| create family | 201 |
| import lead | 201 |
| import twice | 409 |
### `GET /api/admin/roster/households/{id}`
Capability `roster:write`.
| Tried | HTTP |
|---|---|
| one family | 200 |
### `PATCH /api/admin/roster/households/{id}`
Capability `roster:write`.
| Tried | HTTP |
|---|---|
| deactivate family | 200 |
### `POST /api/admin/roster/households/{id}/scouts`
Capability `roster:write`. Body: first_name (required), last_name, unit_id (required), den, bsa_member_id.
| Tried | HTTP |
|---|---|
| add scout | 201 |
| bad bsa id | 422 |
### `PUT /api/admin/roster/households/{id}/people`
Capability `people:invite_leader`. Which accounts belong to this family: body {person_ids: [...]}. This is
| Tried | HTTP |
|---|---|
| leader cannot link accounts | 403 |
| unknown person id | 422 |
| admin links the member | 200 |
### `PATCH /api/admin/roster/scouts/{id}`
Capability `roster:write`.
| Tried | HTTP |
|---|---|
| edit scout | 200 |
### `PUT /api/admin/roster/scouts/{id}/checks/{item}`
Capability `roster:write`. Mark an item collected for this year: body {done: true|false, note}.
| Tried | HTTP |
|---|---|
| mark dues | 200 |
| bad item | 422 |
### `GET /api/admin/family`
Capability `account:self`. The signed-in person's own families: scouts, dens, and this year's
| Tried | HTTP |
|---|---|
| member sees own family | 200 |
| unlinked account sees none | 200 |
| inactive family drops off | 200 |
| anon | 401 |
## Calendar
Writes go to Radicale. The site owns its two UID namespaces and refuses any other before a network call. DELETE really deletes.
### `GET /api/admin/calendar`
Capability `calendar:write`. Every event the public calendar shows, newest first is NOT the order:
| Tried | HTTP |
|---|---|
| member cannot | 403 |
| list | 200 |
### `POST /api/admin/calendar`
Capability `calendar:write`. Add an event. Body: title, date (YYYY-MM-DD), unit (pack|troop|both),
| Tried | HTTP |
|---|---|
| no title | 422 |
| create (2036) | 201 |
### `PUT /api/admin/calendar/{uid}`
Capability `calendar:write`. Replace one site-owned event in full (same body as POST). A UID the
| Tried | HTTP |
|---|---|
| replace | 200 |
| foreign uid | 403 |
### `DELETE /api/admin/calendar/{uid}`
Capability `calendar:write`. Remove one site-owned event from the store. Unlike everything else on
| Tried | HTTP |
|---|---|
| delete | 200 |
| delete again | 404 |
## Facebook posts
The publisher reports; leaders read, cancel and edit. The image hash is verified on arrival and the image route is gated like the list. Cancel and reschedule go through the publisher's signed per-post link.
### `GET /api/admin/fbposts`
Capability `fbposts:read`. Every post the publisher has reported, newest scheduled first, with
| Tried | HTTP |
|---|---|
| member cannot | 403 |
| listed as scheduled | 200 |
| published dropped from open list | 200 |
### `POST /api/admin/fbposts/ingest`
Capability `fbposts:ingest`. scout-publisher reports a post. Body: id (<unit>/<queue-stem>), unit,
| Tried | HTTP |
|---|---|
| leader cannot ingest | 403 |
| hash mismatch refused | 422 |
| ingest with image | 200 |
| past time reads as published | 200 |
### `GET /api/admin/fbposts/{id}/image`
Capability `fbposts:read`. The stored image, behind the same gate as the list.
| Tried | HTTP |
|---|---|
| image anon | 401 |
| image gated | 200 |
### `POST /api/admin/fbposts/{id}/cancel`
Capability `fbposts:read`. Cancel a scheduled post through the publisher's own signed link, then
| Tried | HTTP |
|---|---|
| cancel: publisher refuses a bad signature | 502 |
| cancel after time passed | 409 |
### `POST /api/admin/fbposts/{id}/reschedule`
Capability `fbposts:read`. Edit and repost a scheduled post: body {message, scheduled_for}. The
| Tried | HTTP |
|---|---|
| reschedule: time too soon | 422 |
| reschedule: publisher refuses a bad signature | 502 |